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,1532 @@
|
|
|
1
|
+
import { PW_ACTIVE_SUBSCRIPTION_STATUSES, PW_TABLES, DEFAULT_STALE_CLAIM_AFTER_MS } from '../../chunk-NV3MZF7L.js';
|
|
2
|
+
import { currentPeriod } from '../../chunk-4R5OGOCC.js';
|
|
3
|
+
import { PayweaveConfigError, PayweaveNotFoundError, PayweaveError, PayweaveValidationError } from '../../chunk-HTRGMZFH.js';
|
|
4
|
+
import { mysqlTable, datetime, varchar, json, bigint, boolean, int, uniqueIndex, char, MySqlDatabase } from 'drizzle-orm/mysql-core';
|
|
5
|
+
import { pgTable, timestamp, text, jsonb, bigint as bigint$1, boolean as boolean$1, integer, uniqueIndex as uniqueIndex$1, PgDatabase } from 'drizzle-orm/pg-core';
|
|
6
|
+
import { sqliteTable, integer as integer$1, text as text$1, uniqueIndex as uniqueIndex$2, BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
|
|
7
|
+
import { sql, eq, and } from 'drizzle-orm';
|
|
8
|
+
import { randomInt } from 'crypto';
|
|
9
|
+
|
|
10
|
+
function detectDrizzleDialect(db, override) {
|
|
11
|
+
if (override !== void 0) return override;
|
|
12
|
+
if (db instanceof PgDatabase) return "postgres";
|
|
13
|
+
if (db instanceof MySqlDatabase) return "mysql";
|
|
14
|
+
if (db instanceof BaseSQLiteDatabase) return "sqlite";
|
|
15
|
+
throw new PayweaveConfigError(
|
|
16
|
+
'payweave/db/drizzle: could not detect the SQL dialect of the given Drizzle instance (expected an instance backed by drizzle-orm/pg-core, drizzle-orm/mysql-core, or drizzle-orm/sqlite-core). Pass it explicitly: drizzleAdapter(db, { dialect: "postgres" | "mysql" | "sqlite" }).'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/db/drizzle/errors.ts
|
|
21
|
+
function constraintCode(error) {
|
|
22
|
+
const direct = error?.code;
|
|
23
|
+
if (typeof direct === "string") return direct;
|
|
24
|
+
const cause = error?.cause;
|
|
25
|
+
const nested = cause?.code;
|
|
26
|
+
return typeof nested === "string" ? nested : void 0;
|
|
27
|
+
}
|
|
28
|
+
function isConstraintViolation(error) {
|
|
29
|
+
const code = constraintCode(error);
|
|
30
|
+
if (code === void 0) return false;
|
|
31
|
+
return code.startsWith("SQLITE_CONSTRAINT") || // better-sqlite3 / @libsql/client
|
|
32
|
+
code === "23505" || // postgres unique_violation
|
|
33
|
+
code === "23P01" || // postgres exclusion_violation
|
|
34
|
+
code === "ER_DUP_ENTRY";
|
|
35
|
+
}
|
|
36
|
+
function wrapDriverError(error, message) {
|
|
37
|
+
if (error instanceof PayweaveError) return error;
|
|
38
|
+
if (isConstraintViolation(error)) {
|
|
39
|
+
return new PayweaveValidationError(message, { cause: error });
|
|
40
|
+
}
|
|
41
|
+
return new PayweaveError(message, { cause: error, isRetryable: false });
|
|
42
|
+
}
|
|
43
|
+
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
44
|
+
var ENCODING_LEN = ENCODING.length;
|
|
45
|
+
var TIME_LEN = 10;
|
|
46
|
+
var RANDOM_LEN = 16;
|
|
47
|
+
function encodeTime(timeMs, len) {
|
|
48
|
+
if (!Number.isSafeInteger(timeMs) || timeMs < 0) {
|
|
49
|
+
throw new RangeError(`ulid: time must be a non-negative safe integer \u2014 got ${timeMs}`);
|
|
50
|
+
}
|
|
51
|
+
let remaining = timeMs;
|
|
52
|
+
let out = "";
|
|
53
|
+
for (let i = 0; i < len; i++) {
|
|
54
|
+
const mod = remaining % ENCODING_LEN;
|
|
55
|
+
out = ENCODING.charAt(mod) + out;
|
|
56
|
+
remaining = Math.floor(remaining / ENCODING_LEN);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function encodeRandom(len) {
|
|
61
|
+
let out = "";
|
|
62
|
+
for (let i = 0; i < len; i++) {
|
|
63
|
+
out += ENCODING.charAt(randomInt(0, ENCODING_LEN));
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function ulid(timeMs = Date.now()) {
|
|
68
|
+
return encodeTime(timeMs, TIME_LEN) + encodeRandom(RANDOM_LEN);
|
|
69
|
+
}
|
|
70
|
+
function generatePwId(timeMs = Date.now()) {
|
|
71
|
+
return `pwv_${ulid(timeMs)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/db/drizzle/plan-content.ts
|
|
75
|
+
function stableStringify(value) {
|
|
76
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
77
|
+
if (value !== null && typeof value === "object") {
|
|
78
|
+
const entries = Object.entries(value).sort(
|
|
79
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
80
|
+
);
|
|
81
|
+
return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`).join(",")}}`;
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(value);
|
|
84
|
+
}
|
|
85
|
+
var planContent = (p) => ({
|
|
86
|
+
group: p.group,
|
|
87
|
+
isDefault: p.isDefault,
|
|
88
|
+
name: p.name,
|
|
89
|
+
priceMinor: p.priceMinor,
|
|
90
|
+
priceCurrency: p.priceCurrency,
|
|
91
|
+
priceInterval: p.priceInterval,
|
|
92
|
+
features: p.features,
|
|
93
|
+
providerRefs: p.providerRefs
|
|
94
|
+
});
|
|
95
|
+
function planContentEquals(active, input) {
|
|
96
|
+
return stableStringify(planContent(active)) === stableStringify(planContent(input));
|
|
97
|
+
}
|
|
98
|
+
var ACTIVE_STATUS_SQL_LIST = sql.raw(
|
|
99
|
+
PW_ACTIVE_SUBSCRIPTION_STATUSES.map((s) => `'${s}'`).join(", ")
|
|
100
|
+
);
|
|
101
|
+
var utf8mb4Bin = { charSet: "utf8mb4", collate: "utf8mb4_bin" };
|
|
102
|
+
var pwCustomers = mysqlTable(PW_TABLES.customers, {
|
|
103
|
+
id: varchar("id", { length: 255 }).primaryKey(),
|
|
104
|
+
externalId: varchar("external_id", { length: 255, ...utf8mb4Bin }).notNull().unique(),
|
|
105
|
+
providerIds: json("provider_ids").$type().notNull(),
|
|
106
|
+
email: varchar("email", { length: 255 }),
|
|
107
|
+
createdAt: datetime("created_at", { fsp: 3 }).notNull(),
|
|
108
|
+
updatedAt: datetime("updated_at", { fsp: 3 }).notNull()
|
|
109
|
+
});
|
|
110
|
+
var pwPlans = mysqlTable(
|
|
111
|
+
PW_TABLES.plans,
|
|
112
|
+
{
|
|
113
|
+
id: varchar("id", { length: 255 }).primaryKey(),
|
|
114
|
+
planId: varchar("plan_id", { length: 255 }).notNull(),
|
|
115
|
+
version: int("version").notNull(),
|
|
116
|
+
group: varchar("group", { length: 255 }).notNull(),
|
|
117
|
+
isDefault: boolean("is_default").notNull(),
|
|
118
|
+
name: varchar("name", { length: 255 }),
|
|
119
|
+
priceMinor: bigint("price_minor", { mode: "number" }),
|
|
120
|
+
priceCurrency: varchar("price_currency", { length: 255 }),
|
|
121
|
+
priceInterval: varchar("price_interval", { length: 255 }).$type(),
|
|
122
|
+
features: json("features").$type().notNull(),
|
|
123
|
+
providerRefs: json("provider_refs").$type().notNull(),
|
|
124
|
+
pushedAt: datetime("pushed_at", { fsp: 3 }).notNull()
|
|
125
|
+
},
|
|
126
|
+
(t) => [uniqueIndex("pw_plans_plan_id_version_uq").on(t.planId, t.version)]
|
|
127
|
+
);
|
|
128
|
+
var pwSubscriptions = mysqlTable(
|
|
129
|
+
PW_TABLES.subscriptions,
|
|
130
|
+
{
|
|
131
|
+
id: varchar("id", { length: 255 }).primaryKey(),
|
|
132
|
+
customerId: varchar("customer_id", { length: 255 }).notNull().references(() => pwCustomers.id),
|
|
133
|
+
planId: varchar("plan_id", { length: 255 }).notNull(),
|
|
134
|
+
planVersion: int("plan_version").notNull(),
|
|
135
|
+
group: varchar("group", { length: 255, ...utf8mb4Bin }).notNull(),
|
|
136
|
+
status: varchar("status", { length: 255, ...utf8mb4Bin }).$type().notNull(),
|
|
137
|
+
provider: varchar("provider", { length: 255 }),
|
|
138
|
+
providerSubscriptionRef: varchar("provider_subscription_ref", { length: 255 }),
|
|
139
|
+
currentPeriodStart: datetime("current_period_start", { fsp: 3 }).notNull(),
|
|
140
|
+
currentPeriodEnd: datetime("current_period_end", { fsp: 3 }).notNull(),
|
|
141
|
+
cancelAtPeriodEnd: boolean("cancel_at_period_end").notNull(),
|
|
142
|
+
createdAt: datetime("created_at", { fsp: 3 }).notNull(),
|
|
143
|
+
updatedAt: datetime("updated_at", { fsp: 3 }).notNull(),
|
|
144
|
+
// STORED generated column emulating the partial-unique rule (see module
|
|
145
|
+
// header) — never write to this column directly.
|
|
146
|
+
activeSlot: char("active_slot", { length: 1 }).generatedAlwaysAs(
|
|
147
|
+
sql`case when ${sql.identifier("status")} in (${ACTIVE_STATUS_SQL_LIST}) then 'x' else null end`,
|
|
148
|
+
{ mode: "stored" }
|
|
149
|
+
)
|
|
150
|
+
},
|
|
151
|
+
(t) => [uniqueIndex("pw_subscriptions_active_uq").on(t.customerId, t.group, t.activeSlot)]
|
|
152
|
+
);
|
|
153
|
+
var pwFeatureBalances = mysqlTable(
|
|
154
|
+
PW_TABLES.featureBalances,
|
|
155
|
+
{
|
|
156
|
+
id: varchar("id", { length: 255 }).primaryKey(),
|
|
157
|
+
customerId: varchar("customer_id", { length: 255 }).notNull().references(() => pwCustomers.id),
|
|
158
|
+
featureId: varchar("feature_id", { length: 255 }).notNull(),
|
|
159
|
+
group: varchar("group", { length: 255, ...utf8mb4Bin }).notNull(),
|
|
160
|
+
used: bigint("used", { mode: "number" }).notNull(),
|
|
161
|
+
limit: bigint("limit", { mode: "number" }).notNull(),
|
|
162
|
+
resetInterval: varchar("reset_interval", { length: 255 }).$type().notNull(),
|
|
163
|
+
anchor: datetime("anchor", { fsp: 3 }).notNull(),
|
|
164
|
+
periodStart: datetime("period_start", { fsp: 3 }).notNull(),
|
|
165
|
+
periodEnd: datetime("period_end", { fsp: 3 }).notNull(),
|
|
166
|
+
planId: varchar("plan_id", { length: 255 }).notNull(),
|
|
167
|
+
planVersion: int("plan_version").notNull(),
|
|
168
|
+
updatedAt: datetime("updated_at", { fsp: 3 }).notNull()
|
|
169
|
+
},
|
|
170
|
+
(t) => [
|
|
171
|
+
uniqueIndex("pw_feature_balances_customer_feature_group_uq").on(
|
|
172
|
+
t.customerId,
|
|
173
|
+
t.featureId,
|
|
174
|
+
t.group
|
|
175
|
+
)
|
|
176
|
+
]
|
|
177
|
+
);
|
|
178
|
+
var pwWebhookEvents = mysqlTable(PW_TABLES.webhookEvents, {
|
|
179
|
+
dedupeKey: varchar("dedupe_key", { length: 255 }).primaryKey(),
|
|
180
|
+
provider: varchar("provider", { length: 255 }).notNull(),
|
|
181
|
+
type: varchar("type", { length: 255 }).notNull(),
|
|
182
|
+
receivedAt: datetime("received_at", { fsp: 3 }).notNull(),
|
|
183
|
+
claimedAt: datetime("claimed_at", { fsp: 3 }),
|
|
184
|
+
appliedAt: datetime("applied_at", { fsp: 3 })
|
|
185
|
+
});
|
|
186
|
+
var mysqlSchema = {
|
|
187
|
+
pwCustomers,
|
|
188
|
+
pwPlans,
|
|
189
|
+
pwSubscriptions,
|
|
190
|
+
pwFeatureBalances,
|
|
191
|
+
pwWebhookEvents
|
|
192
|
+
};
|
|
193
|
+
var REQUIRED_TABLES = [
|
|
194
|
+
PW_TABLES.customers,
|
|
195
|
+
PW_TABLES.plans,
|
|
196
|
+
PW_TABLES.subscriptions,
|
|
197
|
+
PW_TABLES.featureBalances,
|
|
198
|
+
PW_TABLES.webhookEvents
|
|
199
|
+
];
|
|
200
|
+
var SCHEMA_FILE_HINT = '"payweave/db/drizzle"\'s published schema (src/db/drizzle/schema/{pg,mysql,sqlite}.ts \u2014 docs/v1/database.md \xA74) \u2014 merge it into your own Drizzle schema and run `drizzle-kit push` or `drizzle-kit generate`/`migrate`.';
|
|
201
|
+
function missingTablesError(missing) {
|
|
202
|
+
return new PayweaveConfigError(
|
|
203
|
+
`payweave/db/drizzle: the following Payweave tables do not exist yet in the connected database: ${missing.join(", ")}. Merge ${SCHEMA_FILE_HINT}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
async function findMissingTables(probe) {
|
|
207
|
+
const missing = [];
|
|
208
|
+
for (const table of REQUIRED_TABLES) {
|
|
209
|
+
if (!await probe(table)) missing.push(table);
|
|
210
|
+
}
|
|
211
|
+
return missing;
|
|
212
|
+
}
|
|
213
|
+
function createVerifier(probe) {
|
|
214
|
+
let cached;
|
|
215
|
+
return function ensureVerified() {
|
|
216
|
+
cached ??= (async () => {
|
|
217
|
+
const missing = await findMissingTables(probe);
|
|
218
|
+
if (missing.length > 0) throw missingTablesError(missing);
|
|
219
|
+
})().catch((error) => {
|
|
220
|
+
cached = void 0;
|
|
221
|
+
throw error;
|
|
222
|
+
});
|
|
223
|
+
return cached;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function findMissingTablesForStatus(probe) {
|
|
227
|
+
return findMissingTables(probe);
|
|
228
|
+
}
|
|
229
|
+
function makeSqliteTableProbe(db) {
|
|
230
|
+
return async (table) => {
|
|
231
|
+
try {
|
|
232
|
+
await db.run(sql`select 1 from ${sql.identifier(table)} limit 0`);
|
|
233
|
+
return true;
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function makeSqlTableProbe(db) {
|
|
240
|
+
return async (table) => {
|
|
241
|
+
try {
|
|
242
|
+
await db.execute(sql`select 1 from ${sql.identifier(table)} limit 0`);
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/db/drizzle/mysql-adapter.ts
|
|
251
|
+
var DRIZZLE_KIT_INSTRUCTIONS = 'payweave/db/drizzle never runs migrations for you. Merge "payweave/db/drizzle"\'s published schema (see `./schema/mysql.ts`) into your own Drizzle schema, then run `drizzle-kit push` (dev) or `drizzle-kit generate` + `drizzle-kit migrate` (tracked migrations) yourself.';
|
|
252
|
+
function asWriteResult(raw) {
|
|
253
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
254
|
+
const affectedRows = header?.affectedRows;
|
|
255
|
+
return { affectedRows: typeof affectedRows === "number" ? affectedRows : 0 };
|
|
256
|
+
}
|
|
257
|
+
function buildMysqlAdapter(db) {
|
|
258
|
+
const ensureVerified = createVerifier(makeSqlTableProbe(db));
|
|
259
|
+
function guarded(fn) {
|
|
260
|
+
return async (...args) => {
|
|
261
|
+
await ensureVerified();
|
|
262
|
+
return fn(...args);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
dialect: "mysql",
|
|
267
|
+
customers: {
|
|
268
|
+
getByExternalId: guarded((externalId) => customersGetByExternalId(db, externalId)),
|
|
269
|
+
upsert: guarded((input) => customersUpsert(db, input)),
|
|
270
|
+
linkProviderRef: guarded(
|
|
271
|
+
(externalId, provider, ref) => customersLinkProviderRef(db, externalId, provider, ref)
|
|
272
|
+
)
|
|
273
|
+
},
|
|
274
|
+
plans: {
|
|
275
|
+
getActiveVersion: guarded((planId) => plansGetActiveVersion(db, planId)),
|
|
276
|
+
listActive: guarded(() => plansListActive(db)),
|
|
277
|
+
pushVersion: guarded((plan) => plansPushVersion(db, plan))
|
|
278
|
+
},
|
|
279
|
+
subscriptions: {
|
|
280
|
+
getActive: guarded(
|
|
281
|
+
(customerId, group) => subscriptionsGetActive(db, customerId, group)
|
|
282
|
+
),
|
|
283
|
+
create: guarded((input) => subscriptionsCreate(db, input)),
|
|
284
|
+
update: guarded(
|
|
285
|
+
(id, patch) => subscriptionsUpdate(db, id, patch)
|
|
286
|
+
)
|
|
287
|
+
},
|
|
288
|
+
balances: {
|
|
289
|
+
get: guarded(
|
|
290
|
+
(customerId, featureId, group) => balancesGet(db, customerId, featureId, group)
|
|
291
|
+
),
|
|
292
|
+
consume: guarded((input) => balancesConsume(db, input)),
|
|
293
|
+
resetTo: guarded(
|
|
294
|
+
(customerId, featureId, group, init) => balancesResetTo(db, customerId, featureId, group, init)
|
|
295
|
+
)
|
|
296
|
+
},
|
|
297
|
+
webhookEvents: {
|
|
298
|
+
claim: guarded(
|
|
299
|
+
(dedupeKey, meta) => webhookEventsClaim(db, dedupeKey, meta)
|
|
300
|
+
),
|
|
301
|
+
markApplied: guarded((dedupeKey) => webhookEventsMarkApplied(db, dedupeKey))
|
|
302
|
+
},
|
|
303
|
+
migrations: {
|
|
304
|
+
status: async () => {
|
|
305
|
+
const missing = await findMissingTablesForStatus(makeSqlTableProbe(db));
|
|
306
|
+
return missing.length > 0 ? { pending: ["0001_init"], applied: [] } : { pending: [], applied: ["0001_init"] };
|
|
307
|
+
},
|
|
308
|
+
apply: () => Promise.resolve({ applied: [], instructions: DRIZZLE_KIT_INSTRUCTIONS })
|
|
309
|
+
},
|
|
310
|
+
transaction: (fn) => db.transaction((tx) => fn(buildMysqlAdapter(tx)))
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
async function customersGetByExternalId(db, externalId) {
|
|
314
|
+
const rows = await db.select().from(pwCustomers).where(eq(pwCustomers.externalId, externalId));
|
|
315
|
+
return rows[0] ?? null;
|
|
316
|
+
}
|
|
317
|
+
async function customersUpsert(db, input) {
|
|
318
|
+
const now = /* @__PURE__ */ new Date();
|
|
319
|
+
const id = generatePwId(now.getTime());
|
|
320
|
+
const set = { updatedAt: now };
|
|
321
|
+
if (input.email !== void 0) set.email = input.email;
|
|
322
|
+
try {
|
|
323
|
+
await db.insert(pwCustomers).values({
|
|
324
|
+
id,
|
|
325
|
+
externalId: input.externalId,
|
|
326
|
+
providerIds: {},
|
|
327
|
+
email: input.email ?? null,
|
|
328
|
+
createdAt: now,
|
|
329
|
+
updatedAt: now
|
|
330
|
+
}).onDuplicateKeyUpdate({ set });
|
|
331
|
+
const rows = await db.select().from(pwCustomers).where(eq(pwCustomers.externalId, input.externalId));
|
|
332
|
+
const row = rows[0];
|
|
333
|
+
if (!row) throw new Error("drizzle adapter (mysql): customers.upsert returned no row");
|
|
334
|
+
return row;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw wrapDriverError(
|
|
337
|
+
error,
|
|
338
|
+
`payweave/db/drizzle (mysql): customers.upsert(${JSON.stringify(input.externalId)}) failed.`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function customersLinkProviderRef(db, externalId, provider, ref) {
|
|
343
|
+
await db.transaction(async (tx) => {
|
|
344
|
+
const rows = await tx.select().from(pwCustomers).where(eq(pwCustomers.externalId, externalId)).for("update");
|
|
345
|
+
const row = rows[0];
|
|
346
|
+
if (!row) {
|
|
347
|
+
throw new PayweaveNotFoundError(
|
|
348
|
+
`payweave/db/drizzle (mysql): customers.linkProviderRef: no customer with externalId ${JSON.stringify(externalId)}.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const providerIds = { ...row.providerIds, [provider]: ref };
|
|
352
|
+
await tx.update(pwCustomers).set({ providerIds, updatedAt: /* @__PURE__ */ new Date() }).where(eq(pwCustomers.externalId, externalId));
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async function plansGetActiveVersion(db, planId) {
|
|
356
|
+
const rows = await db.select().from(pwPlans).where(eq(pwPlans.planId, planId)).orderBy(sql`${pwPlans.version} desc`).limit(1);
|
|
357
|
+
return rows[0] ?? null;
|
|
358
|
+
}
|
|
359
|
+
async function plansListActive(db) {
|
|
360
|
+
const latest = db.select({
|
|
361
|
+
planId: pwPlans.planId,
|
|
362
|
+
version: sql`max(${pwPlans.version})`.as("max_version")
|
|
363
|
+
}).from(pwPlans).groupBy(pwPlans.planId).as("latest");
|
|
364
|
+
return db.select({
|
|
365
|
+
id: pwPlans.id,
|
|
366
|
+
planId: pwPlans.planId,
|
|
367
|
+
version: pwPlans.version,
|
|
368
|
+
group: pwPlans.group,
|
|
369
|
+
isDefault: pwPlans.isDefault,
|
|
370
|
+
name: pwPlans.name,
|
|
371
|
+
priceMinor: pwPlans.priceMinor,
|
|
372
|
+
priceCurrency: pwPlans.priceCurrency,
|
|
373
|
+
priceInterval: pwPlans.priceInterval,
|
|
374
|
+
features: pwPlans.features,
|
|
375
|
+
providerRefs: pwPlans.providerRefs,
|
|
376
|
+
pushedAt: pwPlans.pushedAt
|
|
377
|
+
}).from(pwPlans).innerJoin(latest, and(eq(latest.planId, pwPlans.planId), eq(latest.version, pwPlans.version)));
|
|
378
|
+
}
|
|
379
|
+
async function plansPushVersion(db, plan) {
|
|
380
|
+
return db.transaction(async (tx) => {
|
|
381
|
+
const activeRows = await tx.select().from(pwPlans).where(eq(pwPlans.planId, plan.planId)).orderBy(sql`${pwPlans.version} desc`).limit(1).for("update");
|
|
382
|
+
const active = activeRows[0] ?? null;
|
|
383
|
+
if (active && planContentEquals(active, plan)) {
|
|
384
|
+
return active;
|
|
385
|
+
}
|
|
386
|
+
const now = /* @__PURE__ */ new Date();
|
|
387
|
+
const id = generatePwId(now.getTime());
|
|
388
|
+
const nextVersion = (active?.version ?? 0) + 1;
|
|
389
|
+
await tx.insert(pwPlans).values({
|
|
390
|
+
id,
|
|
391
|
+
planId: plan.planId,
|
|
392
|
+
version: nextVersion,
|
|
393
|
+
group: plan.group,
|
|
394
|
+
isDefault: plan.isDefault,
|
|
395
|
+
name: plan.name,
|
|
396
|
+
priceMinor: plan.priceMinor,
|
|
397
|
+
priceCurrency: plan.priceCurrency,
|
|
398
|
+
priceInterval: plan.priceInterval,
|
|
399
|
+
features: plan.features,
|
|
400
|
+
providerRefs: plan.providerRefs,
|
|
401
|
+
pushedAt: now
|
|
402
|
+
});
|
|
403
|
+
const rows = await tx.select().from(pwPlans).where(eq(pwPlans.id, id));
|
|
404
|
+
const row = rows[0];
|
|
405
|
+
if (!row) throw new Error("drizzle adapter (mysql): plans.pushVersion returned no row");
|
|
406
|
+
return row;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async function subscriptionsGetActive(db, customerId, group) {
|
|
410
|
+
const rows = await db.select().from(pwSubscriptions).where(
|
|
411
|
+
and(
|
|
412
|
+
eq(pwSubscriptions.customerId, customerId),
|
|
413
|
+
eq(pwSubscriptions.group, group),
|
|
414
|
+
sql`${pwSubscriptions.status} in ${PW_ACTIVE_SUBSCRIPTION_STATUSES}`
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
return rows[0] ?? null;
|
|
418
|
+
}
|
|
419
|
+
async function subscriptionsCreate(db, input) {
|
|
420
|
+
const now = /* @__PURE__ */ new Date();
|
|
421
|
+
const id = generatePwId(now.getTime());
|
|
422
|
+
try {
|
|
423
|
+
await db.insert(pwSubscriptions).values({
|
|
424
|
+
id,
|
|
425
|
+
customerId: input.customerId,
|
|
426
|
+
planId: input.planId,
|
|
427
|
+
planVersion: input.planVersion,
|
|
428
|
+
group: input.group,
|
|
429
|
+
status: input.status,
|
|
430
|
+
provider: input.provider,
|
|
431
|
+
providerSubscriptionRef: input.providerSubscriptionRef,
|
|
432
|
+
currentPeriodStart: input.currentPeriodStart,
|
|
433
|
+
currentPeriodEnd: input.currentPeriodEnd,
|
|
434
|
+
cancelAtPeriodEnd: input.cancelAtPeriodEnd,
|
|
435
|
+
createdAt: now,
|
|
436
|
+
updatedAt: now
|
|
437
|
+
});
|
|
438
|
+
const rows = await db.select().from(pwSubscriptions).where(eq(pwSubscriptions.id, id));
|
|
439
|
+
const row = rows[0];
|
|
440
|
+
if (!row) throw new Error("drizzle adapter (mysql): subscriptions.create returned no row");
|
|
441
|
+
return row;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
throw wrapDriverError(
|
|
444
|
+
error,
|
|
445
|
+
`payweave/db/drizzle (mysql): 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).`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function subscriptionsUpdate(db, id, patch) {
|
|
450
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
451
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
452
|
+
if (value !== void 0) set[key] = value;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
await db.update(pwSubscriptions).set(set).where(eq(pwSubscriptions.id, id));
|
|
456
|
+
const rows = await db.select().from(pwSubscriptions).where(eq(pwSubscriptions.id, id));
|
|
457
|
+
const row = rows[0];
|
|
458
|
+
if (!row) {
|
|
459
|
+
throw new PayweaveNotFoundError(
|
|
460
|
+
`payweave/db/drizzle (mysql): subscriptions.update: no subscription with id ${JSON.stringify(id)}.`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return row;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error instanceof PayweaveNotFoundError) throw error;
|
|
466
|
+
throw wrapDriverError(
|
|
467
|
+
error,
|
|
468
|
+
`payweave/db/drizzle (mysql): subscriptions.update(${JSON.stringify(id)}) failed.`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function balancesGet(db, customerId, featureId, group) {
|
|
473
|
+
const rows = await db.select().from(pwFeatureBalances).where(
|
|
474
|
+
and(
|
|
475
|
+
eq(pwFeatureBalances.customerId, customerId),
|
|
476
|
+
eq(pwFeatureBalances.featureId, featureId),
|
|
477
|
+
eq(pwFeatureBalances.group, group)
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
return rows[0] ?? null;
|
|
481
|
+
}
|
|
482
|
+
async function balancesConsume(db, input) {
|
|
483
|
+
return db.transaction(async (tx) => {
|
|
484
|
+
const existingRows = await tx.select().from(pwFeatureBalances).where(
|
|
485
|
+
and(
|
|
486
|
+
eq(pwFeatureBalances.customerId, input.customerId),
|
|
487
|
+
eq(pwFeatureBalances.featureId, input.featureId),
|
|
488
|
+
eq(pwFeatureBalances.group, input.group)
|
|
489
|
+
)
|
|
490
|
+
).for("update");
|
|
491
|
+
const existingRow = existingRows[0];
|
|
492
|
+
const nowMs = input.now.getTime();
|
|
493
|
+
if (!existingRow) {
|
|
494
|
+
const period2 = currentPeriod(input.init.anchor.getTime(), input.init.resetInterval, nowMs);
|
|
495
|
+
const remaining2 = input.init.limit;
|
|
496
|
+
const applied2 = input.conditional !== true || remaining2 >= input.amount;
|
|
497
|
+
const used = applied2 ? input.amount : 0;
|
|
498
|
+
const id = generatePwId(nowMs);
|
|
499
|
+
await tx.insert(pwFeatureBalances).values({
|
|
500
|
+
id,
|
|
501
|
+
customerId: input.customerId,
|
|
502
|
+
featureId: input.featureId,
|
|
503
|
+
group: input.group,
|
|
504
|
+
used,
|
|
505
|
+
limit: input.init.limit,
|
|
506
|
+
resetInterval: input.init.resetInterval,
|
|
507
|
+
anchor: input.init.anchor,
|
|
508
|
+
periodStart: new Date(period2.start),
|
|
509
|
+
periodEnd: new Date(period2.end),
|
|
510
|
+
planId: input.init.planId,
|
|
511
|
+
planVersion: input.init.planVersion,
|
|
512
|
+
updatedAt: input.now
|
|
513
|
+
});
|
|
514
|
+
const rows2 = await tx.select().from(pwFeatureBalances).where(eq(pwFeatureBalances.id, id));
|
|
515
|
+
const row = rows2[0];
|
|
516
|
+
if (!row) {
|
|
517
|
+
throw new Error("drizzle adapter (mysql): balances.consume (create) returned no row");
|
|
518
|
+
}
|
|
519
|
+
return { ...row, applied: applied2 };
|
|
520
|
+
}
|
|
521
|
+
const resetDue = nowMs >= existingRow.periodEnd.getTime();
|
|
522
|
+
const baseUsed = resetDue ? 0 : existingRow.used;
|
|
523
|
+
const period = resetDue ? currentPeriod(existingRow.anchor.getTime(), existingRow.resetInterval, nowMs) : { start: existingRow.periodStart.getTime(), end: existingRow.periodEnd.getTime() };
|
|
524
|
+
const remaining = existingRow.limit - baseUsed;
|
|
525
|
+
const applied = input.conditional !== true || remaining >= input.amount;
|
|
526
|
+
if (!applied && !resetDue) {
|
|
527
|
+
return { ...existingRow, applied: false };
|
|
528
|
+
}
|
|
529
|
+
const finalUsed = applied ? baseUsed + input.amount : baseUsed;
|
|
530
|
+
await tx.update(pwFeatureBalances).set({
|
|
531
|
+
used: finalUsed,
|
|
532
|
+
periodStart: new Date(period.start),
|
|
533
|
+
periodEnd: new Date(period.end),
|
|
534
|
+
updatedAt: input.now
|
|
535
|
+
}).where(eq(pwFeatureBalances.id, existingRow.id));
|
|
536
|
+
const rows = await tx.select().from(pwFeatureBalances).where(eq(pwFeatureBalances.id, existingRow.id));
|
|
537
|
+
const updatedRow = rows[0];
|
|
538
|
+
if (!updatedRow) {
|
|
539
|
+
throw new Error("drizzle adapter (mysql): balances.consume (update) returned no row");
|
|
540
|
+
}
|
|
541
|
+
return { ...updatedRow, applied };
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
async function balancesResetTo(db, customerId, featureId, group, init) {
|
|
545
|
+
const now = /* @__PURE__ */ new Date();
|
|
546
|
+
const period = currentPeriod(init.anchor.getTime(), init.resetInterval, now.getTime());
|
|
547
|
+
const id = generatePwId(now.getTime());
|
|
548
|
+
await db.insert(pwFeatureBalances).values({
|
|
549
|
+
id,
|
|
550
|
+
customerId,
|
|
551
|
+
featureId,
|
|
552
|
+
group,
|
|
553
|
+
used: 0,
|
|
554
|
+
limit: init.limit,
|
|
555
|
+
resetInterval: init.resetInterval,
|
|
556
|
+
anchor: init.anchor,
|
|
557
|
+
periodStart: new Date(period.start),
|
|
558
|
+
periodEnd: new Date(period.end),
|
|
559
|
+
planId: init.planId,
|
|
560
|
+
planVersion: init.planVersion,
|
|
561
|
+
updatedAt: now
|
|
562
|
+
}).onDuplicateKeyUpdate({
|
|
563
|
+
set: {
|
|
564
|
+
used: 0,
|
|
565
|
+
limit: init.limit,
|
|
566
|
+
resetInterval: init.resetInterval,
|
|
567
|
+
anchor: init.anchor,
|
|
568
|
+
periodStart: new Date(period.start),
|
|
569
|
+
periodEnd: new Date(period.end),
|
|
570
|
+
planId: init.planId,
|
|
571
|
+
planVersion: init.planVersion,
|
|
572
|
+
updatedAt: now
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async function webhookEventsClaim(db, dedupeKey, meta) {
|
|
577
|
+
const staleClaimAfterMs = meta.staleClaimAfterMs ?? DEFAULT_STALE_CLAIM_AFTER_MS;
|
|
578
|
+
const staleThreshold = new Date(meta.now.getTime() - staleClaimAfterMs);
|
|
579
|
+
const raw = await db.execute(
|
|
580
|
+
sql`insert into ${pwWebhookEvents} (dedupe_key, provider, type, received_at, claimed_at, applied_at)
|
|
581
|
+
values (${dedupeKey}, ${meta.provider}, ${meta.type}, ${meta.now}, ${meta.now}, null)
|
|
582
|
+
on duplicate key update claimed_at = if(applied_at is null and claimed_at <= ${staleThreshold}, ${meta.now}, claimed_at)`
|
|
583
|
+
);
|
|
584
|
+
const result = asWriteResult(raw);
|
|
585
|
+
return result.affectedRows === 1 || result.affectedRows === 2;
|
|
586
|
+
}
|
|
587
|
+
async function webhookEventsMarkApplied(db, dedupeKey) {
|
|
588
|
+
await db.update(pwWebhookEvents).set({ appliedAt: /* @__PURE__ */ new Date() }).where(eq(pwWebhookEvents.dedupeKey, dedupeKey));
|
|
589
|
+
}
|
|
590
|
+
var ACTIVE_STATUS_SQL_LIST2 = sql.raw(
|
|
591
|
+
PW_ACTIVE_SUBSCRIPTION_STATUSES.map((s) => `'${s}'`).join(", ")
|
|
592
|
+
);
|
|
593
|
+
var pwCustomers2 = pgTable(PW_TABLES.customers, {
|
|
594
|
+
id: text("id").primaryKey(),
|
|
595
|
+
externalId: text("external_id").notNull().unique(),
|
|
596
|
+
providerIds: jsonb("provider_ids").$type().notNull(),
|
|
597
|
+
email: text("email"),
|
|
598
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
|
599
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
|
|
600
|
+
});
|
|
601
|
+
var pwPlans2 = pgTable(
|
|
602
|
+
PW_TABLES.plans,
|
|
603
|
+
{
|
|
604
|
+
id: text("id").primaryKey(),
|
|
605
|
+
planId: text("plan_id").notNull(),
|
|
606
|
+
version: integer("version").notNull(),
|
|
607
|
+
group: text("group").notNull(),
|
|
608
|
+
isDefault: boolean$1("is_default").notNull(),
|
|
609
|
+
name: text("name"),
|
|
610
|
+
priceMinor: bigint$1("price_minor", { mode: "number" }),
|
|
611
|
+
priceCurrency: text("price_currency"),
|
|
612
|
+
priceInterval: text("price_interval").$type(),
|
|
613
|
+
features: jsonb("features").$type().notNull(),
|
|
614
|
+
providerRefs: jsonb("provider_refs").$type().notNull(),
|
|
615
|
+
pushedAt: timestamp("pushed_at", { withTimezone: true }).notNull()
|
|
616
|
+
},
|
|
617
|
+
(t) => [uniqueIndex$1("pw_plans_plan_id_version_uq").on(t.planId, t.version)]
|
|
618
|
+
);
|
|
619
|
+
var pwSubscriptions2 = pgTable(
|
|
620
|
+
PW_TABLES.subscriptions,
|
|
621
|
+
{
|
|
622
|
+
id: text("id").primaryKey(),
|
|
623
|
+
customerId: text("customer_id").notNull().references(() => pwCustomers2.id),
|
|
624
|
+
planId: text("plan_id").notNull(),
|
|
625
|
+
planVersion: integer("plan_version").notNull(),
|
|
626
|
+
group: text("group").notNull(),
|
|
627
|
+
status: text("status").$type().notNull(),
|
|
628
|
+
provider: text("provider"),
|
|
629
|
+
providerSubscriptionRef: text("provider_subscription_ref"),
|
|
630
|
+
currentPeriodStart: timestamp("current_period_start", { withTimezone: true }).notNull(),
|
|
631
|
+
currentPeriodEnd: timestamp("current_period_end", { withTimezone: true }).notNull(),
|
|
632
|
+
cancelAtPeriodEnd: boolean$1("cancel_at_period_end").notNull(),
|
|
633
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
|
634
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
|
|
635
|
+
},
|
|
636
|
+
(t) => [
|
|
637
|
+
uniqueIndex$1("pw_subscriptions_active_uq").on(t.customerId, t.group).where(sql`${t.status} IN (${ACTIVE_STATUS_SQL_LIST2})`)
|
|
638
|
+
]
|
|
639
|
+
);
|
|
640
|
+
var pwFeatureBalances2 = pgTable(
|
|
641
|
+
PW_TABLES.featureBalances,
|
|
642
|
+
{
|
|
643
|
+
id: text("id").primaryKey(),
|
|
644
|
+
customerId: text("customer_id").notNull().references(() => pwCustomers2.id),
|
|
645
|
+
featureId: text("feature_id").notNull(),
|
|
646
|
+
group: text("group").notNull(),
|
|
647
|
+
used: bigint$1("used", { mode: "number" }).notNull(),
|
|
648
|
+
limit: bigint$1("limit", { mode: "number" }).notNull(),
|
|
649
|
+
resetInterval: text("reset_interval").$type().notNull(),
|
|
650
|
+
anchor: timestamp("anchor", { withTimezone: true }).notNull(),
|
|
651
|
+
periodStart: timestamp("period_start", { withTimezone: true }).notNull(),
|
|
652
|
+
periodEnd: timestamp("period_end", { withTimezone: true }).notNull(),
|
|
653
|
+
planId: text("plan_id").notNull(),
|
|
654
|
+
planVersion: integer("plan_version").notNull(),
|
|
655
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
|
|
656
|
+
},
|
|
657
|
+
(t) => [
|
|
658
|
+
uniqueIndex$1("pw_feature_balances_customer_feature_group_uq").on(
|
|
659
|
+
t.customerId,
|
|
660
|
+
t.featureId,
|
|
661
|
+
t.group
|
|
662
|
+
)
|
|
663
|
+
]
|
|
664
|
+
);
|
|
665
|
+
var pwWebhookEvents2 = pgTable(PW_TABLES.webhookEvents, {
|
|
666
|
+
dedupeKey: text("dedupe_key").primaryKey(),
|
|
667
|
+
provider: text("provider").notNull(),
|
|
668
|
+
type: text("type").notNull(),
|
|
669
|
+
receivedAt: timestamp("received_at", { withTimezone: true }).notNull(),
|
|
670
|
+
claimedAt: timestamp("claimed_at", { withTimezone: true }),
|
|
671
|
+
appliedAt: timestamp("applied_at", { withTimezone: true })
|
|
672
|
+
});
|
|
673
|
+
var pgSchema = {
|
|
674
|
+
pwCustomers: pwCustomers2,
|
|
675
|
+
pwPlans: pwPlans2,
|
|
676
|
+
pwSubscriptions: pwSubscriptions2,
|
|
677
|
+
pwFeatureBalances: pwFeatureBalances2,
|
|
678
|
+
pwWebhookEvents: pwWebhookEvents2
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/db/drizzle/postgres-adapter.ts
|
|
682
|
+
var DRIZZLE_KIT_INSTRUCTIONS2 = 'payweave/db/drizzle never runs migrations for you. Merge "payweave/db/drizzle"\'s published schema (see `./schema/pg.ts`) into your own Drizzle schema, then run `drizzle-kit push` (dev) or `drizzle-kit generate` + `drizzle-kit migrate` (tracked migrations) yourself.';
|
|
683
|
+
function buildPostgresAdapter(db) {
|
|
684
|
+
const ensureVerified = createVerifier(makeSqlTableProbe(db));
|
|
685
|
+
function guarded(fn) {
|
|
686
|
+
return async (...args) => {
|
|
687
|
+
await ensureVerified();
|
|
688
|
+
return fn(...args);
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
dialect: "postgres",
|
|
693
|
+
customers: {
|
|
694
|
+
getByExternalId: guarded((externalId) => customersGetByExternalId2(db, externalId)),
|
|
695
|
+
upsert: guarded((input) => customersUpsert2(db, input)),
|
|
696
|
+
linkProviderRef: guarded(
|
|
697
|
+
(externalId, provider, ref) => customersLinkProviderRef2(db, externalId, provider, ref)
|
|
698
|
+
)
|
|
699
|
+
},
|
|
700
|
+
plans: {
|
|
701
|
+
getActiveVersion: guarded((planId) => plansGetActiveVersion2(db, planId)),
|
|
702
|
+
listActive: guarded(() => plansListActive2(db)),
|
|
703
|
+
pushVersion: guarded((plan) => plansPushVersion2(db, plan))
|
|
704
|
+
},
|
|
705
|
+
subscriptions: {
|
|
706
|
+
getActive: guarded(
|
|
707
|
+
(customerId, group) => subscriptionsGetActive2(db, customerId, group)
|
|
708
|
+
),
|
|
709
|
+
create: guarded((input) => subscriptionsCreate2(db, input)),
|
|
710
|
+
update: guarded(
|
|
711
|
+
(id, patch) => subscriptionsUpdate2(db, id, patch)
|
|
712
|
+
)
|
|
713
|
+
},
|
|
714
|
+
balances: {
|
|
715
|
+
get: guarded(
|
|
716
|
+
(customerId, featureId, group) => balancesGet2(db, customerId, featureId, group)
|
|
717
|
+
),
|
|
718
|
+
consume: guarded((input) => balancesConsume2(db, input)),
|
|
719
|
+
resetTo: guarded(
|
|
720
|
+
(customerId, featureId, group, init) => balancesResetTo2(db, customerId, featureId, group, init)
|
|
721
|
+
)
|
|
722
|
+
},
|
|
723
|
+
webhookEvents: {
|
|
724
|
+
claim: guarded(
|
|
725
|
+
(dedupeKey, meta) => webhookEventsClaim2(db, dedupeKey, meta)
|
|
726
|
+
),
|
|
727
|
+
markApplied: guarded((dedupeKey) => webhookEventsMarkApplied2(db, dedupeKey))
|
|
728
|
+
},
|
|
729
|
+
migrations: {
|
|
730
|
+
status: async () => {
|
|
731
|
+
const missing = await findMissingTablesForStatus(makeSqlTableProbe(db));
|
|
732
|
+
return missing.length > 0 ? { pending: ["0001_init"], applied: [] } : { pending: [], applied: ["0001_init"] };
|
|
733
|
+
},
|
|
734
|
+
apply: () => Promise.resolve({ applied: [], instructions: DRIZZLE_KIT_INSTRUCTIONS2 })
|
|
735
|
+
},
|
|
736
|
+
transaction: (fn) => db.transaction((tx) => fn(buildPostgresAdapter(tx)))
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async function customersGetByExternalId2(db, externalId) {
|
|
740
|
+
const rows = await db.select().from(pwCustomers2).where(eq(pwCustomers2.externalId, externalId));
|
|
741
|
+
return rows[0] ?? null;
|
|
742
|
+
}
|
|
743
|
+
async function customersUpsert2(db, input) {
|
|
744
|
+
const now = /* @__PURE__ */ new Date();
|
|
745
|
+
const id = generatePwId(now.getTime());
|
|
746
|
+
try {
|
|
747
|
+
const rows = await db.insert(pwCustomers2).values({
|
|
748
|
+
id,
|
|
749
|
+
externalId: input.externalId,
|
|
750
|
+
providerIds: {},
|
|
751
|
+
email: input.email ?? null,
|
|
752
|
+
createdAt: now,
|
|
753
|
+
updatedAt: now
|
|
754
|
+
}).onConflictDoUpdate({
|
|
755
|
+
target: pwCustomers2.externalId,
|
|
756
|
+
set: {
|
|
757
|
+
email: sql`coalesce(excluded.email, ${pwCustomers2.email})`,
|
|
758
|
+
updatedAt: now
|
|
759
|
+
}
|
|
760
|
+
}).returning();
|
|
761
|
+
const row = rows[0];
|
|
762
|
+
if (!row) throw new Error("drizzle adapter (postgres): customers.upsert returned no row");
|
|
763
|
+
return row;
|
|
764
|
+
} catch (error) {
|
|
765
|
+
throw wrapDriverError(
|
|
766
|
+
error,
|
|
767
|
+
`payweave/db/drizzle (postgres): customers.upsert(${JSON.stringify(input.externalId)}) failed.`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function customersLinkProviderRef2(db, externalId, provider, ref) {
|
|
772
|
+
await db.transaction(async (tx) => {
|
|
773
|
+
const rows = await tx.select().from(pwCustomers2).where(eq(pwCustomers2.externalId, externalId)).for("update");
|
|
774
|
+
const row = rows[0];
|
|
775
|
+
if (!row) {
|
|
776
|
+
throw new PayweaveNotFoundError(
|
|
777
|
+
`payweave/db/drizzle (postgres): customers.linkProviderRef: no customer with externalId ${JSON.stringify(externalId)}.`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const providerIds = { ...row.providerIds, [provider]: ref };
|
|
781
|
+
await tx.update(pwCustomers2).set({ providerIds, updatedAt: /* @__PURE__ */ new Date() }).where(eq(pwCustomers2.externalId, externalId));
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async function plansGetActiveVersion2(db, planId) {
|
|
785
|
+
const rows = await db.select().from(pwPlans2).where(eq(pwPlans2.planId, planId)).orderBy(sql`${pwPlans2.version} desc`).limit(1);
|
|
786
|
+
return rows[0] ?? null;
|
|
787
|
+
}
|
|
788
|
+
async function plansListActive2(db) {
|
|
789
|
+
const latest = db.select({
|
|
790
|
+
planId: pwPlans2.planId,
|
|
791
|
+
version: sql`max(${pwPlans2.version})`.as("max_version")
|
|
792
|
+
}).from(pwPlans2).groupBy(pwPlans2.planId).as("latest");
|
|
793
|
+
return db.select({
|
|
794
|
+
id: pwPlans2.id,
|
|
795
|
+
planId: pwPlans2.planId,
|
|
796
|
+
version: pwPlans2.version,
|
|
797
|
+
group: pwPlans2.group,
|
|
798
|
+
isDefault: pwPlans2.isDefault,
|
|
799
|
+
name: pwPlans2.name,
|
|
800
|
+
priceMinor: pwPlans2.priceMinor,
|
|
801
|
+
priceCurrency: pwPlans2.priceCurrency,
|
|
802
|
+
priceInterval: pwPlans2.priceInterval,
|
|
803
|
+
features: pwPlans2.features,
|
|
804
|
+
providerRefs: pwPlans2.providerRefs,
|
|
805
|
+
pushedAt: pwPlans2.pushedAt
|
|
806
|
+
}).from(pwPlans2).innerJoin(latest, and(eq(latest.planId, pwPlans2.planId), eq(latest.version, pwPlans2.version)));
|
|
807
|
+
}
|
|
808
|
+
async function plansPushVersion2(db, plan) {
|
|
809
|
+
return db.transaction(async (tx) => {
|
|
810
|
+
const activeRows = await tx.select().from(pwPlans2).where(eq(pwPlans2.planId, plan.planId)).orderBy(sql`${pwPlans2.version} desc`).limit(1).for("update");
|
|
811
|
+
const active = activeRows[0] ?? null;
|
|
812
|
+
if (active && planContentEquals(active, plan)) {
|
|
813
|
+
return active;
|
|
814
|
+
}
|
|
815
|
+
const now = /* @__PURE__ */ new Date();
|
|
816
|
+
const id = generatePwId(now.getTime());
|
|
817
|
+
const nextVersion = (active?.version ?? 0) + 1;
|
|
818
|
+
const rows = await tx.insert(pwPlans2).values({
|
|
819
|
+
id,
|
|
820
|
+
planId: plan.planId,
|
|
821
|
+
version: nextVersion,
|
|
822
|
+
group: plan.group,
|
|
823
|
+
isDefault: plan.isDefault,
|
|
824
|
+
name: plan.name,
|
|
825
|
+
priceMinor: plan.priceMinor,
|
|
826
|
+
priceCurrency: plan.priceCurrency,
|
|
827
|
+
priceInterval: plan.priceInterval,
|
|
828
|
+
features: plan.features,
|
|
829
|
+
providerRefs: plan.providerRefs,
|
|
830
|
+
pushedAt: now
|
|
831
|
+
}).returning();
|
|
832
|
+
const row = rows[0];
|
|
833
|
+
if (!row) throw new Error("drizzle adapter (postgres): plans.pushVersion returned no row");
|
|
834
|
+
return row;
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
async function subscriptionsGetActive2(db, customerId, group) {
|
|
838
|
+
const rows = await db.select().from(pwSubscriptions2).where(
|
|
839
|
+
and(
|
|
840
|
+
eq(pwSubscriptions2.customerId, customerId),
|
|
841
|
+
eq(pwSubscriptions2.group, group),
|
|
842
|
+
sql`${pwSubscriptions2.status} in ${PW_ACTIVE_SUBSCRIPTION_STATUSES}`
|
|
843
|
+
)
|
|
844
|
+
);
|
|
845
|
+
return rows[0] ?? null;
|
|
846
|
+
}
|
|
847
|
+
async function subscriptionsCreate2(db, input) {
|
|
848
|
+
const now = /* @__PURE__ */ new Date();
|
|
849
|
+
const id = generatePwId(now.getTime());
|
|
850
|
+
try {
|
|
851
|
+
const rows = await db.insert(pwSubscriptions2).values({
|
|
852
|
+
id,
|
|
853
|
+
customerId: input.customerId,
|
|
854
|
+
planId: input.planId,
|
|
855
|
+
planVersion: input.planVersion,
|
|
856
|
+
group: input.group,
|
|
857
|
+
status: input.status,
|
|
858
|
+
provider: input.provider,
|
|
859
|
+
providerSubscriptionRef: input.providerSubscriptionRef,
|
|
860
|
+
currentPeriodStart: input.currentPeriodStart,
|
|
861
|
+
currentPeriodEnd: input.currentPeriodEnd,
|
|
862
|
+
cancelAtPeriodEnd: input.cancelAtPeriodEnd,
|
|
863
|
+
createdAt: now,
|
|
864
|
+
updatedAt: now
|
|
865
|
+
}).returning();
|
|
866
|
+
const row = rows[0];
|
|
867
|
+
if (!row) throw new Error("drizzle adapter (postgres): subscriptions.create returned no row");
|
|
868
|
+
return row;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
throw wrapDriverError(
|
|
871
|
+
error,
|
|
872
|
+
`payweave/db/drizzle (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).`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function subscriptionsUpdate2(db, id, patch) {
|
|
877
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
878
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
879
|
+
if (value !== void 0) set[key] = value;
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
const rows = await db.update(pwSubscriptions2).set(set).where(eq(pwSubscriptions2.id, id)).returning();
|
|
883
|
+
const row = rows[0];
|
|
884
|
+
if (!row) {
|
|
885
|
+
throw new PayweaveNotFoundError(
|
|
886
|
+
`payweave/db/drizzle (postgres): subscriptions.update: no subscription with id ${JSON.stringify(id)}.`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
return row;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
if (error instanceof PayweaveNotFoundError) throw error;
|
|
892
|
+
throw wrapDriverError(
|
|
893
|
+
error,
|
|
894
|
+
`payweave/db/drizzle (postgres): subscriptions.update(${JSON.stringify(id)}) failed.`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function balancesGet2(db, customerId, featureId, group) {
|
|
899
|
+
const rows = await db.select().from(pwFeatureBalances2).where(
|
|
900
|
+
and(
|
|
901
|
+
eq(pwFeatureBalances2.customerId, customerId),
|
|
902
|
+
eq(pwFeatureBalances2.featureId, featureId),
|
|
903
|
+
eq(pwFeatureBalances2.group, group)
|
|
904
|
+
)
|
|
905
|
+
);
|
|
906
|
+
return rows[0] ?? null;
|
|
907
|
+
}
|
|
908
|
+
async function balancesConsume2(db, input) {
|
|
909
|
+
return db.transaction(async (tx) => {
|
|
910
|
+
const existingRows = await tx.select().from(pwFeatureBalances2).where(
|
|
911
|
+
and(
|
|
912
|
+
eq(pwFeatureBalances2.customerId, input.customerId),
|
|
913
|
+
eq(pwFeatureBalances2.featureId, input.featureId),
|
|
914
|
+
eq(pwFeatureBalances2.group, input.group)
|
|
915
|
+
)
|
|
916
|
+
).for("update");
|
|
917
|
+
const existingRow = existingRows[0];
|
|
918
|
+
const nowMs = input.now.getTime();
|
|
919
|
+
if (!existingRow) {
|
|
920
|
+
const period2 = currentPeriod(input.init.anchor.getTime(), input.init.resetInterval, nowMs);
|
|
921
|
+
const remaining2 = input.init.limit;
|
|
922
|
+
const applied2 = input.conditional !== true || remaining2 >= input.amount;
|
|
923
|
+
const used = applied2 ? input.amount : 0;
|
|
924
|
+
const id = generatePwId(nowMs);
|
|
925
|
+
const rows2 = await tx.insert(pwFeatureBalances2).values({
|
|
926
|
+
id,
|
|
927
|
+
customerId: input.customerId,
|
|
928
|
+
featureId: input.featureId,
|
|
929
|
+
group: input.group,
|
|
930
|
+
used,
|
|
931
|
+
limit: input.init.limit,
|
|
932
|
+
resetInterval: input.init.resetInterval,
|
|
933
|
+
anchor: input.init.anchor,
|
|
934
|
+
periodStart: new Date(period2.start),
|
|
935
|
+
periodEnd: new Date(period2.end),
|
|
936
|
+
planId: input.init.planId,
|
|
937
|
+
planVersion: input.init.planVersion,
|
|
938
|
+
updatedAt: input.now
|
|
939
|
+
}).returning();
|
|
940
|
+
const row = rows2[0];
|
|
941
|
+
if (!row) {
|
|
942
|
+
throw new Error("drizzle adapter (postgres): balances.consume (create) returned no row");
|
|
943
|
+
}
|
|
944
|
+
return { ...row, applied: applied2 };
|
|
945
|
+
}
|
|
946
|
+
const resetDue = nowMs >= existingRow.periodEnd.getTime();
|
|
947
|
+
const baseUsed = resetDue ? 0 : existingRow.used;
|
|
948
|
+
const period = resetDue ? currentPeriod(existingRow.anchor.getTime(), existingRow.resetInterval, nowMs) : { start: existingRow.periodStart.getTime(), end: existingRow.periodEnd.getTime() };
|
|
949
|
+
const remaining = existingRow.limit - baseUsed;
|
|
950
|
+
const applied = input.conditional !== true || remaining >= input.amount;
|
|
951
|
+
if (!applied && !resetDue) {
|
|
952
|
+
return { ...existingRow, applied: false };
|
|
953
|
+
}
|
|
954
|
+
const finalUsed = applied ? baseUsed + input.amount : baseUsed;
|
|
955
|
+
const rows = await tx.update(pwFeatureBalances2).set({
|
|
956
|
+
used: finalUsed,
|
|
957
|
+
periodStart: new Date(period.start),
|
|
958
|
+
periodEnd: new Date(period.end),
|
|
959
|
+
updatedAt: input.now
|
|
960
|
+
}).where(eq(pwFeatureBalances2.id, existingRow.id)).returning();
|
|
961
|
+
const updatedRow = rows[0];
|
|
962
|
+
if (!updatedRow) {
|
|
963
|
+
throw new Error("drizzle adapter (postgres): balances.consume (update) returned no row");
|
|
964
|
+
}
|
|
965
|
+
return { ...updatedRow, applied };
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
async function balancesResetTo2(db, customerId, featureId, group, init) {
|
|
969
|
+
const now = /* @__PURE__ */ new Date();
|
|
970
|
+
const period = currentPeriod(init.anchor.getTime(), init.resetInterval, now.getTime());
|
|
971
|
+
const id = generatePwId(now.getTime());
|
|
972
|
+
await db.insert(pwFeatureBalances2).values({
|
|
973
|
+
id,
|
|
974
|
+
customerId,
|
|
975
|
+
featureId,
|
|
976
|
+
group,
|
|
977
|
+
used: 0,
|
|
978
|
+
limit: init.limit,
|
|
979
|
+
resetInterval: init.resetInterval,
|
|
980
|
+
anchor: init.anchor,
|
|
981
|
+
periodStart: new Date(period.start),
|
|
982
|
+
periodEnd: new Date(period.end),
|
|
983
|
+
planId: init.planId,
|
|
984
|
+
planVersion: init.planVersion,
|
|
985
|
+
updatedAt: now
|
|
986
|
+
}).onConflictDoUpdate({
|
|
987
|
+
target: [pwFeatureBalances2.customerId, pwFeatureBalances2.featureId, pwFeatureBalances2.group],
|
|
988
|
+
set: {
|
|
989
|
+
used: 0,
|
|
990
|
+
limit: init.limit,
|
|
991
|
+
resetInterval: init.resetInterval,
|
|
992
|
+
anchor: init.anchor,
|
|
993
|
+
periodStart: new Date(period.start),
|
|
994
|
+
periodEnd: new Date(period.end),
|
|
995
|
+
planId: init.planId,
|
|
996
|
+
planVersion: init.planVersion,
|
|
997
|
+
updatedAt: now
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
async function webhookEventsClaim2(db, dedupeKey, meta) {
|
|
1002
|
+
const staleClaimAfterMs = meta.staleClaimAfterMs ?? DEFAULT_STALE_CLAIM_AFTER_MS;
|
|
1003
|
+
const staleThreshold = new Date(meta.now.getTime() - staleClaimAfterMs);
|
|
1004
|
+
const rows = await db.insert(pwWebhookEvents2).values({
|
|
1005
|
+
dedupeKey,
|
|
1006
|
+
provider: meta.provider,
|
|
1007
|
+
type: meta.type,
|
|
1008
|
+
receivedAt: meta.now,
|
|
1009
|
+
claimedAt: meta.now,
|
|
1010
|
+
appliedAt: null
|
|
1011
|
+
}).onConflictDoUpdate({
|
|
1012
|
+
target: pwWebhookEvents2.dedupeKey,
|
|
1013
|
+
set: { claimedAt: meta.now },
|
|
1014
|
+
// See the sqlite dialect file's identical comment: `and(...)` with two
|
|
1015
|
+
// fixed conditions never actually returns `undefined`.
|
|
1016
|
+
setWhere: and(
|
|
1017
|
+
sql`${pwWebhookEvents2.appliedAt} is null`,
|
|
1018
|
+
sql`${pwWebhookEvents2.claimedAt} <= ${staleThreshold}`
|
|
1019
|
+
)
|
|
1020
|
+
}).returning({ claimedAt: pwWebhookEvents2.claimedAt });
|
|
1021
|
+
const row = rows[0];
|
|
1022
|
+
return row !== void 0 && row.claimedAt !== null && row.claimedAt.getTime() === meta.now.getTime();
|
|
1023
|
+
}
|
|
1024
|
+
async function webhookEventsMarkApplied2(db, dedupeKey) {
|
|
1025
|
+
await db.update(pwWebhookEvents2).set({ appliedAt: /* @__PURE__ */ new Date() }).where(eq(pwWebhookEvents2.dedupeKey, dedupeKey));
|
|
1026
|
+
}
|
|
1027
|
+
var ACTIVE_STATUS_SQL_LIST3 = sql.raw(
|
|
1028
|
+
PW_ACTIVE_SUBSCRIPTION_STATUSES.map((s) => `'${s}'`).join(", ")
|
|
1029
|
+
);
|
|
1030
|
+
var pwCustomers3 = sqliteTable(PW_TABLES.customers, {
|
|
1031
|
+
id: text$1("id").primaryKey(),
|
|
1032
|
+
externalId: text$1("external_id").notNull().unique(),
|
|
1033
|
+
providerIds: text$1("provider_ids", { mode: "json" }).$type().notNull(),
|
|
1034
|
+
email: text$1("email"),
|
|
1035
|
+
createdAt: integer$1("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
1036
|
+
updatedAt: integer$1("updated_at", { mode: "timestamp_ms" }).notNull()
|
|
1037
|
+
});
|
|
1038
|
+
var pwPlans3 = sqliteTable(
|
|
1039
|
+
PW_TABLES.plans,
|
|
1040
|
+
{
|
|
1041
|
+
id: text$1("id").primaryKey(),
|
|
1042
|
+
planId: text$1("plan_id").notNull(),
|
|
1043
|
+
version: integer$1("version").notNull(),
|
|
1044
|
+
group: text$1("group").notNull(),
|
|
1045
|
+
isDefault: integer$1("is_default", { mode: "boolean" }).notNull(),
|
|
1046
|
+
name: text$1("name"),
|
|
1047
|
+
priceMinor: integer$1("price_minor"),
|
|
1048
|
+
priceCurrency: text$1("price_currency"),
|
|
1049
|
+
priceInterval: text$1("price_interval").$type(),
|
|
1050
|
+
features: text$1("features", { mode: "json" }).$type().notNull(),
|
|
1051
|
+
providerRefs: text$1("provider_refs", { mode: "json" }).$type().notNull(),
|
|
1052
|
+
pushedAt: integer$1("pushed_at", { mode: "timestamp_ms" }).notNull()
|
|
1053
|
+
},
|
|
1054
|
+
(t) => [uniqueIndex$2("pw_plans_plan_id_version_uq").on(t.planId, t.version)]
|
|
1055
|
+
);
|
|
1056
|
+
var pwSubscriptions3 = sqliteTable(
|
|
1057
|
+
PW_TABLES.subscriptions,
|
|
1058
|
+
{
|
|
1059
|
+
id: text$1("id").primaryKey(),
|
|
1060
|
+
customerId: text$1("customer_id").notNull(),
|
|
1061
|
+
planId: text$1("plan_id").notNull(),
|
|
1062
|
+
planVersion: integer$1("plan_version").notNull(),
|
|
1063
|
+
group: text$1("group").notNull(),
|
|
1064
|
+
status: text$1("status").$type().notNull(),
|
|
1065
|
+
provider: text$1("provider"),
|
|
1066
|
+
providerSubscriptionRef: text$1("provider_subscription_ref"),
|
|
1067
|
+
currentPeriodStart: integer$1("current_period_start", { mode: "timestamp_ms" }).notNull(),
|
|
1068
|
+
currentPeriodEnd: integer$1("current_period_end", { mode: "timestamp_ms" }).notNull(),
|
|
1069
|
+
cancelAtPeriodEnd: integer$1("cancel_at_period_end", { mode: "boolean" }).notNull(),
|
|
1070
|
+
createdAt: integer$1("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
1071
|
+
updatedAt: integer$1("updated_at", { mode: "timestamp_ms" }).notNull()
|
|
1072
|
+
},
|
|
1073
|
+
(t) => [
|
|
1074
|
+
uniqueIndex$2("pw_subscriptions_active_uq").on(t.customerId, t.group).where(sql`${t.status} IN (${ACTIVE_STATUS_SQL_LIST3})`)
|
|
1075
|
+
]
|
|
1076
|
+
);
|
|
1077
|
+
var pwFeatureBalances3 = sqliteTable(
|
|
1078
|
+
PW_TABLES.featureBalances,
|
|
1079
|
+
{
|
|
1080
|
+
id: text$1("id").primaryKey(),
|
|
1081
|
+
customerId: text$1("customer_id").notNull(),
|
|
1082
|
+
featureId: text$1("feature_id").notNull(),
|
|
1083
|
+
group: text$1("group").notNull(),
|
|
1084
|
+
used: integer$1("used").notNull(),
|
|
1085
|
+
limit: integer$1("limit").notNull(),
|
|
1086
|
+
resetInterval: text$1("reset_interval").$type().notNull(),
|
|
1087
|
+
anchor: integer$1("anchor", { mode: "timestamp_ms" }).notNull(),
|
|
1088
|
+
periodStart: integer$1("period_start", { mode: "timestamp_ms" }).notNull(),
|
|
1089
|
+
periodEnd: integer$1("period_end", { mode: "timestamp_ms" }).notNull(),
|
|
1090
|
+
planId: text$1("plan_id").notNull(),
|
|
1091
|
+
planVersion: integer$1("plan_version").notNull(),
|
|
1092
|
+
updatedAt: integer$1("updated_at", { mode: "timestamp_ms" }).notNull()
|
|
1093
|
+
},
|
|
1094
|
+
(t) => [
|
|
1095
|
+
uniqueIndex$2("pw_feature_balances_customer_feature_group_uq").on(
|
|
1096
|
+
t.customerId,
|
|
1097
|
+
t.featureId,
|
|
1098
|
+
t.group
|
|
1099
|
+
)
|
|
1100
|
+
]
|
|
1101
|
+
);
|
|
1102
|
+
var pwWebhookEvents3 = sqliteTable(PW_TABLES.webhookEvents, {
|
|
1103
|
+
dedupeKey: text$1("dedupe_key").primaryKey(),
|
|
1104
|
+
provider: text$1("provider").notNull(),
|
|
1105
|
+
type: text$1("type").notNull(),
|
|
1106
|
+
receivedAt: integer$1("received_at", { mode: "timestamp_ms" }).notNull(),
|
|
1107
|
+
claimedAt: integer$1("claimed_at", { mode: "timestamp_ms" }),
|
|
1108
|
+
appliedAt: integer$1("applied_at", { mode: "timestamp_ms" })
|
|
1109
|
+
});
|
|
1110
|
+
var sqliteSchema = {
|
|
1111
|
+
pwCustomers: pwCustomers3,
|
|
1112
|
+
pwPlans: pwPlans3,
|
|
1113
|
+
pwSubscriptions: pwSubscriptions3,
|
|
1114
|
+
pwFeatureBalances: pwFeatureBalances3,
|
|
1115
|
+
pwWebhookEvents: pwWebhookEvents3
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/db/drizzle/sqlite-adapter.ts
|
|
1119
|
+
var SerialQueue = class {
|
|
1120
|
+
#tail = Promise.resolve();
|
|
1121
|
+
run(fn) {
|
|
1122
|
+
const result = this.#tail.then(fn);
|
|
1123
|
+
this.#tail = result.then(
|
|
1124
|
+
() => void 0,
|
|
1125
|
+
() => void 0
|
|
1126
|
+
);
|
|
1127
|
+
return result;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
var TopLevelRunner = class {
|
|
1131
|
+
#queue = new SerialQueue();
|
|
1132
|
+
enqueue(fn) {
|
|
1133
|
+
return this.#queue.run(fn);
|
|
1134
|
+
}
|
|
1135
|
+
transaction(db, fn) {
|
|
1136
|
+
return this.#queue.run(async () => {
|
|
1137
|
+
await db.run(sql`BEGIN IMMEDIATE`);
|
|
1138
|
+
try {
|
|
1139
|
+
const nested = buildSqliteAdapter(db, new NestedRunner());
|
|
1140
|
+
const result = await fn(nested);
|
|
1141
|
+
await db.run(sql`COMMIT`);
|
|
1142
|
+
return result;
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
try {
|
|
1145
|
+
await db.run(sql`ROLLBACK`);
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
throw error;
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
var NestedRunner = class {
|
|
1154
|
+
enqueue(fn) {
|
|
1155
|
+
return fn();
|
|
1156
|
+
}
|
|
1157
|
+
transaction(db, fn) {
|
|
1158
|
+
return fn(buildSqliteAdapter(db, this));
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
function buildSqliteAdapter(db, runner = new TopLevelRunner()) {
|
|
1162
|
+
const ensureVerified = createVerifier(makeSqliteTableProbe(db));
|
|
1163
|
+
function guarded(fn) {
|
|
1164
|
+
return (...args) => runner.enqueue(async () => {
|
|
1165
|
+
await ensureVerified();
|
|
1166
|
+
return fn(...args);
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
dialect: "sqlite",
|
|
1171
|
+
customers: {
|
|
1172
|
+
getByExternalId: guarded((externalId) => customersGetByExternalId3(db, externalId)),
|
|
1173
|
+
upsert: guarded((input) => customersUpsert3(db, input)),
|
|
1174
|
+
linkProviderRef: guarded(
|
|
1175
|
+
(externalId, provider, ref) => customersLinkProviderRef3(db, externalId, provider, ref)
|
|
1176
|
+
)
|
|
1177
|
+
},
|
|
1178
|
+
plans: {
|
|
1179
|
+
getActiveVersion: guarded((planId) => plansGetActiveVersion3(db, planId)),
|
|
1180
|
+
listActive: guarded(() => plansListActive3(db)),
|
|
1181
|
+
pushVersion: guarded((plan) => plansPushVersion3(db, plan))
|
|
1182
|
+
},
|
|
1183
|
+
subscriptions: {
|
|
1184
|
+
getActive: guarded(
|
|
1185
|
+
(customerId, group) => subscriptionsGetActive3(db, customerId, group)
|
|
1186
|
+
),
|
|
1187
|
+
create: guarded((input) => subscriptionsCreate3(db, input)),
|
|
1188
|
+
update: guarded(
|
|
1189
|
+
(id, patch) => subscriptionsUpdate3(db, id, patch)
|
|
1190
|
+
)
|
|
1191
|
+
},
|
|
1192
|
+
balances: {
|
|
1193
|
+
get: guarded(
|
|
1194
|
+
(customerId, featureId, group) => balancesGet3(db, customerId, featureId, group)
|
|
1195
|
+
),
|
|
1196
|
+
consume: guarded((input) => balancesConsume3(db, input)),
|
|
1197
|
+
resetTo: guarded(
|
|
1198
|
+
(customerId, featureId, group, init) => balancesResetTo3(db, customerId, featureId, group, init)
|
|
1199
|
+
)
|
|
1200
|
+
},
|
|
1201
|
+
webhookEvents: {
|
|
1202
|
+
claim: guarded(
|
|
1203
|
+
(dedupeKey, meta) => webhookEventsClaim3(db, dedupeKey, meta)
|
|
1204
|
+
),
|
|
1205
|
+
markApplied: guarded((dedupeKey) => webhookEventsMarkApplied3(db, dedupeKey))
|
|
1206
|
+
},
|
|
1207
|
+
migrations: {
|
|
1208
|
+
status: () => sqliteMigrationsStatus(db),
|
|
1209
|
+
apply: () => Promise.resolve(sqliteMigrationsApply())
|
|
1210
|
+
},
|
|
1211
|
+
transaction: (fn) => runner.transaction(db, fn)
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
var DRIZZLE_KIT_INSTRUCTIONS3 = 'payweave/db/drizzle never runs migrations for you. Merge "payweave/db/drizzle"\'s published schema (see `./schema/sqlite.ts`) into your own Drizzle schema, then run `drizzle-kit push` (dev) or `drizzle-kit generate` + `drizzle-kit migrate` (tracked migrations) yourself.';
|
|
1215
|
+
function sqliteMigrationsApply() {
|
|
1216
|
+
return { applied: [], instructions: DRIZZLE_KIT_INSTRUCTIONS3 };
|
|
1217
|
+
}
|
|
1218
|
+
async function sqliteMigrationsStatus(db) {
|
|
1219
|
+
const missing = await findMissingTablesForStatus(makeSqliteTableProbe(db));
|
|
1220
|
+
return missing.length > 0 ? { pending: ["0001_init"], applied: [] } : { pending: [], applied: ["0001_init"] };
|
|
1221
|
+
}
|
|
1222
|
+
async function customersGetByExternalId3(db, externalId) {
|
|
1223
|
+
const rows = await db.select().from(pwCustomers3).where(eq(pwCustomers3.externalId, externalId));
|
|
1224
|
+
return rows[0] ?? null;
|
|
1225
|
+
}
|
|
1226
|
+
async function customersUpsert3(db, input) {
|
|
1227
|
+
const now = /* @__PURE__ */ new Date();
|
|
1228
|
+
const id = generatePwId(now.getTime());
|
|
1229
|
+
try {
|
|
1230
|
+
const rows = await db.insert(pwCustomers3).values({
|
|
1231
|
+
id,
|
|
1232
|
+
externalId: input.externalId,
|
|
1233
|
+
providerIds: {},
|
|
1234
|
+
email: input.email ?? null,
|
|
1235
|
+
createdAt: now,
|
|
1236
|
+
updatedAt: now
|
|
1237
|
+
}).onConflictDoUpdate({
|
|
1238
|
+
target: pwCustomers3.externalId,
|
|
1239
|
+
set: {
|
|
1240
|
+
email: sql`coalesce(excluded.email, ${pwCustomers3.email})`,
|
|
1241
|
+
updatedAt: now
|
|
1242
|
+
}
|
|
1243
|
+
}).returning();
|
|
1244
|
+
const row = rows[0];
|
|
1245
|
+
if (!row) throw new Error("drizzle adapter (sqlite): customers.upsert returned no row");
|
|
1246
|
+
return row;
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
throw wrapDriverError(
|
|
1249
|
+
error,
|
|
1250
|
+
`payweave/db/drizzle (sqlite): customers.upsert(${JSON.stringify(input.externalId)}) failed.`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function customersLinkProviderRef3(db, externalId, provider, ref) {
|
|
1255
|
+
const rows = await db.select().from(pwCustomers3).where(eq(pwCustomers3.externalId, externalId));
|
|
1256
|
+
const row = rows[0];
|
|
1257
|
+
if (!row) {
|
|
1258
|
+
throw new PayweaveNotFoundError(
|
|
1259
|
+
`payweave/db/drizzle (sqlite): customers.linkProviderRef: no customer with externalId ${JSON.stringify(externalId)}.`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
const providerIds = { ...row.providerIds, [provider]: ref };
|
|
1263
|
+
await db.update(pwCustomers3).set({ providerIds, updatedAt: /* @__PURE__ */ new Date() }).where(eq(pwCustomers3.externalId, externalId));
|
|
1264
|
+
}
|
|
1265
|
+
async function plansGetActiveVersion3(db, planId) {
|
|
1266
|
+
const rows = await db.select().from(pwPlans3).where(eq(pwPlans3.planId, planId)).orderBy(sql`${pwPlans3.version} desc`).limit(1);
|
|
1267
|
+
return rows[0] ?? null;
|
|
1268
|
+
}
|
|
1269
|
+
async function plansListActive3(db) {
|
|
1270
|
+
const latest = db.select({
|
|
1271
|
+
planId: pwPlans3.planId,
|
|
1272
|
+
version: sql`max(${pwPlans3.version})`.as("max_version")
|
|
1273
|
+
}).from(pwPlans3).groupBy(pwPlans3.planId).as("latest");
|
|
1274
|
+
const rows = await db.select({
|
|
1275
|
+
id: pwPlans3.id,
|
|
1276
|
+
planId: pwPlans3.planId,
|
|
1277
|
+
version: pwPlans3.version,
|
|
1278
|
+
group: pwPlans3.group,
|
|
1279
|
+
isDefault: pwPlans3.isDefault,
|
|
1280
|
+
name: pwPlans3.name,
|
|
1281
|
+
priceMinor: pwPlans3.priceMinor,
|
|
1282
|
+
priceCurrency: pwPlans3.priceCurrency,
|
|
1283
|
+
priceInterval: pwPlans3.priceInterval,
|
|
1284
|
+
features: pwPlans3.features,
|
|
1285
|
+
providerRefs: pwPlans3.providerRefs,
|
|
1286
|
+
pushedAt: pwPlans3.pushedAt
|
|
1287
|
+
}).from(pwPlans3).innerJoin(
|
|
1288
|
+
latest,
|
|
1289
|
+
and(eq(latest.planId, pwPlans3.planId), eq(latest.version, pwPlans3.version))
|
|
1290
|
+
);
|
|
1291
|
+
return rows;
|
|
1292
|
+
}
|
|
1293
|
+
async function plansPushVersion3(db, plan) {
|
|
1294
|
+
const activeRows = await db.select().from(pwPlans3).where(eq(pwPlans3.planId, plan.planId)).orderBy(sql`${pwPlans3.version} desc`).limit(1);
|
|
1295
|
+
const active = activeRows[0] ?? null;
|
|
1296
|
+
if (active && planContentEquals(active, plan)) {
|
|
1297
|
+
return active;
|
|
1298
|
+
}
|
|
1299
|
+
const now = /* @__PURE__ */ new Date();
|
|
1300
|
+
const id = generatePwId(now.getTime());
|
|
1301
|
+
const nextVersion = (active?.version ?? 0) + 1;
|
|
1302
|
+
const rows = await db.insert(pwPlans3).values({
|
|
1303
|
+
id,
|
|
1304
|
+
planId: plan.planId,
|
|
1305
|
+
version: nextVersion,
|
|
1306
|
+
group: plan.group,
|
|
1307
|
+
isDefault: plan.isDefault,
|
|
1308
|
+
name: plan.name,
|
|
1309
|
+
priceMinor: plan.priceMinor,
|
|
1310
|
+
priceCurrency: plan.priceCurrency,
|
|
1311
|
+
priceInterval: plan.priceInterval,
|
|
1312
|
+
features: plan.features,
|
|
1313
|
+
providerRefs: plan.providerRefs,
|
|
1314
|
+
pushedAt: now
|
|
1315
|
+
}).returning();
|
|
1316
|
+
const row = rows[0];
|
|
1317
|
+
if (!row) throw new Error("drizzle adapter (sqlite): plans.pushVersion returned no row");
|
|
1318
|
+
return row;
|
|
1319
|
+
}
|
|
1320
|
+
async function subscriptionsGetActive3(db, customerId, group) {
|
|
1321
|
+
const rows = await db.select().from(pwSubscriptions3).where(
|
|
1322
|
+
and(
|
|
1323
|
+
eq(pwSubscriptions3.customerId, customerId),
|
|
1324
|
+
eq(pwSubscriptions3.group, group),
|
|
1325
|
+
sql`${pwSubscriptions3.status} in ${PW_ACTIVE_SUBSCRIPTION_STATUSES}`
|
|
1326
|
+
)
|
|
1327
|
+
);
|
|
1328
|
+
return rows[0] ?? null;
|
|
1329
|
+
}
|
|
1330
|
+
async function subscriptionsCreate3(db, input) {
|
|
1331
|
+
const now = /* @__PURE__ */ new Date();
|
|
1332
|
+
const id = generatePwId(now.getTime());
|
|
1333
|
+
try {
|
|
1334
|
+
const rows = await db.insert(pwSubscriptions3).values({
|
|
1335
|
+
id,
|
|
1336
|
+
customerId: input.customerId,
|
|
1337
|
+
planId: input.planId,
|
|
1338
|
+
planVersion: input.planVersion,
|
|
1339
|
+
group: input.group,
|
|
1340
|
+
status: input.status,
|
|
1341
|
+
provider: input.provider,
|
|
1342
|
+
providerSubscriptionRef: input.providerSubscriptionRef,
|
|
1343
|
+
currentPeriodStart: input.currentPeriodStart,
|
|
1344
|
+
currentPeriodEnd: input.currentPeriodEnd,
|
|
1345
|
+
cancelAtPeriodEnd: input.cancelAtPeriodEnd,
|
|
1346
|
+
createdAt: now,
|
|
1347
|
+
updatedAt: now
|
|
1348
|
+
}).returning();
|
|
1349
|
+
const row = rows[0];
|
|
1350
|
+
if (!row) throw new Error("drizzle adapter (sqlite): subscriptions.create returned no row");
|
|
1351
|
+
return row;
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
throw wrapDriverError(
|
|
1354
|
+
error,
|
|
1355
|
+
`payweave/db/drizzle (sqlite): 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).`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
async function subscriptionsUpdate3(db, id, patch) {
|
|
1360
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
1361
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1362
|
+
if (value !== void 0) set[key] = value;
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
const rows = await db.update(pwSubscriptions3).set(set).where(eq(pwSubscriptions3.id, id)).returning();
|
|
1366
|
+
const row = rows[0];
|
|
1367
|
+
if (!row) {
|
|
1368
|
+
throw new PayweaveNotFoundError(
|
|
1369
|
+
`payweave/db/drizzle (sqlite): subscriptions.update: no subscription with id ${JSON.stringify(id)}.`
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
return row;
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (error instanceof PayweaveNotFoundError) throw error;
|
|
1375
|
+
throw wrapDriverError(
|
|
1376
|
+
error,
|
|
1377
|
+
`payweave/db/drizzle (sqlite): subscriptions.update(${JSON.stringify(id)}) failed.`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
async function balancesGet3(db, customerId, featureId, group) {
|
|
1382
|
+
const rows = await db.select().from(pwFeatureBalances3).where(
|
|
1383
|
+
and(
|
|
1384
|
+
eq(pwFeatureBalances3.customerId, customerId),
|
|
1385
|
+
eq(pwFeatureBalances3.featureId, featureId),
|
|
1386
|
+
eq(pwFeatureBalances3.group, group)
|
|
1387
|
+
)
|
|
1388
|
+
);
|
|
1389
|
+
return rows[0] ?? null;
|
|
1390
|
+
}
|
|
1391
|
+
async function balancesConsume3(db, input) {
|
|
1392
|
+
const existingRows = await db.select().from(pwFeatureBalances3).where(
|
|
1393
|
+
and(
|
|
1394
|
+
eq(pwFeatureBalances3.customerId, input.customerId),
|
|
1395
|
+
eq(pwFeatureBalances3.featureId, input.featureId),
|
|
1396
|
+
eq(pwFeatureBalances3.group, input.group)
|
|
1397
|
+
)
|
|
1398
|
+
);
|
|
1399
|
+
const existingRow = existingRows[0];
|
|
1400
|
+
const nowMs = input.now.getTime();
|
|
1401
|
+
if (!existingRow) {
|
|
1402
|
+
const period2 = currentPeriod(input.init.anchor.getTime(), input.init.resetInterval, nowMs);
|
|
1403
|
+
const remaining2 = input.init.limit;
|
|
1404
|
+
const applied2 = input.conditional !== true || remaining2 >= input.amount;
|
|
1405
|
+
const used = applied2 ? input.amount : 0;
|
|
1406
|
+
const id = generatePwId(nowMs);
|
|
1407
|
+
const rows2 = await db.insert(pwFeatureBalances3).values({
|
|
1408
|
+
id,
|
|
1409
|
+
customerId: input.customerId,
|
|
1410
|
+
featureId: input.featureId,
|
|
1411
|
+
group: input.group,
|
|
1412
|
+
used,
|
|
1413
|
+
limit: input.init.limit,
|
|
1414
|
+
resetInterval: input.init.resetInterval,
|
|
1415
|
+
anchor: input.init.anchor,
|
|
1416
|
+
periodStart: new Date(period2.start),
|
|
1417
|
+
periodEnd: new Date(period2.end),
|
|
1418
|
+
planId: input.init.planId,
|
|
1419
|
+
planVersion: input.init.planVersion,
|
|
1420
|
+
updatedAt: input.now
|
|
1421
|
+
}).returning();
|
|
1422
|
+
const row = rows2[0];
|
|
1423
|
+
if (!row) throw new Error("drizzle adapter (sqlite): balances.consume (create) returned no row");
|
|
1424
|
+
return { ...row, applied: applied2 };
|
|
1425
|
+
}
|
|
1426
|
+
const resetDue = nowMs >= existingRow.periodEnd.getTime();
|
|
1427
|
+
const baseUsed = resetDue ? 0 : existingRow.used;
|
|
1428
|
+
const period = resetDue ? currentPeriod(existingRow.anchor.getTime(), existingRow.resetInterval, nowMs) : { start: existingRow.periodStart.getTime(), end: existingRow.periodEnd.getTime() };
|
|
1429
|
+
const remaining = existingRow.limit - baseUsed;
|
|
1430
|
+
const applied = input.conditional !== true || remaining >= input.amount;
|
|
1431
|
+
if (!applied && !resetDue) {
|
|
1432
|
+
return { ...existingRow, applied: false };
|
|
1433
|
+
}
|
|
1434
|
+
const finalUsed = applied ? baseUsed + input.amount : baseUsed;
|
|
1435
|
+
const rows = await db.update(pwFeatureBalances3).set({
|
|
1436
|
+
used: finalUsed,
|
|
1437
|
+
periodStart: new Date(period.start),
|
|
1438
|
+
periodEnd: new Date(period.end),
|
|
1439
|
+
updatedAt: input.now
|
|
1440
|
+
}).where(eq(pwFeatureBalances3.id, existingRow.id)).returning();
|
|
1441
|
+
const updatedRow = rows[0];
|
|
1442
|
+
if (!updatedRow) throw new Error("drizzle adapter (sqlite): balances.consume (update) returned no row");
|
|
1443
|
+
return { ...updatedRow, applied };
|
|
1444
|
+
}
|
|
1445
|
+
async function balancesResetTo3(db, customerId, featureId, group, init) {
|
|
1446
|
+
const now = /* @__PURE__ */ new Date();
|
|
1447
|
+
const period = currentPeriod(init.anchor.getTime(), init.resetInterval, now.getTime());
|
|
1448
|
+
const id = generatePwId(now.getTime());
|
|
1449
|
+
await db.insert(pwFeatureBalances3).values({
|
|
1450
|
+
id,
|
|
1451
|
+
customerId,
|
|
1452
|
+
featureId,
|
|
1453
|
+
group,
|
|
1454
|
+
used: 0,
|
|
1455
|
+
limit: init.limit,
|
|
1456
|
+
resetInterval: init.resetInterval,
|
|
1457
|
+
anchor: init.anchor,
|
|
1458
|
+
periodStart: new Date(period.start),
|
|
1459
|
+
periodEnd: new Date(period.end),
|
|
1460
|
+
planId: init.planId,
|
|
1461
|
+
planVersion: init.planVersion,
|
|
1462
|
+
updatedAt: now
|
|
1463
|
+
}).onConflictDoUpdate({
|
|
1464
|
+
target: [pwFeatureBalances3.customerId, pwFeatureBalances3.featureId, pwFeatureBalances3.group],
|
|
1465
|
+
set: {
|
|
1466
|
+
used: 0,
|
|
1467
|
+
limit: init.limit,
|
|
1468
|
+
resetInterval: init.resetInterval,
|
|
1469
|
+
anchor: init.anchor,
|
|
1470
|
+
periodStart: new Date(period.start),
|
|
1471
|
+
periodEnd: new Date(period.end),
|
|
1472
|
+
planId: init.planId,
|
|
1473
|
+
planVersion: init.planVersion,
|
|
1474
|
+
updatedAt: now
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
async function webhookEventsClaim3(db, dedupeKey, meta) {
|
|
1479
|
+
const staleClaimAfterMs = meta.staleClaimAfterMs ?? DEFAULT_STALE_CLAIM_AFTER_MS;
|
|
1480
|
+
const staleThreshold = new Date(meta.now.getTime() - staleClaimAfterMs);
|
|
1481
|
+
const rows = await db.insert(pwWebhookEvents3).values({
|
|
1482
|
+
dedupeKey,
|
|
1483
|
+
provider: meta.provider,
|
|
1484
|
+
type: meta.type,
|
|
1485
|
+
receivedAt: meta.now,
|
|
1486
|
+
claimedAt: meta.now,
|
|
1487
|
+
appliedAt: null
|
|
1488
|
+
}).onConflictDoUpdate({
|
|
1489
|
+
target: pwWebhookEvents3.dedupeKey,
|
|
1490
|
+
set: { claimedAt: meta.now },
|
|
1491
|
+
// `and(...)` only returns `undefined` when given zero conditions —
|
|
1492
|
+
// these two are always present, so the non-null assertion is safe
|
|
1493
|
+
// (satisfies `exactOptionalPropertyTypes`, which forbids `| undefined`
|
|
1494
|
+
// on `setWhere` even though `and()`'s own return type allows it).
|
|
1495
|
+
setWhere: and(
|
|
1496
|
+
sql`${pwWebhookEvents3.appliedAt} is null`,
|
|
1497
|
+
sql`${pwWebhookEvents3.claimedAt} <= ${staleThreshold}`
|
|
1498
|
+
)
|
|
1499
|
+
}).returning({ claimedAt: pwWebhookEvents3.claimedAt });
|
|
1500
|
+
const row = rows[0];
|
|
1501
|
+
return row !== void 0 && row.claimedAt !== null && row.claimedAt.getTime() === meta.now.getTime();
|
|
1502
|
+
}
|
|
1503
|
+
async function webhookEventsMarkApplied3(db, dedupeKey) {
|
|
1504
|
+
await db.update(pwWebhookEvents3).set({ appliedAt: /* @__PURE__ */ new Date() }).where(eq(pwWebhookEvents3.dedupeKey, dedupeKey));
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/db/drizzle/index.ts
|
|
1508
|
+
function looksLikeDrizzleDb(value) {
|
|
1509
|
+
if (value === null || typeof value !== "object") return false;
|
|
1510
|
+
const candidate = value;
|
|
1511
|
+
return typeof candidate.select === "function" && typeof candidate.insert === "function" && typeof candidate.update === "function" && typeof candidate.transaction === "function";
|
|
1512
|
+
}
|
|
1513
|
+
function drizzleAdapter(db, options = {}) {
|
|
1514
|
+
if (!looksLikeDrizzleDb(db)) {
|
|
1515
|
+
throw new PayweaveConfigError(
|
|
1516
|
+
`payweave/db/drizzle: drizzleAdapter(db) expects your own \`drizzle-orm\` database instance (the object returned by e.g. \`drizzle(pool)\`/\`drizzle(client)\`) \u2014 got ${typeof db === "object" && db !== null ? "an object missing select/insert/update/transaction" : typeof db}.`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
const dialect = detectDrizzleDialect(db, options.dialect);
|
|
1520
|
+
switch (dialect) {
|
|
1521
|
+
case "sqlite":
|
|
1522
|
+
return buildSqliteAdapter(db);
|
|
1523
|
+
case "postgres":
|
|
1524
|
+
return buildPostgresAdapter(db);
|
|
1525
|
+
case "mysql":
|
|
1526
|
+
return buildMysqlAdapter(db);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
export { drizzleAdapter, mysqlSchema, pgSchema, sqliteSchema };
|
|
1531
|
+
//# sourceMappingURL=index.js.map
|
|
1532
|
+
//# sourceMappingURL=index.js.map
|