payweave 0.0.0

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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +272 -0
  3. package/dist/_tsup-dts-rollup.d.ts +19470 -0
  4. package/dist/chunk-4R5OGOCC.js +94 -0
  5. package/dist/chunk-4R5OGOCC.js.map +1 -0
  6. package/dist/chunk-535Q3GUN.js +175 -0
  7. package/dist/chunk-535Q3GUN.js.map +1 -0
  8. package/dist/chunk-67NRYGRC.js +192 -0
  9. package/dist/chunk-67NRYGRC.js.map +1 -0
  10. package/dist/chunk-CGQ73I4I.js +568 -0
  11. package/dist/chunk-CGQ73I4I.js.map +1 -0
  12. package/dist/chunk-CJEYDHY6.js +18 -0
  13. package/dist/chunk-CJEYDHY6.js.map +1 -0
  14. package/dist/chunk-CR63JAAS.js +3 -0
  15. package/dist/chunk-CR63JAAS.js.map +1 -0
  16. package/dist/chunk-E633MKVX.js +1507 -0
  17. package/dist/chunk-E633MKVX.js.map +1 -0
  18. package/dist/chunk-FBPOCXRZ.js +1135 -0
  19. package/dist/chunk-FBPOCXRZ.js.map +1 -0
  20. package/dist/chunk-HTRGMZFH.js +267 -0
  21. package/dist/chunk-HTRGMZFH.js.map +1 -0
  22. package/dist/chunk-IUVGM3K7.js +765 -0
  23. package/dist/chunk-IUVGM3K7.js.map +1 -0
  24. package/dist/chunk-JD4OZHRI.js +326 -0
  25. package/dist/chunk-JD4OZHRI.js.map +1 -0
  26. package/dist/chunk-NV3MZF7L.js +139 -0
  27. package/dist/chunk-NV3MZF7L.js.map +1 -0
  28. package/dist/chunk-NXQBP35U.js +13 -0
  29. package/dist/chunk-NXQBP35U.js.map +1 -0
  30. package/dist/chunk-QJHXDZYC.js +412 -0
  31. package/dist/chunk-QJHXDZYC.js.map +1 -0
  32. package/dist/chunk-TWZK62TZ.js +83 -0
  33. package/dist/chunk-TWZK62TZ.js.map +1 -0
  34. package/dist/cli/index.js +35381 -0
  35. package/dist/core/index.d.ts +85 -0
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -0
  38. package/dist/db/drizzle/index.d.ts +6 -0
  39. package/dist/db/drizzle/index.js +1532 -0
  40. package/dist/db/drizzle/index.js.map +1 -0
  41. package/dist/db/index.d.ts +43 -0
  42. package/dist/db/index.js +3 -0
  43. package/dist/db/index.js.map +1 -0
  44. package/dist/db/mongodb/index.d.ts +3 -0
  45. package/dist/db/mongodb/index.js +934 -0
  46. package/dist/db/mongodb/index.js.map +1 -0
  47. package/dist/db/mysql/index.d.ts +1 -0
  48. package/dist/db/mysql/index.js +12 -0
  49. package/dist/db/mysql/index.js.map +1 -0
  50. package/dist/db/postgres/index.d.ts +5 -0
  51. package/dist/db/postgres/index.js +791 -0
  52. package/dist/db/postgres/index.js.map +1 -0
  53. package/dist/db/prisma/index.d.ts +1 -0
  54. package/dist/db/prisma/index.js +12 -0
  55. package/dist/db/prisma/index.js.map +1 -0
  56. package/dist/db/sqlite/index.d.ts +5 -0
  57. package/dist/db/sqlite/index.js +750 -0
  58. package/dist/db/sqlite/index.js.map +1 -0
  59. package/dist/express/index.d.ts +1 -0
  60. package/dist/express/index.js +3 -0
  61. package/dist/express/index.js.map +1 -0
  62. package/dist/fastify/index.d.ts +1 -0
  63. package/dist/fastify/index.js +3 -0
  64. package/dist/fastify/index.js.map +1 -0
  65. package/dist/flutterwave/index.d.ts +27 -0
  66. package/dist/flutterwave/index.js +9 -0
  67. package/dist/flutterwave/index.js.map +1 -0
  68. package/dist/index.d.ts +85 -0
  69. package/dist/index.js +3677 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/next/index.d.ts +1 -0
  72. package/dist/next/index.js +3 -0
  73. package/dist/next/index.js.map +1 -0
  74. package/dist/paystack/index.d.ts +33 -0
  75. package/dist/paystack/index.js +9 -0
  76. package/dist/paystack/index.js.map +1 -0
  77. package/dist/products/index.d.ts +22 -0
  78. package/dist/products/index.js +7 -0
  79. package/dist/products/index.js.map +1 -0
  80. package/dist/testing/index.d.ts +11 -0
  81. package/dist/testing/index.js +69 -0
  82. package/dist/testing/index.js.map +1 -0
  83. package/dist/unified/index.d.ts +41 -0
  84. package/dist/unified/index.js +6 -0
  85. package/dist/unified/index.js.map +1 -0
  86. package/dist/webhooks/index.d.ts +8 -0
  87. package/dist/webhooks/index.js +8 -0
  88. package/dist/webhooks/index.js.map +1 -0
  89. package/package.json +164 -0
