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.
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/dist/_tsup-dts-rollup.d.ts +19470 -0
- package/dist/chunk-4R5OGOCC.js +94 -0
- package/dist/chunk-4R5OGOCC.js.map +1 -0
- package/dist/chunk-535Q3GUN.js +175 -0
- package/dist/chunk-535Q3GUN.js.map +1 -0
- package/dist/chunk-67NRYGRC.js +192 -0
- package/dist/chunk-67NRYGRC.js.map +1 -0
- package/dist/chunk-CGQ73I4I.js +568 -0
- package/dist/chunk-CGQ73I4I.js.map +1 -0
- package/dist/chunk-CJEYDHY6.js +18 -0
- package/dist/chunk-CJEYDHY6.js.map +1 -0
- package/dist/chunk-CR63JAAS.js +3 -0
- package/dist/chunk-CR63JAAS.js.map +1 -0
- package/dist/chunk-E633MKVX.js +1507 -0
- package/dist/chunk-E633MKVX.js.map +1 -0
- package/dist/chunk-FBPOCXRZ.js +1135 -0
- package/dist/chunk-FBPOCXRZ.js.map +1 -0
- package/dist/chunk-HTRGMZFH.js +267 -0
- package/dist/chunk-HTRGMZFH.js.map +1 -0
- package/dist/chunk-IUVGM3K7.js +765 -0
- package/dist/chunk-IUVGM3K7.js.map +1 -0
- package/dist/chunk-JD4OZHRI.js +326 -0
- package/dist/chunk-JD4OZHRI.js.map +1 -0
- package/dist/chunk-NV3MZF7L.js +139 -0
- package/dist/chunk-NV3MZF7L.js.map +1 -0
- package/dist/chunk-NXQBP35U.js +13 -0
- package/dist/chunk-NXQBP35U.js.map +1 -0
- package/dist/chunk-QJHXDZYC.js +412 -0
- package/dist/chunk-QJHXDZYC.js.map +1 -0
- package/dist/chunk-TWZK62TZ.js +83 -0
- package/dist/chunk-TWZK62TZ.js.map +1 -0
- package/dist/cli/index.js +35381 -0
- package/dist/core/index.d.ts +85 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/db/drizzle/index.d.ts +6 -0
- package/dist/db/drizzle/index.js +1532 -0
- package/dist/db/drizzle/index.js.map +1 -0
- package/dist/db/index.d.ts +43 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/mongodb/index.d.ts +3 -0
- package/dist/db/mongodb/index.js +934 -0
- package/dist/db/mongodb/index.js.map +1 -0
- package/dist/db/mysql/index.d.ts +1 -0
- package/dist/db/mysql/index.js +12 -0
- package/dist/db/mysql/index.js.map +1 -0
- package/dist/db/postgres/index.d.ts +5 -0
- package/dist/db/postgres/index.js +791 -0
- package/dist/db/postgres/index.js.map +1 -0
- package/dist/db/prisma/index.d.ts +1 -0
- package/dist/db/prisma/index.js +12 -0
- package/dist/db/prisma/index.js.map +1 -0
- package/dist/db/sqlite/index.d.ts +5 -0
- package/dist/db/sqlite/index.js +750 -0
- package/dist/db/sqlite/index.js.map +1 -0
- package/dist/express/index.d.ts +1 -0
- package/dist/express/index.js +3 -0
- package/dist/express/index.js.map +1 -0
- package/dist/fastify/index.d.ts +1 -0
- package/dist/fastify/index.js +3 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/flutterwave/index.d.ts +27 -0
- package/dist/flutterwave/index.js +9 -0
- package/dist/flutterwave/index.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +3677 -0
- package/dist/index.js.map +1 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +3 -0
- package/dist/next/index.js.map +1 -0
- package/dist/paystack/index.d.ts +33 -0
- package/dist/paystack/index.js +9 -0
- package/dist/paystack/index.js.map +1 -0
- package/dist/products/index.d.ts +22 -0
- package/dist/products/index.js +7 -0
- package/dist/products/index.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +69 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/unified/index.d.ts +41 -0
- package/dist/unified/index.js +6 -0
- package/dist/unified/index.js.map +1 -0
- package/dist/webhooks/index.d.ts +8 -0
- package/dist/webhooks/index.js +8 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +164 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import { PW_TABLES, PW_ACTIVE_SUBSCRIPTION_STATUSES, DEFAULT_STALE_CLAIM_AFTER_MS } from '../../chunk-NV3MZF7L.js';
|
|
2
|
+
import { currentPeriod, DAY_MS, WEEK_MS } from '../../chunk-4R5OGOCC.js';
|
|
3
|
+
import { PayweaveConfigError, PayweaveNotFoundError, PayweaveError, PayweaveValidationError } from '../../chunk-HTRGMZFH.js';
|
|
4
|
+
import { randomInt } from 'crypto';
|
|
5
|
+
|
|
6
|
+
// src/db/mongodb/errors.ts
|
|
7
|
+
function installHintError(cause) {
|
|
8
|
+
return new PayweaveConfigError(
|
|
9
|
+
'payweave/db/mongodb: connecting requires the "mongodb" package, which is not installed (it is an optional peerDependency of "payweave"). Run `npm install mongodb` (or your package manager\'s equivalent) and try again.',
|
|
10
|
+
{ cause }
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
var DUPLICATE_KEY_CODE = 11e3;
|
|
14
|
+
function isDuplicateKeyError(error) {
|
|
15
|
+
const code = error?.code;
|
|
16
|
+
return code === DUPLICATE_KEY_CODE;
|
|
17
|
+
}
|
|
18
|
+
function wrapDriverError(error, message) {
|
|
19
|
+
if (error instanceof PayweaveError) return error;
|
|
20
|
+
if (isDuplicateKeyError(error)) {
|
|
21
|
+
return new PayweaveValidationError(message, { cause: error });
|
|
22
|
+
}
|
|
23
|
+
return new PayweaveError(message, { cause: error, isRetryable: false });
|
|
24
|
+
}
|
|
25
|
+
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
26
|
+
var ENCODING_LEN = ENCODING.length;
|
|
27
|
+
var TIME_LEN = 10;
|
|
28
|
+
var RANDOM_LEN = 16;
|
|
29
|
+
function encodeTime(timeMs, len) {
|
|
30
|
+
if (!Number.isSafeInteger(timeMs) || timeMs < 0) {
|
|
31
|
+
throw new RangeError(`ulid: time must be a non-negative safe integer \u2014 got ${timeMs}`);
|
|
32
|
+
}
|
|
33
|
+
let remaining = timeMs;
|
|
34
|
+
let out = "";
|
|
35
|
+
for (let i = 0; i < len; i++) {
|
|
36
|
+
const mod = remaining % ENCODING_LEN;
|
|
37
|
+
out = ENCODING.charAt(mod) + out;
|
|
38
|
+
remaining = Math.floor(remaining / ENCODING_LEN);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function encodeRandom(len) {
|
|
43
|
+
let out = "";
|
|
44
|
+
for (let i = 0; i < len; i++) {
|
|
45
|
+
out += ENCODING.charAt(randomInt(0, ENCODING_LEN));
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function ulid(timeMs = Date.now()) {
|
|
50
|
+
return encodeTime(timeMs, TIME_LEN) + encodeRandom(RANDOM_LEN);
|
|
51
|
+
}
|
|
52
|
+
function generatePwId(timeMs = Date.now()) {
|
|
53
|
+
return `pwv_${ulid(timeMs)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/db/mongodb/migrations.ts
|
|
57
|
+
var MIGRATION_NAME = "0001_init";
|
|
58
|
+
var REQUIRED_INDEXES = [
|
|
59
|
+
{
|
|
60
|
+
collection: PW_TABLES.customers,
|
|
61
|
+
name: "pw_customers_externalId_unique",
|
|
62
|
+
key: { externalId: 1 },
|
|
63
|
+
options: { unique: true }
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
collection: PW_TABLES.plans,
|
|
67
|
+
name: "pw_plans_planId_version_unique",
|
|
68
|
+
key: { planId: 1, version: 1 },
|
|
69
|
+
options: { unique: true }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
collection: PW_TABLES.subscriptions,
|
|
73
|
+
name: "pw_subscriptions_active_unique",
|
|
74
|
+
key: { customerId: 1, group: 1 },
|
|
75
|
+
options: {
|
|
76
|
+
unique: true,
|
|
77
|
+
// The §2 partial-unique active-subscription rule: at most one row per
|
|
78
|
+
// (customerId, group) whose status is in the active set — everything
|
|
79
|
+
// else (canceled/incomplete, and any OTHER status a community fork
|
|
80
|
+
// might add) falls outside the filter and never collides.
|
|
81
|
+
partialFilterExpression: { status: { $in: [...PW_ACTIVE_SUBSCRIPTION_STATUSES] } }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
collection: PW_TABLES.featureBalances,
|
|
86
|
+
name: "pw_feature_balances_unique",
|
|
87
|
+
key: { customerId: 1, featureId: 1, group: 1 },
|
|
88
|
+
options: { unique: true }
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
function isNamespaceNotFound(error) {
|
|
92
|
+
const code = error?.code;
|
|
93
|
+
return code === 26;
|
|
94
|
+
}
|
|
95
|
+
async function existingIndexNames(db, collectionName) {
|
|
96
|
+
try {
|
|
97
|
+
const infos = await db.collection(collectionName).indexes();
|
|
98
|
+
return new Set(infos.map((i) => i.name).filter((n) => typeof n === "string"));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (isNamespaceNotFound(error)) return /* @__PURE__ */ new Set();
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function missingIndexes(db) {
|
|
105
|
+
const missing = [];
|
|
106
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const required of REQUIRED_INDEXES) {
|
|
108
|
+
let names = byCollection.get(required.collection);
|
|
109
|
+
if (!names) {
|
|
110
|
+
names = await existingIndexNames(db, required.collection);
|
|
111
|
+
byCollection.set(required.collection, names);
|
|
112
|
+
}
|
|
113
|
+
if (!names.has(required.name)) missing.push(required);
|
|
114
|
+
}
|
|
115
|
+
return missing;
|
|
116
|
+
}
|
|
117
|
+
async function mongoMigrationStatus(db) {
|
|
118
|
+
const missing = await missingIndexes(db);
|
|
119
|
+
return missing.length === 0 ? { pending: [], applied: [MIGRATION_NAME] } : { pending: [MIGRATION_NAME], applied: [] };
|
|
120
|
+
}
|
|
121
|
+
async function mongoMigrationApply(db) {
|
|
122
|
+
const missing = await missingIndexes(db);
|
|
123
|
+
if (missing.length === 0) return { applied: [] };
|
|
124
|
+
for (const required of missing) {
|
|
125
|
+
await db.collection(required.collection).createIndex(required.key, {
|
|
126
|
+
name: required.name,
|
|
127
|
+
...required.options
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { applied: [MIGRATION_NAME] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/db/mongodb/period-pipeline.ts
|
|
134
|
+
function buildPeriodExpr(nowMs, anchorExpr, resetExpr) {
|
|
135
|
+
const nowLiteral = new Date(nowMs);
|
|
136
|
+
return {
|
|
137
|
+
$let: {
|
|
138
|
+
vars: { anchor: anchorExpr, reset: resetExpr },
|
|
139
|
+
in: {
|
|
140
|
+
$switch: {
|
|
141
|
+
branches: [
|
|
142
|
+
{
|
|
143
|
+
case: { $in: ["$$reset", ["day", "week"]] },
|
|
144
|
+
then: {
|
|
145
|
+
$let: {
|
|
146
|
+
vars: { periodMs: { $cond: [{ $eq: ["$$reset", "day"] }, DAY_MS, WEEK_MS] } },
|
|
147
|
+
in: {
|
|
148
|
+
$let: {
|
|
149
|
+
vars: {
|
|
150
|
+
idx: {
|
|
151
|
+
$max: [
|
|
152
|
+
0,
|
|
153
|
+
{
|
|
154
|
+
$floor: {
|
|
155
|
+
$divide: [{ $subtract: [nowLiteral, "$$anchor"] }, "$$periodMs"]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
in: {
|
|
162
|
+
index: "$$idx",
|
|
163
|
+
start: { $add: ["$$anchor", { $multiply: ["$$idx", "$$periodMs"] }] },
|
|
164
|
+
end: {
|
|
165
|
+
$add: ["$$anchor", { $multiply: [{ $add: ["$$idx", 1] }, "$$periodMs"] }]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
default: {
|
|
175
|
+
$let: {
|
|
176
|
+
vars: {
|
|
177
|
+
monthsPerPeriod: { $cond: [{ $eq: ["$$reset", "year"] }, 12, 1] },
|
|
178
|
+
anchorParts: { $dateToParts: { date: "$$anchor", timezone: "UTC" } },
|
|
179
|
+
nowParts: { $dateToParts: { date: nowLiteral, timezone: "UTC" } }
|
|
180
|
+
},
|
|
181
|
+
in: {
|
|
182
|
+
$let: {
|
|
183
|
+
vars: {
|
|
184
|
+
monthDiff: {
|
|
185
|
+
$add: [
|
|
186
|
+
{ $multiply: [{ $subtract: ["$$nowParts.year", "$$anchorParts.year"] }, 12] },
|
|
187
|
+
{ $subtract: ["$$nowParts.month", "$$anchorParts.month"] }
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
in: {
|
|
192
|
+
$let: {
|
|
193
|
+
vars: {
|
|
194
|
+
idx0: {
|
|
195
|
+
$max: [
|
|
196
|
+
0,
|
|
197
|
+
{
|
|
198
|
+
$subtract: [
|
|
199
|
+
{ $floor: { $divide: ["$$monthDiff", "$$monthsPerPeriod"] } },
|
|
200
|
+
1
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
in: {
|
|
207
|
+
$let: {
|
|
208
|
+
vars: {
|
|
209
|
+
end0: {
|
|
210
|
+
$dateAdd: {
|
|
211
|
+
startDate: "$$anchor",
|
|
212
|
+
unit: "month",
|
|
213
|
+
amount: { $multiply: [{ $add: ["$$idx0", 1] }, "$$monthsPerPeriod"] }
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
end1: {
|
|
217
|
+
$dateAdd: {
|
|
218
|
+
startDate: "$$anchor",
|
|
219
|
+
unit: "month",
|
|
220
|
+
amount: { $multiply: [{ $add: ["$$idx0", 2] }, "$$monthsPerPeriod"] }
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
end2: {
|
|
224
|
+
$dateAdd: {
|
|
225
|
+
startDate: "$$anchor",
|
|
226
|
+
unit: "month",
|
|
227
|
+
amount: { $multiply: [{ $add: ["$$idx0", 3] }, "$$monthsPerPeriod"] }
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
end3: {
|
|
231
|
+
$dateAdd: {
|
|
232
|
+
startDate: "$$anchor",
|
|
233
|
+
unit: "month",
|
|
234
|
+
amount: { $multiply: [{ $add: ["$$idx0", 4] }, "$$monthsPerPeriod"] }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
in: {
|
|
239
|
+
$let: {
|
|
240
|
+
vars: {
|
|
241
|
+
chosenIdx: {
|
|
242
|
+
$switch: {
|
|
243
|
+
branches: [
|
|
244
|
+
{ case: { $lt: [nowLiteral, "$$end0"] }, then: "$$idx0" },
|
|
245
|
+
{
|
|
246
|
+
case: { $lt: [nowLiteral, "$$end1"] },
|
|
247
|
+
then: { $add: ["$$idx0", 1] }
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
case: { $lt: [nowLiteral, "$$end2"] },
|
|
251
|
+
then: { $add: ["$$idx0", 2] }
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
default: { $add: ["$$idx0", 3] }
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
chosenEnd: {
|
|
258
|
+
$switch: {
|
|
259
|
+
branches: [
|
|
260
|
+
{ case: { $lt: [nowLiteral, "$$end0"] }, then: "$$end0" },
|
|
261
|
+
{ case: { $lt: [nowLiteral, "$$end1"] }, then: "$$end1" },
|
|
262
|
+
{ case: { $lt: [nowLiteral, "$$end2"] }, then: "$$end2" }
|
|
263
|
+
],
|
|
264
|
+
default: "$$end3"
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
in: {
|
|
269
|
+
index: "$$chosenIdx",
|
|
270
|
+
start: {
|
|
271
|
+
$dateAdd: {
|
|
272
|
+
startDate: "$$anchor",
|
|
273
|
+
unit: "month",
|
|
274
|
+
amount: { $multiply: ["$$chosenIdx", "$$monthsPerPeriod"] }
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
end: "$$chosenEnd"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/db/mongodb/rows.ts
|
|
296
|
+
function docToCustomer(doc) {
|
|
297
|
+
return {
|
|
298
|
+
id: doc._id,
|
|
299
|
+
externalId: doc.externalId,
|
|
300
|
+
providerIds: doc.providerIds ?? {},
|
|
301
|
+
email: doc.email ?? null,
|
|
302
|
+
createdAt: doc.createdAt,
|
|
303
|
+
updatedAt: doc.updatedAt
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function docToPlanVersion(doc) {
|
|
307
|
+
return {
|
|
308
|
+
id: doc._id,
|
|
309
|
+
planId: doc.planId,
|
|
310
|
+
version: doc.version,
|
|
311
|
+
group: doc.group,
|
|
312
|
+
isDefault: doc.isDefault,
|
|
313
|
+
name: doc.name,
|
|
314
|
+
priceMinor: doc.priceMinor,
|
|
315
|
+
priceCurrency: doc.priceCurrency,
|
|
316
|
+
priceInterval: doc.priceInterval,
|
|
317
|
+
features: doc.features,
|
|
318
|
+
providerRefs: doc.providerRefs,
|
|
319
|
+
pushedAt: doc.pushedAt
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function docToSubscription(doc) {
|
|
323
|
+
return {
|
|
324
|
+
id: doc._id,
|
|
325
|
+
customerId: doc.customerId,
|
|
326
|
+
planId: doc.planId,
|
|
327
|
+
planVersion: doc.planVersion,
|
|
328
|
+
group: doc.group,
|
|
329
|
+
status: doc.status,
|
|
330
|
+
provider: doc.provider,
|
|
331
|
+
providerSubscriptionRef: doc.providerSubscriptionRef,
|
|
332
|
+
currentPeriodStart: doc.currentPeriodStart,
|
|
333
|
+
currentPeriodEnd: doc.currentPeriodEnd,
|
|
334
|
+
cancelAtPeriodEnd: doc.cancelAtPeriodEnd,
|
|
335
|
+
createdAt: doc.createdAt,
|
|
336
|
+
updatedAt: doc.updatedAt
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function docToFeatureBalance(doc) {
|
|
340
|
+
return {
|
|
341
|
+
id: doc._id,
|
|
342
|
+
customerId: doc.customerId,
|
|
343
|
+
featureId: doc.featureId,
|
|
344
|
+
group: doc.group,
|
|
345
|
+
used: doc.used,
|
|
346
|
+
limit: doc.limit,
|
|
347
|
+
resetInterval: doc.resetInterval,
|
|
348
|
+
anchor: doc.anchor,
|
|
349
|
+
periodStart: doc.periodStart,
|
|
350
|
+
periodEnd: doc.periodEnd,
|
|
351
|
+
planId: doc.planId,
|
|
352
|
+
planVersion: doc.planVersion,
|
|
353
|
+
updatedAt: doc.updatedAt
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/db/mongodb/topology.ts
|
|
358
|
+
var UNSUPPORTED_MESSAGE_PATTERN = /transaction numbers are only allowed on a replica set member or mongos|this mongodb deployment does not support transactions|transactions are not supported|standalone/i;
|
|
359
|
+
function isTransactionsUnsupportedError(error) {
|
|
360
|
+
if (error === null || typeof error !== "object") return false;
|
|
361
|
+
const record = error;
|
|
362
|
+
const message = typeof record.message === "string" ? record.message : "";
|
|
363
|
+
if (message.length === 0) return false;
|
|
364
|
+
if (record.code === 20 && /transaction/i.test(message)) return true;
|
|
365
|
+
return /transaction/i.test(message) && UNSUPPORTED_MESSAGE_PATTERN.test(message);
|
|
366
|
+
}
|
|
367
|
+
async function runTransaction(client, fn) {
|
|
368
|
+
const session = client.startSession();
|
|
369
|
+
try {
|
|
370
|
+
let result;
|
|
371
|
+
try {
|
|
372
|
+
await session.withTransaction(async () => {
|
|
373
|
+
result = await fn(session);
|
|
374
|
+
});
|
|
375
|
+
return result;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (isTransactionsUnsupportedError(error)) {
|
|
378
|
+
return await fn(void 0);
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
await session.endSession();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/db/mongodb/adapter.ts
|
|
388
|
+
var ACTIVE_STATUSES = [...PW_ACTIVE_SUBSCRIPTION_STATUSES];
|
|
389
|
+
function stableStringify(value) {
|
|
390
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
391
|
+
if (value !== null && typeof value === "object") {
|
|
392
|
+
const entries = Object.entries(value).sort(
|
|
393
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
394
|
+
);
|
|
395
|
+
return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`).join(",")}}`;
|
|
396
|
+
}
|
|
397
|
+
return JSON.stringify(value);
|
|
398
|
+
}
|
|
399
|
+
var planContent = (p) => ({
|
|
400
|
+
group: p.group,
|
|
401
|
+
isDefault: p.isDefault,
|
|
402
|
+
name: p.name,
|
|
403
|
+
priceMinor: p.priceMinor,
|
|
404
|
+
priceCurrency: p.priceCurrency,
|
|
405
|
+
priceInterval: p.priceInterval,
|
|
406
|
+
features: p.features,
|
|
407
|
+
providerRefs: p.providerRefs
|
|
408
|
+
});
|
|
409
|
+
function planContentEquals(active, input) {
|
|
410
|
+
return stableStringify(planContent(active)) === stableStringify(planContent(input));
|
|
411
|
+
}
|
|
412
|
+
function lit(value) {
|
|
413
|
+
return { $literal: value };
|
|
414
|
+
}
|
|
415
|
+
var SUBSCRIPTION_PATCH_KEYS = [
|
|
416
|
+
"planId",
|
|
417
|
+
"planVersion",
|
|
418
|
+
"status",
|
|
419
|
+
"provider",
|
|
420
|
+
"providerSubscriptionRef",
|
|
421
|
+
"currentPeriodStart",
|
|
422
|
+
"currentPeriodEnd",
|
|
423
|
+
"cancelAtPeriodEnd"
|
|
424
|
+
];
|
|
425
|
+
function buildAdapter(deps) {
|
|
426
|
+
const sessionOpt = deps.session ? { session: deps.session } : {};
|
|
427
|
+
const customers = deps.db.collection(PW_TABLES.customers);
|
|
428
|
+
const plans = deps.db.collection(PW_TABLES.plans);
|
|
429
|
+
const subscriptions = deps.db.collection(PW_TABLES.subscriptions);
|
|
430
|
+
const balances = deps.db.collection(PW_TABLES.featureBalances);
|
|
431
|
+
const webhookEvents = deps.db.collection(PW_TABLES.webhookEvents);
|
|
432
|
+
return {
|
|
433
|
+
dialect: "mongodb",
|
|
434
|
+
customers: {
|
|
435
|
+
getByExternalId: (externalId) => customersGetByExternalId(customers, externalId, sessionOpt),
|
|
436
|
+
upsert: (input) => customersUpsert(customers, input, sessionOpt),
|
|
437
|
+
linkProviderRef: (externalId, provider, ref) => customersLinkProviderRef(customers, externalId, provider, ref, sessionOpt)
|
|
438
|
+
},
|
|
439
|
+
plans: {
|
|
440
|
+
getActiveVersion: (planId) => plansGetActiveVersion(plans, planId, sessionOpt),
|
|
441
|
+
listActive: () => plansListActive(plans, sessionOpt),
|
|
442
|
+
pushVersion: (plan) => plansPushVersion(plans, plan, sessionOpt)
|
|
443
|
+
},
|
|
444
|
+
subscriptions: {
|
|
445
|
+
getActive: (customerId, group) => subscriptionsGetActive(subscriptions, customerId, group, sessionOpt),
|
|
446
|
+
create: (input) => subscriptionsCreate(subscriptions, input, sessionOpt),
|
|
447
|
+
update: (id, patch) => subscriptionsUpdate(subscriptions, id, patch, sessionOpt)
|
|
448
|
+
},
|
|
449
|
+
balances: {
|
|
450
|
+
get: (customerId, featureId, group) => balancesGet(balances, customerId, featureId, group, sessionOpt),
|
|
451
|
+
consume: (input) => balancesConsume(balances, input, sessionOpt),
|
|
452
|
+
resetTo: (customerId, featureId, group, init) => balancesResetTo(balances, customerId, featureId, group, init, sessionOpt)
|
|
453
|
+
},
|
|
454
|
+
webhookEvents: {
|
|
455
|
+
claim: (dedupeKey, meta) => webhookEventsClaim(webhookEvents, dedupeKey, meta, sessionOpt),
|
|
456
|
+
markApplied: (dedupeKey) => webhookEventsMarkApplied(webhookEvents, dedupeKey, sessionOpt)
|
|
457
|
+
},
|
|
458
|
+
migrations: {
|
|
459
|
+
status: () => mongoMigrationStatus(deps.db),
|
|
460
|
+
apply: () => mongoMigrationApply(deps.db)
|
|
461
|
+
},
|
|
462
|
+
transaction: (fn) => runTransaction(deps.client, (session) => fn(buildAdapter({ ...deps, session })))
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async function customersGetByExternalId(collection, externalId, sessionOpt) {
|
|
466
|
+
const doc = await collection.findOne({ externalId }, sessionOpt);
|
|
467
|
+
return doc ? docToCustomer(doc) : null;
|
|
468
|
+
}
|
|
469
|
+
async function customersUpsert(collection, input, sessionOpt) {
|
|
470
|
+
const now = /* @__PURE__ */ new Date();
|
|
471
|
+
const generatedId = generatePwId(now.getTime());
|
|
472
|
+
const emailExpr = input.email !== void 0 ? lit(input.email) : { $ifNull: ["$email", null] };
|
|
473
|
+
try {
|
|
474
|
+
const doc = await collection.findOneAndUpdate(
|
|
475
|
+
{ externalId: input.externalId },
|
|
476
|
+
[
|
|
477
|
+
{
|
|
478
|
+
$set: {
|
|
479
|
+
_id: { $ifNull: ["$_id", lit(generatedId)] },
|
|
480
|
+
externalId: lit(input.externalId),
|
|
481
|
+
providerIds: { $ifNull: ["$providerIds", {}] },
|
|
482
|
+
email: emailExpr,
|
|
483
|
+
createdAt: { $ifNull: ["$createdAt", now] },
|
|
484
|
+
updatedAt: now
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
],
|
|
488
|
+
{ upsert: true, returnDocument: "after", ...sessionOpt }
|
|
489
|
+
);
|
|
490
|
+
if (!doc) throw new Error("mongodb adapter: customers.upsert returned no document");
|
|
491
|
+
return docToCustomer(doc);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
throw wrapDriverError(
|
|
494
|
+
error,
|
|
495
|
+
`payweave/db/mongodb: customers.upsert(${JSON.stringify(input.externalId)}) failed.`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function customersLinkProviderRef(collection, externalId, provider, ref, sessionOpt) {
|
|
500
|
+
const result = await collection.updateOne(
|
|
501
|
+
{ externalId },
|
|
502
|
+
{ $set: { [`providerIds.${provider}`]: ref, updatedAt: /* @__PURE__ */ new Date() } },
|
|
503
|
+
sessionOpt
|
|
504
|
+
);
|
|
505
|
+
if (result.matchedCount === 0) {
|
|
506
|
+
throw new PayweaveNotFoundError(
|
|
507
|
+
`payweave/db/mongodb: customers.linkProviderRef: no customer with externalId ${JSON.stringify(externalId)}.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function plansGetActiveVersion(collection, planId, sessionOpt) {
|
|
512
|
+
const doc = await collection.findOne({ planId }, { sort: { version: -1 }, ...sessionOpt });
|
|
513
|
+
return doc ? docToPlanVersion(doc) : null;
|
|
514
|
+
}
|
|
515
|
+
async function plansListActive(collection, sessionOpt) {
|
|
516
|
+
const docs = await collection.aggregate(
|
|
517
|
+
[
|
|
518
|
+
{ $sort: { planId: 1, version: -1 } },
|
|
519
|
+
{ $group: { _id: "$planId", doc: { $first: "$$ROOT" } } },
|
|
520
|
+
{ $replaceRoot: { newRoot: "$doc" } }
|
|
521
|
+
],
|
|
522
|
+
sessionOpt
|
|
523
|
+
).toArray();
|
|
524
|
+
return docs.map(docToPlanVersion);
|
|
525
|
+
}
|
|
526
|
+
var PUSH_VERSION_MAX_ATTEMPTS = 5;
|
|
527
|
+
async function plansPushVersion(collection, plan, sessionOpt) {
|
|
528
|
+
for (let attempt = 0; attempt < PUSH_VERSION_MAX_ATTEMPTS; attempt++) {
|
|
529
|
+
const active = await plansGetActiveVersion(collection, plan.planId, sessionOpt);
|
|
530
|
+
if (active && planContentEquals(active, plan)) return active;
|
|
531
|
+
const now = /* @__PURE__ */ new Date();
|
|
532
|
+
const nextVersion = (active?.version ?? 0) + 1;
|
|
533
|
+
const doc = {
|
|
534
|
+
_id: generatePwId(now.getTime()),
|
|
535
|
+
planId: plan.planId,
|
|
536
|
+
version: nextVersion,
|
|
537
|
+
group: plan.group,
|
|
538
|
+
isDefault: plan.isDefault,
|
|
539
|
+
name: plan.name,
|
|
540
|
+
priceMinor: plan.priceMinor,
|
|
541
|
+
priceCurrency: plan.priceCurrency,
|
|
542
|
+
priceInterval: plan.priceInterval,
|
|
543
|
+
features: plan.features,
|
|
544
|
+
providerRefs: plan.providerRefs,
|
|
545
|
+
pushedAt: now
|
|
546
|
+
};
|
|
547
|
+
try {
|
|
548
|
+
await collection.insertOne(doc, sessionOpt);
|
|
549
|
+
return docToPlanVersion(doc);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (isDuplicateKeyError(error) && attempt < PUSH_VERSION_MAX_ATTEMPTS - 1) continue;
|
|
552
|
+
throw wrapDriverError(
|
|
553
|
+
error,
|
|
554
|
+
`payweave/db/mongodb: plans.pushVersion(${JSON.stringify(plan.planId)}) failed.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
throw new Error("payweave/db/mongodb: plans.pushVersion exhausted retries under contention");
|
|
559
|
+
}
|
|
560
|
+
async function subscriptionsGetActive(collection, customerId, group, sessionOpt) {
|
|
561
|
+
const doc = await collection.findOne(
|
|
562
|
+
{ customerId, group, status: { $in: ACTIVE_STATUSES } },
|
|
563
|
+
sessionOpt
|
|
564
|
+
);
|
|
565
|
+
return doc ? docToSubscription(doc) : null;
|
|
566
|
+
}
|
|
567
|
+
async function subscriptionsCreate(collection, input, sessionOpt) {
|
|
568
|
+
const now = /* @__PURE__ */ new Date();
|
|
569
|
+
const doc = {
|
|
570
|
+
_id: generatePwId(now.getTime()),
|
|
571
|
+
customerId: input.customerId,
|
|
572
|
+
planId: input.planId,
|
|
573
|
+
planVersion: input.planVersion,
|
|
574
|
+
group: input.group,
|
|
575
|
+
status: input.status,
|
|
576
|
+
provider: input.provider,
|
|
577
|
+
providerSubscriptionRef: input.providerSubscriptionRef,
|
|
578
|
+
currentPeriodStart: input.currentPeriodStart,
|
|
579
|
+
currentPeriodEnd: input.currentPeriodEnd,
|
|
580
|
+
cancelAtPeriodEnd: input.cancelAtPeriodEnd,
|
|
581
|
+
createdAt: now,
|
|
582
|
+
updatedAt: now
|
|
583
|
+
};
|
|
584
|
+
try {
|
|
585
|
+
await collection.insertOne(doc, sessionOpt);
|
|
586
|
+
return docToSubscription(doc);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
throw wrapDriverError(
|
|
589
|
+
error,
|
|
590
|
+
`payweave/db/mongodb: 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).`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async function subscriptionsUpdate(collection, id, patch, sessionOpt) {
|
|
595
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
596
|
+
const patchRecord = patch;
|
|
597
|
+
for (const key of SUBSCRIPTION_PATCH_KEYS) {
|
|
598
|
+
if (!(key in patch) || patchRecord[key] === void 0) continue;
|
|
599
|
+
set[key] = patchRecord[key];
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const doc = await collection.findOneAndUpdate(
|
|
603
|
+
{ _id: id },
|
|
604
|
+
{ $set: set },
|
|
605
|
+
{ returnDocument: "after", ...sessionOpt }
|
|
606
|
+
);
|
|
607
|
+
if (!doc) {
|
|
608
|
+
throw new PayweaveNotFoundError(
|
|
609
|
+
`payweave/db/mongodb: subscriptions.update: no subscription with id ${JSON.stringify(id)}.`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return docToSubscription(doc);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error instanceof PayweaveNotFoundError) throw error;
|
|
615
|
+
throw wrapDriverError(error, `payweave/db/mongodb: subscriptions.update(${JSON.stringify(id)}) failed.`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function balancesGet(collection, customerId, featureId, group, sessionOpt) {
|
|
619
|
+
const doc = await collection.findOne({ customerId, featureId, group }, sessionOpt);
|
|
620
|
+
return doc ? docToFeatureBalance(doc) : null;
|
|
621
|
+
}
|
|
622
|
+
function buildConsumePipeline(input, generatedId) {
|
|
623
|
+
const nowMs = input.now.getTime();
|
|
624
|
+
const nowLiteral = input.now;
|
|
625
|
+
const conditional = input.conditional === true;
|
|
626
|
+
const amount = input.amount;
|
|
627
|
+
return [
|
|
628
|
+
// A: fill creation defaults. Deliberately does NOT rely on MongoDB
|
|
629
|
+
// auto-seeding the query filter's equality fields into a fresh upserted
|
|
630
|
+
// document (that behavior is a real-server implementation detail this
|
|
631
|
+
// sandbox cannot verify one way or the other) — EVERY field the document
|
|
632
|
+
// needs, including customerId/featureId/group, is explicitly `$ifNull`'d
|
|
633
|
+
// from this call's own inputs (the "$ifNull defaults" pattern),
|
|
634
|
+
// so the pipeline is correct regardless of that assumption.
|
|
635
|
+
{
|
|
636
|
+
$set: {
|
|
637
|
+
_id: { $ifNull: ["$_id", lit(generatedId)] },
|
|
638
|
+
customerId: { $ifNull: ["$customerId", lit(input.customerId)] },
|
|
639
|
+
featureId: { $ifNull: ["$featureId", lit(input.featureId)] },
|
|
640
|
+
group: { $ifNull: ["$group", lit(input.group)] },
|
|
641
|
+
anchor: { $ifNull: ["$anchor", input.init.anchor] },
|
|
642
|
+
resetInterval: { $ifNull: ["$resetInterval", lit(input.init.resetInterval)] },
|
|
643
|
+
limit: { $ifNull: ["$limit", input.init.limit] },
|
|
644
|
+
planId: { $ifNull: ["$planId", lit(input.init.planId)] },
|
|
645
|
+
planVersion: { $ifNull: ["$planVersion", input.init.planVersion] },
|
|
646
|
+
used: { $ifNull: ["$used", 0] }
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
// B: the anchor-relative current period (./period-pipeline.ts).
|
|
650
|
+
{
|
|
651
|
+
$set: {
|
|
652
|
+
__period: buildPeriodExpr(nowMs, "$anchor", "$resetInterval")
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
// C: reset-due / conditional-gate arithmetic, entirely server-side.
|
|
656
|
+
{
|
|
657
|
+
$set: {
|
|
658
|
+
__hasPeriodEnd: { $ne: [{ $ifNull: ["$periodEnd", null] }, null] }
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
$set: {
|
|
663
|
+
__resetDue: {
|
|
664
|
+
$cond: ["$__hasPeriodEnd", { $gte: [nowLiteral, "$periodEnd"] }, true]
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
$set: {
|
|
670
|
+
__baseUsed: { $cond: ["$__resetDue", 0, "$used"] }
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
// Each of the following stays in its OWN `$set` stage rather than sharing
|
|
674
|
+
// one with the field it depends on — deliberately conservative given this
|
|
675
|
+
// sandbox cannot execute a real pipeline to confirm same-stage field
|
|
676
|
+
// visibility ordering.
|
|
677
|
+
{
|
|
678
|
+
$set: {
|
|
679
|
+
__remaining: { $subtract: ["$limit", "$__baseUsed"] }
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
$set: {
|
|
684
|
+
__applied: conditional ? { $gte: ["$__remaining", amount] } : true
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
$set: {
|
|
689
|
+
__finalUsed: {
|
|
690
|
+
$cond: ["$__applied", { $add: ["$__baseUsed", amount] }, "$__baseUsed"]
|
|
691
|
+
},
|
|
692
|
+
__shouldWrite: { $or: ["$__applied", "$__resetDue"] }
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
// D: write the real fields — untouched (including `updatedAt`) when
|
|
696
|
+
// denied AND no reset was due (leave the row untouched in that case).
|
|
697
|
+
{
|
|
698
|
+
$set: {
|
|
699
|
+
used: { $cond: ["$__shouldWrite", "$__finalUsed", "$used"] },
|
|
700
|
+
periodStart: { $cond: ["$__shouldWrite", "$__period.start", "$periodStart"] },
|
|
701
|
+
periodEnd: { $cond: ["$__shouldWrite", "$__period.end", "$periodEnd"] },
|
|
702
|
+
updatedAt: { $cond: ["$__shouldWrite", nowLiteral, { $ifNull: ["$updatedAt", nowLiteral] }] },
|
|
703
|
+
_pwLastApplied: "$__applied"
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
$unset: [
|
|
708
|
+
"__period",
|
|
709
|
+
"__hasPeriodEnd",
|
|
710
|
+
"__resetDue",
|
|
711
|
+
"__baseUsed",
|
|
712
|
+
"__remaining",
|
|
713
|
+
"__applied",
|
|
714
|
+
"__finalUsed",
|
|
715
|
+
"__shouldWrite"
|
|
716
|
+
]
|
|
717
|
+
}
|
|
718
|
+
];
|
|
719
|
+
}
|
|
720
|
+
async function balancesConsume(collection, input, sessionOpt) {
|
|
721
|
+
const generatedId = generatePwId(input.now.getTime());
|
|
722
|
+
const pipeline = buildConsumePipeline(input, generatedId);
|
|
723
|
+
const doc = await collection.findOneAndUpdate(
|
|
724
|
+
{ customerId: input.customerId, featureId: input.featureId, group: input.group },
|
|
725
|
+
pipeline,
|
|
726
|
+
{ upsert: true, returnDocument: "after", ...sessionOpt }
|
|
727
|
+
);
|
|
728
|
+
if (!doc) throw new Error("mongodb adapter: balances.consume returned no document");
|
|
729
|
+
const applied = doc._pwLastApplied ?? true;
|
|
730
|
+
return { ...docToFeatureBalance(doc), applied };
|
|
731
|
+
}
|
|
732
|
+
async function balancesResetTo(collection, customerId, featureId, group, init, sessionOpt) {
|
|
733
|
+
const now = /* @__PURE__ */ new Date();
|
|
734
|
+
const period = currentPeriod(init.anchor.getTime(), init.resetInterval, now.getTime());
|
|
735
|
+
const generatedId = generatePwId(now.getTime());
|
|
736
|
+
await collection.findOneAndUpdate(
|
|
737
|
+
{ customerId, featureId, group },
|
|
738
|
+
[
|
|
739
|
+
{
|
|
740
|
+
$set: {
|
|
741
|
+
_id: { $ifNull: ["$_id", lit(generatedId)] },
|
|
742
|
+
// Same defensive explicitness as `buildConsumePipeline` — never
|
|
743
|
+
// rely on MongoDB auto-seeding the filter's equality fields, and
|
|
744
|
+
// never let a caller-supplied string be misread as a field path.
|
|
745
|
+
customerId: lit(customerId),
|
|
746
|
+
featureId: lit(featureId),
|
|
747
|
+
group: lit(group),
|
|
748
|
+
used: 0,
|
|
749
|
+
limit: init.limit,
|
|
750
|
+
resetInterval: lit(init.resetInterval),
|
|
751
|
+
anchor: init.anchor,
|
|
752
|
+
periodStart: new Date(period.start),
|
|
753
|
+
periodEnd: new Date(period.end),
|
|
754
|
+
planId: lit(init.planId),
|
|
755
|
+
planVersion: init.planVersion,
|
|
756
|
+
updatedAt: now
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
],
|
|
760
|
+
{ upsert: true, ...sessionOpt }
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
function buildClaimPipeline(dedupeKey, meta) {
|
|
764
|
+
const staleClaimAfterMs = meta.staleClaimAfterMs ?? DEFAULT_STALE_CLAIM_AFTER_MS;
|
|
765
|
+
const staleThreshold = new Date(meta.now.getTime() - staleClaimAfterMs);
|
|
766
|
+
return [
|
|
767
|
+
{
|
|
768
|
+
$set: {
|
|
769
|
+
// Same defensive explicitness as `buildConsumePipeline` — never rely
|
|
770
|
+
// on MongoDB auto-seeding the `_id` filter's equality match on
|
|
771
|
+
// upsert, and never let a caller-supplied string be misread as a
|
|
772
|
+
// field path.
|
|
773
|
+
_id: { $ifNull: ["$_id", lit(dedupeKey)] },
|
|
774
|
+
provider: { $ifNull: ["$provider", lit(meta.provider)] },
|
|
775
|
+
type: { $ifNull: ["$type", lit(meta.type)] },
|
|
776
|
+
receivedAt: { $ifNull: ["$receivedAt", meta.now] },
|
|
777
|
+
appliedAt: { $ifNull: ["$appliedAt", null] },
|
|
778
|
+
claimedAt: { $ifNull: ["$claimedAt", null] }
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
$set: {
|
|
783
|
+
_pwWon: {
|
|
784
|
+
$and: [
|
|
785
|
+
{ $eq: ["$appliedAt", null] },
|
|
786
|
+
{ $or: [{ $eq: ["$claimedAt", null] }, { $lte: ["$claimedAt", staleThreshold] }] }
|
|
787
|
+
]
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
$set: {
|
|
793
|
+
claimedAt: { $cond: ["$_pwWon", meta.now, "$claimedAt"] }
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
async function webhookEventsClaim(collection, dedupeKey, meta, sessionOpt) {
|
|
799
|
+
const pipeline = buildClaimPipeline(dedupeKey, meta);
|
|
800
|
+
const doc = await collection.findOneAndUpdate(
|
|
801
|
+
{ _id: dedupeKey },
|
|
802
|
+
pipeline,
|
|
803
|
+
{ upsert: true, returnDocument: "after", ...sessionOpt }
|
|
804
|
+
);
|
|
805
|
+
return doc?._pwWon === true;
|
|
806
|
+
}
|
|
807
|
+
async function webhookEventsMarkApplied(collection, dedupeKey, sessionOpt) {
|
|
808
|
+
await collection.updateOne({ _id: dedupeKey }, { $set: { appliedAt: /* @__PURE__ */ new Date() } }, sessionOpt);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/db/mongodb/url.ts
|
|
812
|
+
var URL_SCHEMES = ["mongodb://", "mongodb+srv://"];
|
|
813
|
+
function looksLikeMongoClient(value) {
|
|
814
|
+
const candidate = value;
|
|
815
|
+
return typeof candidate.connect === "function" && typeof candidate.close === "function" && typeof candidate.db === "function" && typeof candidate.startSession === "function";
|
|
816
|
+
}
|
|
817
|
+
function validateMongoUrl(url) {
|
|
818
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
819
|
+
throw new PayweaveConfigError(
|
|
820
|
+
"payweave/db/mongodb: mongodbAdapter({ url, dbName }) requires a non-empty string url."
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
if (!URL_SCHEMES.some((scheme) => url.startsWith(scheme))) {
|
|
824
|
+
throw new PayweaveConfigError(
|
|
825
|
+
'payweave/db/mongodb: mongodbAdapter({ url, ... }) requires a "mongodb://" or "mongodb+srv://" connection string.'
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function validateDbName(dbName) {
|
|
830
|
+
if (typeof dbName !== "string" || dbName.length === 0) {
|
|
831
|
+
throw new PayweaveConfigError(
|
|
832
|
+
"payweave/db/mongodb: mongodbAdapter(...) requires a non-empty `dbName` \u2014 a MongoDB connection string may name a default database in its path, but this adapter always requires `dbName` explicitly, for both { url } and { client }."
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function resolveMongoInput(input) {
|
|
837
|
+
if (input === null || typeof input !== "object") {
|
|
838
|
+
throw new PayweaveConfigError(
|
|
839
|
+
`payweave/db/mongodb: mongodbAdapter(...) expects { url, dbName } (a mongodb:///mongodb+srv:// connection string) or { client, dbName } (an existing MongoClient) \u2014 got ${input === null ? "null" : typeof input}.`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const record = input;
|
|
843
|
+
if ("url" in record) {
|
|
844
|
+
validateMongoUrl(record.url);
|
|
845
|
+
validateDbName(record.dbName);
|
|
846
|
+
return { kind: "url", url: record.url, dbName: record.dbName };
|
|
847
|
+
}
|
|
848
|
+
if ("client" in record) {
|
|
849
|
+
const client = record.client;
|
|
850
|
+
if (client === null || typeof client !== "object" || !looksLikeMongoClient(client)) {
|
|
851
|
+
throw new PayweaveConfigError(
|
|
852
|
+
`payweave/db/mongodb: mongodbAdapter({ client, dbName }) expects \`client\` to be a MongoClient-like instance (has .connect()/.close()/.db()/.startSession()) \u2014 got ${client === null ? "null" : typeof client === "object" ? "an object missing one of those methods" : typeof client}.`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
validateDbName(record.dbName);
|
|
856
|
+
return { kind: "client", client, dbName: record.dbName };
|
|
857
|
+
}
|
|
858
|
+
throw new PayweaveConfigError(
|
|
859
|
+
"payweave/db/mongodb: mongodbAdapter(...) expects { url, dbName } (a mongodb:///mongodb+srv:// connection string) or { client, dbName } (an existing MongoClient) \u2014 got an object matching neither shape."
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/db/mongodb/index.ts
|
|
864
|
+
async function connect(target) {
|
|
865
|
+
if (target.kind === "client") {
|
|
866
|
+
const client2 = target.client;
|
|
867
|
+
await target.client.connect();
|
|
868
|
+
return { client: client2, db: client2.db(target.dbName) };
|
|
869
|
+
}
|
|
870
|
+
let mod;
|
|
871
|
+
try {
|
|
872
|
+
mod = await import('mongodb');
|
|
873
|
+
} catch (cause) {
|
|
874
|
+
throw installHintError(cause);
|
|
875
|
+
}
|
|
876
|
+
const rawClient = new mod.MongoClient(target.url);
|
|
877
|
+
await rawClient.connect();
|
|
878
|
+
const client = rawClient;
|
|
879
|
+
return { client, db: client.db(target.dbName) };
|
|
880
|
+
}
|
|
881
|
+
function buildLazyAdapter(getConnection) {
|
|
882
|
+
let builtPromise;
|
|
883
|
+
const getReal = () => {
|
|
884
|
+
builtPromise ??= getConnection().then(
|
|
885
|
+
({ client, db }) => buildAdapter({ client, db })
|
|
886
|
+
);
|
|
887
|
+
return builtPromise;
|
|
888
|
+
};
|
|
889
|
+
return {
|
|
890
|
+
dialect: "mongodb",
|
|
891
|
+
customers: {
|
|
892
|
+
getByExternalId: async (externalId) => (await getReal()).customers.getByExternalId(externalId),
|
|
893
|
+
upsert: async (input) => (await getReal()).customers.upsert(input),
|
|
894
|
+
linkProviderRef: async (externalId, provider, ref) => (await getReal()).customers.linkProviderRef(externalId, provider, ref)
|
|
895
|
+
},
|
|
896
|
+
plans: {
|
|
897
|
+
getActiveVersion: async (planId) => (await getReal()).plans.getActiveVersion(planId),
|
|
898
|
+
listActive: async () => (await getReal()).plans.listActive(),
|
|
899
|
+
pushVersion: async (plan) => (await getReal()).plans.pushVersion(plan)
|
|
900
|
+
},
|
|
901
|
+
subscriptions: {
|
|
902
|
+
getActive: async (customerId, group) => (await getReal()).subscriptions.getActive(customerId, group),
|
|
903
|
+
create: async (input) => (await getReal()).subscriptions.create(input),
|
|
904
|
+
update: async (id, patch) => (await getReal()).subscriptions.update(id, patch)
|
|
905
|
+
},
|
|
906
|
+
balances: {
|
|
907
|
+
get: async (customerId, featureId, group) => (await getReal()).balances.get(customerId, featureId, group),
|
|
908
|
+
consume: async (input) => (await getReal()).balances.consume(input),
|
|
909
|
+
resetTo: async (customerId, featureId, group, init) => (await getReal()).balances.resetTo(customerId, featureId, group, init)
|
|
910
|
+
},
|
|
911
|
+
webhookEvents: {
|
|
912
|
+
claim: async (dedupeKey, meta) => (await getReal()).webhookEvents.claim(dedupeKey, meta),
|
|
913
|
+
markApplied: async (dedupeKey) => (await getReal()).webhookEvents.markApplied(dedupeKey)
|
|
914
|
+
},
|
|
915
|
+
migrations: {
|
|
916
|
+
status: async () => (await getReal()).migrations.status(),
|
|
917
|
+
apply: async () => (await getReal()).migrations.apply()
|
|
918
|
+
},
|
|
919
|
+
transaction: async (fn) => (await getReal()).transaction(fn)
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function mongodbAdapter(input) {
|
|
923
|
+
const target = resolveMongoInput(input);
|
|
924
|
+
let connectPromise;
|
|
925
|
+
const getConnection = () => {
|
|
926
|
+
connectPromise ??= connect(target);
|
|
927
|
+
return connectPromise;
|
|
928
|
+
};
|
|
929
|
+
return buildLazyAdapter(getConnection);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export { mongodbAdapter };
|
|
933
|
+
//# sourceMappingURL=index.js.map
|
|
934
|
+
//# sourceMappingURL=index.js.map
|