@@ -0,0 +1,791 @@
1
+ import { applyMigrations, planMigrations } from '../../chunk-QJHXDZYC.js';
2
+ import { PW_TABLES, PW_ACTIVE_SUBSCRIPTION_STATUSES, DEFAULT_STALE_CLAIM_AFTER_MS } from '../../chunk-NV3MZF7L.js';
3
+ import { currentPeriod } from '../../chunk-4R5OGOCC.js';
4
+ import { PayweaveConfigError, PayweaveNotFoundError, PayweaveError, PayweaveValidationError } from '../../chunk-HTRGMZFH.js';
5
+ import { randomInt } from 'crypto';
6
+
7
+ // src/db/postgres/errors.ts
8
+ function installHintError(cause) {
9
+ return new PayweaveConfigError(
10
+ 'payweave/db/postgres: this adapter requires the "pg" package, which is not installed (it is an optional peerDependency of "payweave"). Run `npm install pg` (or your package manager\'s equivalent) and try again.',
11
+ { cause }
12
+ );
13
+ }
14
+ var CONSTRAINT_VIOLATION_CODES = /* @__PURE__ */ new Set([
15
+ "23505",
16
+ // unique_violation — incl. the partial-unique active-subscription index
17
+ "23P01",
18
+ // exclusion_violation
19
+ "23503",
20
+ // foreign_key_violation
21
+ "23514"
22
+ // check_violation
23
+ ]);
24
+ function pgErrorCode(error) {
25
+ const code = error?.code;
26
+ return typeof code === "string" ? code : void 0;
27
+ }
28
+ function isConstraintViolation(error) {
29
+ const code = pgErrorCode(error);
30
+ return code !== void 0 && CONSTRAINT_VIOLATION_CODES.has(code);
31
+ }
32
+ function wrapDriverError(error, message) {
33
+ if (error instanceof PayweaveError) return error;
34
+ if (isConstraintViolation(error)) {
35
+ return new PayweaveValidationError(message, { cause: error });
36
+ }
37
+ return new PayweaveError(message, { cause: error, isRetryable: false });
38
+ }
39
+
40
+ // src/db/postgres/runner.ts
41
+ var PoolRunner = class {
42
+ #pool;
43
+ constructor(pool) {
44
+ this.#pool = pool;
45
+ }
46
+ query(text, params = []) {
47
+ return this.#pool.query(text, params);
48
+ }
49
+ async transaction(fn) {
50
+ const client = await this.#pool.connect();
51
+ try {
52
+ await client.query("BEGIN");
53
+ const txRunner = new ClientRunner(client);
54
+ try {
55
+ const result = await fn(txRunner);
56
+ await client.query("COMMIT");
57
+ return result;
58
+ } catch (error) {
59
+ await client.query("ROLLBACK").catch(() => void 0);
60
+ throw error;
61
+ }
62
+ } finally {
63
+ client.release();
64
+ }
65
+ }
66
+ };
67
+ var ClientRunner = class {
68
+ #client;
69
+ constructor(client) {
70
+ this.#client = client;
71
+ }
72
+ query(text, params = []) {
73
+ return this.#client.query(text, params);
74
+ }
75
+ // Reentrant: already inside one atomic unit, so nested `transaction()`
76
+ // calls (e.g. a user's `db.transaction(tx => tx.plans.pushVersion(...))`)
77
+ // just run inline rather than attempting a nested BEGIN.
78
+ transaction(fn) {
79
+ return fn(this);
80
+ }
81
+ };
82
+
83
+ // src/db/postgres/url.ts
84
+ var VALID_SCHEMES = ["postgres://", "postgresql://"];
85
+ function assertPostgresConnectionString(value) {
86
+ if (typeof value !== "string" || value.length === 0) {
87
+ throw new PayweaveConfigError(
88
+ `payweave/db/postgres: postgresAdapter({ connectionString }) requires a non-empty string \u2014 got ${JSON.stringify(value)}.`
89
+ );
90
+ }
91
+ if (!VALID_SCHEMES.some((scheme) => value.startsWith(scheme))) {
92
+ throw new PayweaveConfigError(
93
+ `payweave/db/postgres: connectionString must start with "postgres://" or "postgresql://" \u2014 got an unsupported scheme. Never log or include the full string in an error message (it may carry credentials); check the beginning of your DATABASE_URL.`
94
+ );
95
+ }
96
+ return value;
97
+ }
98
+ function looksLikePgPool(value) {
99
+ const candidate = value;
100
+ return typeof candidate.query === "function" && typeof candidate.connect === "function";
101
+ }
102
+ function resolvePostgresInput(input) {
103
+ if (input === null || typeof input !== "object") {
104
+ throw new PayweaveConfigError(
105
+ `payweave/db/postgres: postgresAdapter(...) expects { connectionString } (a postgres:///postgresql:// URL) or an existing \`pg\` Pool instance \u2014 got ${input === null ? "null" : typeof input}.`
106
+ );
107
+ }
108
+ if ("connectionString" in input) {
109
+ const connectionString = assertPostgresConnectionString(
110
+ input.connectionString
111
+ );
112
+ return { kind: "connectionString", connectionString };
113
+ }
114
+ if (looksLikePgPool(input)) {
115
+ return { kind: "pool-instance", pool: input };
116
+ }
117
+ throw new PayweaveConfigError(
118
+ "payweave/db/postgres: postgresAdapter(...) expects { connectionString } (a postgres:///postgresql:// URL) or an existing `pg` Pool instance (has .query()/.connect()) \u2014 got an object matching neither shape."
119
+ );
120
+ }
121
+ var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
122
+ var ENCODING_LEN = ENCODING.length;
123
+ var TIME_LEN = 10;
124
+ var RANDOM_LEN = 16;
125
+ function encodeTime(timeMs, len) {
126
+ if (!Number.isSafeInteger(timeMs) || timeMs < 0) {
127
+ throw new RangeError(`ulid: time must be a non-negative safe integer \u2014 got ${timeMs}`);
128
+ }
129
+ let remaining = timeMs;
130
+ let out = "";
131
+ for (let i = 0; i < len; i++) {
132
+ const mod = remaining % ENCODING_LEN;
133
+ out = ENCODING.charAt(mod) + out;
134
+ remaining = Math.floor(remaining / ENCODING_LEN);
135
+ }
136
+ return out;
137
+ }
138
+ function encodeRandom(len) {
139
+ let out = "";
140
+ for (let i = 0; i < len; i++) {
141
+ out += ENCODING.charAt(randomInt(0, ENCODING_LEN));
142
+ }
143
+ return out;
144
+ }
145
+ function ulid(timeMs = Date.now()) {
146
+ return encodeTime(timeMs, TIME_LEN) + encodeRandom(RANDOM_LEN);
147
+ }
148
+ function generatePwId(timeMs = Date.now()) {
149
+ return `pwv_${ulid(timeMs)}`;
150
+ }
151
+
152
+ // src/db/postgres/migrations-executor.ts
153
+ var PostgresMigrationExecutor = class _PostgresMigrationExecutor {
154
+ #runner;
155
+ constructor(runner) {
156
+ this.#runner = runner;
157
+ }
158
+ async query(sql, params = []) {
159
+ const { rows } = await this.#runner.query(sql, params);
160
+ return { rows };
161
+ }
162
+ async transaction(fn) {
163
+ return this.#runner.transaction((tx) => fn(new _PostgresMigrationExecutor(tx)));
164
+ }
165
+ };
166
+
167
+ // src/db/postgres/rows.ts
168
+ function requireColumn(row, column) {
169
+ if (!(column in row)) {
170
+ throw new Error(`postgres adapter: row is missing expected column "${column}"`);
171
+ }
172
+ return row[column];
173
+ }
174
+ function str(row, column) {
175
+ const value = requireColumn(row, column);
176
+ if (typeof value !== "string") {
177
+ throw new Error(`postgres adapter: expected column "${column}" to be a string, got ${typeof value}`);
178
+ }
179
+ return value;
180
+ }
181
+ function nullableStr(row, column) {
182
+ const value = requireColumn(row, column);
183
+ if (value === null || value === void 0) return null;
184
+ if (typeof value !== "string") {
185
+ throw new Error(
186
+ `postgres adapter: expected column "${column}" to be a string or null, got ${typeof value}`
187
+ );
188
+ }
189
+ return value;
190
+ }
191
+ function int(row, column) {
192
+ const value = requireColumn(row, column);
193
+ if (typeof value !== "number") {
194
+ throw new Error(`postgres adapter: expected column "${column}" to be a number, got ${typeof value}`);
195
+ }
196
+ return value;
197
+ }
198
+ function bigint(row, column) {
199
+ const value = requireColumn(row, column);
200
+ if (typeof value === "number") return value;
201
+ if (typeof value === "string") {
202
+ const n = Number(value);
203
+ if (!Number.isSafeInteger(n)) {
204
+ throw new Error(
205
+ `postgres adapter: column "${column}" (${value}) is outside Number.isSafeInteger range`
206
+ );
207
+ }
208
+ return n;
209
+ }
210
+ throw new Error(
211
+ `postgres adapter: expected column "${column}" to be a bigint string or number, got ${typeof value}`
212
+ );
213
+ }
214
+ function bool(row, column) {
215
+ const value = requireColumn(row, column);
216
+ if (typeof value !== "boolean") {
217
+ throw new Error(`postgres adapter: expected column "${column}" to be a boolean, got ${typeof value}`);
218
+ }
219
+ return value;
220
+ }
221
+ function dateOf(row, column) {
222
+ const value = requireColumn(row, column);
223
+ if (!(value instanceof Date)) {
224
+ throw new Error(`postgres adapter: expected column "${column}" to be a Date, got ${typeof value}`);
225
+ }
226
+ return value;
227
+ }
228
+ function json(row, column) {
229
+ return requireColumn(row, column);
230
+ }
231
+ function rowToCustomer(row) {
232
+ return {
233
+ id: str(row, "id"),
234
+ externalId: str(row, "external_id"),
235
+ providerIds: json(row, "provider_ids"),
236
+ email: nullableStr(row, "email"),
237
+ createdAt: dateOf(row, "created_at"),
238
+ updatedAt: dateOf(row, "updated_at")
239
+ };
240
+ }
241
+ function rowToPlanVersion(row) {
242
+ return {
243
+ id: str(row, "id"),
244
+ planId: str(row, "plan_id"),
245
+ version: int(row, "version"),
246
+ group: str(row, "group"),
247
+ isDefault: bool(row, "is_default"),
248
+ name: nullableStr(row, "name"),
249
+ priceMinor: (() => {
250
+ const v = requireColumn(row, "price_minor");
251
+ return v === null ? null : bigint(row, "price_minor");
252
+ })(),
253
+ priceCurrency: nullableStr(row, "price_currency"),
254
+ priceInterval: nullableStr(row, "price_interval"),
255
+ features: json(row, "features"),
256
+ providerRefs: json(row, "provider_refs"),
257
+ pushedAt: dateOf(row, "pushed_at")
258
+ };
259
+ }
260
+ function rowToSubscription(row) {
261
+ return {
262
+ id: str(row, "id"),
263
+ customerId: str(row, "customer_id"),
264
+ planId: str(row, "plan_id"),
265
+ planVersion: int(row, "plan_version"),
266
+ group: str(row, "group"),
267
+ status: str(row, "status"),
268
+ provider: nullableStr(row, "provider"),
269
+ providerSubscriptionRef: nullableStr(row, "provider_subscription_ref"),
270
+ currentPeriodStart: dateOf(row, "current_period_start"),
271
+ currentPeriodEnd: dateOf(row, "current_period_end"),
272
+ cancelAtPeriodEnd: bool(row, "cancel_at_period_end"),
273
+ createdAt: dateOf(row, "created_at"),
274
+ updatedAt: dateOf(row, "updated_at")
275
+ };
276
+ }
277
+ function rowToFeatureBalance(row) {
278
+ return {
279
+ id: str(row, "id"),
280
+ customerId: str(row, "customer_id"),
281
+ featureId: str(row, "feature_id"),
282
+ group: str(row, "group"),
283
+ used: bigint(row, "used"),
284
+ limit: bigint(row, "limit"),
285
+ resetInterval: str(row, "reset_interval"),
286
+ anchor: dateOf(row, "anchor"),
287
+ periodStart: dateOf(row, "period_start"),
288
+ periodEnd: dateOf(row, "period_end"),
289
+ planId: str(row, "plan_id"),
290
+ planVersion: int(row, "plan_version"),
291
+ updatedAt: dateOf(row, "updated_at")
292
+ };
293
+ }
294
+
295
+ // src/db/postgres/period-sql.ts
296
+ var DAY_MS = "86400000";
297
+ var WEEK_MS = "604800000";
298
+ function daysInMonthExpr(yearExpr, monthExpr) {
299
+ return `(EXTRACT(DAY FROM (make_date((${yearExpr})::int, (${monthExpr})::int, 1) + INTERVAL '1 month - 1 day')))::int`;
300
+ }
301
+ function clampedMonthAddExpr(anchorUtc, totalMonthsExpr) {
302
+ const rawMonths = `((EXTRACT(MONTH FROM ${anchorUtc}))::int - 1 + (${totalMonthsExpr}))`;
303
+ const year = `((EXTRACT(YEAR FROM ${anchorUtc}))::int + FLOOR((${rawMonths})::numeric / 12))::int`;
304
+ const monthIndex = `(MOD(MOD((${rawMonths})::int, 12) + 12, 12))::int`;
305
+ const month = `(${monthIndex} + 1)`;
306
+ const day = `LEAST((EXTRACT(DAY FROM ${anchorUtc}))::int, ${daysInMonthExpr(year, month)})`;
307
+ return `make_timestamptz(${year}, ${month}, (${day})::int, (EXTRACT(HOUR FROM ${anchorUtc}))::int, (EXTRACT(MINUTE FROM ${anchorUtc}))::int, (EXTRACT(SECOND FROM ${anchorUtc}))::double precision, 'UTC')`;
308
+ }
309
+ function advanceExpr(anchorRaw, resetIntervalRaw, nExpr) {
310
+ const anchorUtc = `(${anchorRaw} AT TIME ZONE 'UTC')`;
311
+ return `(CASE ${resetIntervalRaw}
312
+ WHEN 'day' THEN ${anchorRaw} + ((${nExpr}) * ${DAY_MS}) * INTERVAL '1 millisecond'
313
+ WHEN 'week' THEN ${anchorRaw} + ((${nExpr}) * ${WEEK_MS}) * INTERVAL '1 millisecond'
314
+ WHEN 'month' THEN ${clampedMonthAddExpr(anchorUtc, `(${nExpr}) * 1`)}
315
+ WHEN 'year' THEN ${clampedMonthAddExpr(anchorUtc, `(${nExpr}) * 12`)}
316
+ END)`;
317
+ }
318
+ function estimateExpr(anchorRaw, resetIntervalRaw, nowRaw) {
319
+ const anchorUtc = `(${anchorRaw} AT TIME ZONE 'UTC')`;
320
+ const nowUtc = `(${nowRaw} AT TIME ZONE 'UTC')`;
321
+ const diffMs = `ROUND(EXTRACT(EPOCH FROM (${nowRaw} - ${anchorRaw})) * 1000)`;
322
+ const monthDiff = `(((EXTRACT(YEAR FROM ${nowUtc}))::int - (EXTRACT(YEAR FROM ${anchorUtc}))::int) * 12 + ((EXTRACT(MONTH FROM ${nowUtc}))::int - (EXTRACT(MONTH FROM ${anchorUtc}))::int))`;
323
+ return `((CASE ${resetIntervalRaw}
324
+ WHEN 'day' THEN FLOOR((${diffMs})::numeric / ${DAY_MS})
325
+ WHEN 'week' THEN FLOOR((${diffMs})::numeric / ${WEEK_MS})
326
+ WHEN 'month' THEN FLOOR((${monthDiff})::numeric / 1)
327
+ WHEN 'year' THEN FLOOR((${monthDiff})::numeric / 12)
328
+ END)::bigint)`;
329
+ }
330
+ function buildPeriodMathSql(anchorRaw, resetIntervalRaw, nowRaw) {
331
+ const estimate = estimateExpr(anchorRaw, resetIntervalRaw, nowRaw);
332
+ const index0 = `(GREATEST(0, (${estimate}) - 1))`;
333
+ const adv = (offset) => advanceExpr(anchorRaw, resetIntervalRaw, `(${index0}) + ${offset}`);
334
+ const adv0 = adv(0);
335
+ const adv1 = adv(1);
336
+ const adv2 = adv(2);
337
+ const adv3 = adv(3);
338
+ const increments = `(CASE WHEN ${nowRaw} < ${adv1} THEN 0 WHEN ${nowRaw} < ${adv2} THEN 1 ELSE 2 END)`;
339
+ return {
340
+ periodStart: `(CASE ${increments} WHEN 0 THEN ${adv0} WHEN 1 THEN ${adv1} ELSE ${adv2} END)`,
341
+ periodEnd: `(CASE ${increments} WHEN 0 THEN ${adv1} WHEN 1 THEN ${adv2} ELSE ${adv3} END)`
342
+ };
343
+ }
344
+
345
+ // src/db/postgres/sql.ts
346
+ var FEATURE_BALANCES = PW_TABLES.featureBalances;
347
+ var WEBHOOK_EVENTS = PW_TABLES.webhookEvents;
348
+ function buildConsumeQuery(input, newId) {
349
+ const conditional = input.conditional === true;
350
+ const freshPeriod = currentPeriod(
351
+ input.init.anchor.getTime(),
352
+ input.init.resetInterval,
353
+ input.now.getTime()
354
+ );
355
+ const params = [
356
+ newId,
357
+ input.customerId,
358
+ input.featureId,
359
+ input.group,
360
+ input.now,
361
+ input.amount,
362
+ conditional,
363
+ input.init.limit,
364
+ input.init.resetInterval,
365
+ input.init.anchor,
366
+ input.init.planId,
367
+ input.init.planVersion,
368
+ new Date(freshPeriod.start),
369
+ new Date(freshPeriod.end)
370
+ ];
371
+ const resetDue = `($5::timestamptz >= e.period_end)`;
372
+ const baseUsed = `(CASE WHEN ${resetDue} THEN 0 ELSE e.used END)`;
373
+ const remaining = `(e."limit" - ${baseUsed})`;
374
+ const appliedExisting = `(($7::boolean) IS NOT TRUE OR ${remaining} >= $6::bigint)`;
375
+ const finalUsedExisting = `(CASE WHEN ${appliedExisting} THEN ${baseUsed} + $6::bigint ELSE ${baseUsed} END)`;
376
+ const updatedAtExisting = `(CASE WHEN (${appliedExisting} OR ${resetDue}) THEN $5::timestamptz ELSE e.updated_at END)`;
377
+ const existingPeriod = buildPeriodMathSql("e.anchor", "e.reset_interval", "$5::timestamptz");
378
+ const periodStartExisting = `(CASE WHEN ${resetDue} THEN ${existingPeriod.periodStart} ELSE e.period_start END)`;
379
+ const periodEndExisting = `(CASE WHEN ${resetDue} THEN ${existingPeriod.periodEnd} ELSE e.period_end END)`;
380
+ const freshDenied = `(($7::boolean) IS TRUE AND $8::bigint < $6::bigint)`;
381
+ const freshUsed = `(CASE WHEN ${freshDenied} THEN 0 ELSE $6::bigint END)`;
382
+ const freshApplied = `(CASE WHEN ${freshDenied} THEN FALSE ELSE TRUE END)`;
383
+ const text = `
384
+ WITH existing AS (
385
+ SELECT * FROM ${FEATURE_BALANCES}
386
+ WHERE customer_id = $2 AND feature_id = $3 AND "group" = $4
387
+ FOR UPDATE
388
+ ),
389
+ computed AS (
390
+ SELECT
391
+ CASE WHEN e.id IS NULL THEN ${freshUsed} ELSE ${finalUsedExisting} END AS used,
392
+ CASE WHEN e.id IS NULL THEN $13::timestamptz ELSE ${periodStartExisting} END AS period_start,
393
+ CASE WHEN e.id IS NULL THEN $14::timestamptz ELSE ${periodEndExisting} END AS period_end,
394
+ CASE WHEN e.id IS NULL THEN $5::timestamptz ELSE ${updatedAtExisting} END AS updated_at,
395
+ CASE WHEN e.id IS NULL THEN ${freshApplied} ELSE ${appliedExisting} END AS applied
396
+ FROM (SELECT 1 AS one) dummy
397
+ LEFT JOIN existing e ON true
398
+ )
399
+ INSERT INTO ${FEATURE_BALANCES}
400
+ (id, customer_id, feature_id, "group", used, "limit", reset_interval, anchor, period_start, period_end, plan_id, plan_version, updated_at)
401
+ SELECT $1, $2, $3, $4, computed.used, $8::bigint, $9::text, $10::timestamptz, computed.period_start, computed.period_end, $11::text, $12::int, computed.updated_at
402
+ FROM computed
403
+ ON CONFLICT (customer_id, feature_id, "group") DO UPDATE SET
404
+ used = (SELECT used FROM computed),
405
+ period_start = (SELECT period_start FROM computed),
406
+ period_end = (SELECT period_end FROM computed),
407
+ updated_at = (SELECT updated_at FROM computed)
408
+ RETURNING id, customer_id, feature_id, "group", used, "limit", reset_interval, anchor, period_start, period_end, plan_id, plan_version, updated_at,
409
+ (SELECT applied FROM computed) AS applied
410
+ `.trim();
411
+ return { text, params };
412
+ }
413
+ function buildClaimQuery(dedupeKey, meta, defaultStaleClaimAfterMs) {
414
+ const staleClaimAfterMs = meta.staleClaimAfterMs ?? defaultStaleClaimAfterMs;
415
+ const staleThreshold = new Date(meta.now.getTime() - staleClaimAfterMs);
416
+ const text = `
417
+ INSERT INTO ${WEBHOOK_EVENTS} (dedupe_key, provider, type, received_at, claimed_at, applied_at)
418
+ VALUES ($1, $2, $3, $4::timestamptz, $5::timestamptz, NULL)
419
+ ON CONFLICT (dedupe_key) DO UPDATE SET claimed_at = EXCLUDED.claimed_at
420
+ WHERE ${WEBHOOK_EVENTS}.applied_at IS NULL AND ${WEBHOOK_EVENTS}.claimed_at <= $6::timestamptz
421
+ RETURNING dedupe_key
422
+ `.trim();
423
+ return {
424
+ text,
425
+ params: [dedupeKey, meta.provider, meta.type, meta.now, meta.now, staleThreshold]
426
+ };
427
+ }
428
+
429
+ // src/db/postgres/adapter.ts
430
+ var ACTIVE_STATUS_PLACEHOLDERS = PW_ACTIVE_SUBSCRIPTION_STATUSES.map((_, i) => `$${i + 3}`).join(", ");
431
+ function stableStringify(value) {
432
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
433
+ if (value !== null && typeof value === "object") {
434
+ const entries = Object.entries(value).sort(
435
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
436
+ );
437
+ return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`).join(",")}}`;
438
+ }
439
+ return JSON.stringify(value);
440
+ }
441
+ var planContent = (p) => ({
442
+ group: p.group,
443
+ isDefault: p.isDefault,
444
+ name: p.name,
445
+ priceMinor: p.priceMinor,
446
+ priceCurrency: p.priceCurrency,
447
+ priceInterval: p.priceInterval,
448
+ features: p.features,
449
+ providerRefs: p.providerRefs
450
+ });
451
+ function planContentEquals(active, input) {
452
+ return stableStringify(planContent(active)) === stableStringify(planContent(input));
453
+ }
454
+ var SUBSCRIPTION_PATCH_FIELDS = [
455
+ { key: "planId", column: "plan_id" },
456
+ { key: "planVersion", column: "plan_version" },
457
+ { key: "status", column: "status" },
458
+ { key: "provider", column: "provider" },
459
+ { key: "providerSubscriptionRef", column: "provider_subscription_ref" },
460
+ { key: "currentPeriodStart", column: "current_period_start" },
461
+ { key: "currentPeriodEnd", column: "current_period_end" },
462
+ { key: "cancelAtPeriodEnd", column: "cancel_at_period_end" }
463
+ ];
464
+ function buildAdapter(runner) {
465
+ return {
466
+ dialect: "postgres",
467
+ customers: {
468
+ getByExternalId: (externalId) => customersGetByExternalId(runner, externalId),
469
+ upsert: (input) => customersUpsert(runner, input),
470
+ linkProviderRef: (externalId, provider, ref) => customersLinkProviderRef(runner, externalId, provider, ref)
471
+ },
472
+ plans: {
473
+ getActiveVersion: (planId) => plansGetActiveVersion(runner, planId),
474
+ listActive: () => plansListActive(runner),
475
+ pushVersion: (plan) => plansPushVersion(runner, plan)
476
+ },
477
+ subscriptions: {
478
+ getActive: (customerId, group) => subscriptionsGetActive(runner, customerId, group),
479
+ create: (input) => subscriptionsCreate(runner, input),
480
+ update: (id, patch) => subscriptionsUpdate(runner, id, patch)
481
+ },
482
+ balances: {
483
+ get: (customerId, featureId, group) => balancesGet(runner, customerId, featureId, group),
484
+ consume: (input) => balancesConsume(runner, input),
485
+ resetTo: (customerId, featureId, group, init) => balancesResetTo(runner, customerId, featureId, group, init)
486
+ },
487
+ webhookEvents: {
488
+ claim: (dedupeKey, meta) => webhookEventsClaim(runner, dedupeKey, meta),
489
+ markApplied: (dedupeKey) => webhookEventsMarkApplied(runner, dedupeKey)
490
+ },
491
+ migrations: {
492
+ status: () => planMigrations(new PostgresMigrationExecutor(runner), "postgres"),
493
+ apply: () => applyMigrations(new PostgresMigrationExecutor(runner), "postgres")
494
+ },
495
+ transaction: (fn) => runner.transaction((tx) => fn(buildAdapter(tx)))
496
+ };
497
+ }
498
+ async function customersGetByExternalId(runner, externalId) {
499
+ const { rows } = await runner.query(
500
+ `SELECT * FROM ${PW_TABLES.customers} WHERE external_id = $1`,
501
+ [externalId]
502
+ );
503
+ const row = rows[0];
504
+ return row ? rowToCustomer(row) : null;
505
+ }
506
+ async function customersUpsert(runner, input) {
507
+ const now = /* @__PURE__ */ new Date();
508
+ const id = generatePwId(now.getTime());
509
+ try {
510
+ const { rows } = await runner.query(
511
+ `INSERT INTO ${PW_TABLES.customers} (id, external_id, provider_ids, email, created_at, updated_at)
512
+ VALUES ($1, $2, $3::jsonb, $4, $5, $6)
513
+ ON CONFLICT (external_id) DO UPDATE SET
514
+ email = COALESCE(excluded.email, ${PW_TABLES.customers}.email),
515
+ updated_at = excluded.updated_at
516
+ RETURNING *`,
517
+ [id, input.externalId, JSON.stringify({}), input.email ?? null, now, now]
518
+ );
519
+ const row = rows[0];
520
+ if (!row) throw new Error("postgres adapter: customers.upsert returned no row");
521
+ return rowToCustomer(row);
522
+ } catch (error) {
523
+ throw wrapDriverError(
524
+ error,
525
+ `payweave/db/postgres: customers.upsert(${JSON.stringify(input.externalId)}) failed.`
526
+ );
527
+ }
528
+ }
529
+ async function customersLinkProviderRef(runner, externalId, provider, ref) {
530
+ await runner.transaction(async (tx) => {
531
+ const { rows } = await tx.query(
532
+ `SELECT * FROM ${PW_TABLES.customers} WHERE external_id = $1 FOR UPDATE`,
533
+ [externalId]
534
+ );
535
+ const row = rows[0];
536
+ if (!row) {
537
+ throw new PayweaveNotFoundError(
538
+ `payweave/db/postgres: customers.linkProviderRef: no customer with externalId ${JSON.stringify(externalId)}.`
539
+ );
540
+ }
541
+ const customer = rowToCustomer(row);
542
+ const providerIds = { ...customer.providerIds, [provider]: ref };
543
+ await tx.query(
544
+ `UPDATE ${PW_TABLES.customers} SET provider_ids = $1::jsonb, updated_at = $2 WHERE external_id = $3`,
545
+ [JSON.stringify(providerIds), /* @__PURE__ */ new Date(), externalId]
546
+ );
547
+ });
548
+ }
549
+ async function plansGetActiveVersion(runner, planId) {
550
+ const { rows } = await runner.query(
551
+ `SELECT * FROM ${PW_TABLES.plans} WHERE plan_id = $1 ORDER BY version DESC LIMIT 1`,
552
+ [planId]
553
+ );
554
+ const row = rows[0];
555
+ return row ? rowToPlanVersion(row) : null;
556
+ }
557
+ async function plansListActive(runner) {
558
+ const { rows } = await runner.query(
559
+ `SELECT p.* FROM ${PW_TABLES.plans} p
560
+ INNER JOIN (
561
+ SELECT plan_id, MAX(version) AS version FROM ${PW_TABLES.plans} GROUP BY plan_id
562
+ ) latest ON latest.plan_id = p.plan_id AND latest.version = p.version`
563
+ );
564
+ return rows.map(rowToPlanVersion);
565
+ }
566
+ async function plansPushVersion(runner, plan) {
567
+ return runner.transaction(async (tx) => {
568
+ const { rows } = await tx.query(
569
+ `SELECT * FROM ${PW_TABLES.plans} WHERE plan_id = $1 ORDER BY version DESC LIMIT 1 FOR UPDATE`,
570
+ [plan.planId]
571
+ );
572
+ const activeRow = rows[0];
573
+ const active = activeRow ? rowToPlanVersion(activeRow) : null;
574
+ if (active && planContentEquals(active, plan)) {
575
+ return active;
576
+ }
577
+ const now = /* @__PURE__ */ new Date();
578
+ const id = generatePwId(now.getTime());
579
+ const nextVersion = (active?.version ?? 0) + 1;
580
+ const { rows: inserted } = await tx.query(
581
+ `INSERT INTO ${PW_TABLES.plans}
582
+ (id, plan_id, version, "group", is_default, name, price_minor, price_currency, price_interval, features, provider_refs, pushed_at)
583
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)
584
+ RETURNING *`,
585
+ [
586
+ id,
587
+ plan.planId,
588
+ nextVersion,
589
+ plan.group,
590
+ plan.isDefault,
591
+ plan.name,
592
+ plan.priceMinor,
593
+ plan.priceCurrency,
594
+ plan.priceInterval,
595
+ JSON.stringify(plan.features),
596
+ JSON.stringify(plan.providerRefs),
597
+ now
598
+ ]
599
+ );
600
+ const row = inserted[0];
601
+ if (!row) throw new Error("postgres adapter: plans.pushVersion returned no row");
602
+ return rowToPlanVersion(row);
603
+ });
604
+ }
605
+ async function subscriptionsGetActive(runner, customerId, group) {
606
+ const { rows } = await runner.query(
607
+ `SELECT * FROM ${PW_TABLES.subscriptions}
608
+ WHERE customer_id = $1 AND "group" = $2 AND status IN (${ACTIVE_STATUS_PLACEHOLDERS})`,
609
+ [customerId, group, ...PW_ACTIVE_SUBSCRIPTION_STATUSES]
610
+ );
611
+ const row = rows[0];
612
+ return row ? rowToSubscription(row) : null;
613
+ }
614
+ async function subscriptionsCreate(runner, input) {
615
+ const now = /* @__PURE__ */ new Date();
616
+ const id = generatePwId(now.getTime());
617
+ try {
618
+ const { rows } = await runner.query(
619
+ `INSERT INTO ${PW_TABLES.subscriptions}
620
+ (id, customer_id, plan_id, plan_version, "group", status, provider, provider_subscription_ref,
621
+ current_period_start, current_period_end, cancel_at_period_end, created_at, updated_at)
622
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
623
+ RETURNING *`,
624
+ [
625
+ id,
626
+ input.customerId,
627
+ input.planId,
628
+ input.planVersion,
629
+ input.group,
630
+ input.status,
631
+ input.provider,
632
+ input.providerSubscriptionRef,
633
+ input.currentPeriodStart,
634
+ input.currentPeriodEnd,
635
+ input.cancelAtPeriodEnd,
636
+ now,
637
+ now
638
+ ]
639
+ );
640
+ const row = rows[0];
641
+ if (!row) throw new Error("postgres adapter: subscriptions.create returned no row");
642
+ return rowToSubscription(row);
643
+ } catch (error) {
644
+ throw wrapDriverError(
645
+ error,
646
+ `payweave/db/postgres: subscriptions.create for customer ${JSON.stringify(input.customerId)} group ${JSON.stringify(input.group)} violated a uniqueness rule (an active-set subscription already exists for this customer/group, or another constraint failed).`
647
+ );
648
+ }
649
+ }
650
+ async function subscriptionsUpdate(runner, id, patch) {
651
+ const sets = [];
652
+ const params = [];
653
+ const patchRecord = patch;
654
+ for (const field of SUBSCRIPTION_PATCH_FIELDS) {
655
+ if (!(field.key in patch) || patchRecord[field.key] === void 0) continue;
656
+ params.push(patchRecord[field.key]);
657
+ sets.push(`${field.column} = $${params.length}`);
658
+ }
659
+ params.push(/* @__PURE__ */ new Date());
660
+ sets.push(`updated_at = $${params.length}`);
661
+ params.push(id);
662
+ const idParamIndex = params.length;
663
+ try {
664
+ const { rows } = await runner.query(
665
+ `UPDATE ${PW_TABLES.subscriptions} SET ${sets.join(", ")} WHERE id = $${idParamIndex} RETURNING *`,
666
+ params
667
+ );
668
+ const row = rows[0];
669
+ if (!row) {
670
+ throw new PayweaveNotFoundError(
671
+ `payweave/db/postgres: subscriptions.update: no subscription with id ${JSON.stringify(id)}.`
672
+ );
673
+ }
674
+ return rowToSubscription(row);
675
+ } catch (error) {
676
+ if (error instanceof PayweaveNotFoundError) throw error;
677
+ throw wrapDriverError(error, `payweave/db/postgres: subscriptions.update(${JSON.stringify(id)}) failed.`);
678
+ }
679
+ }
680
+ async function balancesGet(runner, customerId, featureId, group) {
681
+ const { rows } = await runner.query(
682
+ `SELECT * FROM ${PW_TABLES.featureBalances} WHERE customer_id = $1 AND feature_id = $2 AND "group" = $3`,
683
+ [customerId, featureId, group]
684
+ );
685
+ const row = rows[0];
686
+ return row ? rowToFeatureBalance(row) : null;
687
+ }
688
+ async function balancesConsume(runner, input) {
689
+ const id = generatePwId(input.now.getTime());
690
+ const { text, params } = buildConsumeQuery(input, id);
691
+ try {
692
+ const { rows } = await runner.query(text, params);
693
+ const row = rows[0];
694
+ if (!row) throw new Error("postgres adapter: balances.consume returned no row");
695
+ const applied = row.applied;
696
+ if (typeof applied !== "boolean") {
697
+ throw new Error(
698
+ `postgres adapter: balances.consume's RETURNING "applied" column was ${typeof applied}, expected boolean`
699
+ );
700
+ }
701
+ return { ...rowToFeatureBalance(row), applied };
702
+ } catch (error) {
703
+ throw wrapDriverError(
704
+ error,
705
+ `payweave/db/postgres: balances.consume for customer ${JSON.stringify(input.customerId)} feature ${JSON.stringify(input.featureId)} group ${JSON.stringify(input.group)} failed.`
706
+ );
707
+ }
708
+ }
709
+ async function balancesResetTo(runner, customerId, featureId, group, init) {
710
+ const now = /* @__PURE__ */ new Date();
711
+ const period = currentPeriod(init.anchor.getTime(), init.resetInterval, now.getTime());
712
+ const id = generatePwId(now.getTime());
713
+ await runner.query(
714
+ `INSERT INTO ${PW_TABLES.featureBalances}
715
+ (id, customer_id, feature_id, "group", used, "limit", reset_interval, anchor, period_start, period_end, plan_id, plan_version, updated_at)
716
+ VALUES ($1, $2, $3, $4, 0, $5, $6, $7, $8, $9, $10, $11, $12)
717
+ ON CONFLICT (customer_id, feature_id, "group") DO UPDATE SET
718
+ used = 0,
719
+ "limit" = excluded."limit",
720
+ reset_interval = excluded.reset_interval,
721
+ anchor = excluded.anchor,
722
+ period_start = excluded.period_start,
723
+ period_end = excluded.period_end,
724
+ plan_id = excluded.plan_id,
725
+ plan_version = excluded.plan_version,
726
+ updated_at = excluded.updated_at`,
727
+ [
728
+ id,
729
+ customerId,
730
+ featureId,
731
+ group,
732
+ init.limit,
733
+ init.resetInterval,
734
+ init.anchor,
735
+ new Date(period.start),
736
+ new Date(period.end),
737
+ init.planId,
738
+ init.planVersion,
739
+ now
740
+ ]
741
+ );
742
+ }
743
+ async function webhookEventsClaim(runner, dedupeKey, meta) {
744
+ const { text, params } = buildClaimQuery(dedupeKey, meta, DEFAULT_STALE_CLAIM_AFTER_MS);
745
+ const { rows } = await runner.query(text, params);
746
+ return rows.length > 0;
747
+ }
748
+ async function webhookEventsMarkApplied(runner, dedupeKey) {
749
+ await runner.query(
750
+ `UPDATE ${PW_TABLES.webhookEvents} SET applied_at = $1 WHERE dedupe_key = $2`,
751
+ [/* @__PURE__ */ new Date(), dedupeKey]
752
+ );
753
+ }
754
+
755
+ // src/db/postgres/index.ts
756
+ async function openPool(connectionString) {
757
+ let mod;
758
+ try {
759
+ mod = await import('pg');
760
+ } catch (cause) {
761
+ throw installHintError(cause);
762
+ }
763
+ const Pool = (mod.default ?? mod).Pool;
764
+ return new Pool({ connectionString });
765
+ }
766
+ function connect(target) {
767
+ return target.kind === "pool-instance" ? Promise.resolve(target.pool) : openPool(target.connectionString);
768
+ }
769
+ function postgresAdapter(input) {
770
+ const target = resolvePostgresInput(input);
771
+ let poolPromise;
772
+ const getPool = () => {
773
+ poolPromise ??= connect(target);
774
+ return poolPromise;
775
+ };
776
+ const runner = {
777
+ query: async (text, params = []) => {
778
+ const pool = await getPool();
779
+ return pool.query(text, params);
780
+ },
781
+ transaction: async (fn) => {
782
+ const pool = await getPool();
783
+ return new PoolRunner(pool).transaction(fn);
784
+ }
785
+ };
786
+ return buildAdapter(runner);
787
+ }
788
+
789
+ export { postgresAdapter };
790
+ //# sourceMappingURL=index.js.map
791
+ //# sourceMappingURL=index.js.map