@murai-wallet/storage-drizzle 1.0.2
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 +37 -0
- package/dist/index.cjs +269 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +267 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 murai contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @murai-wallet/storage-drizzle
|
|
2
|
+
|
|
3
|
+
Drizzle ORM storage adapter for [Murai](https://github.com/ebuntario/murai) — PostgreSQL with row-level locking.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @murai-wallet/storage-drizzle @murai-wallet/core drizzle-orm postgres
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import postgres from 'postgres';
|
|
15
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
16
|
+
import { createDrizzleStorage } from '@murai-wallet/storage-drizzle';
|
|
17
|
+
|
|
18
|
+
const sql = postgres(process.env.DATABASE_URL);
|
|
19
|
+
const storage = createDrizzleStorage(drizzle(sql));
|
|
20
|
+
|
|
21
|
+
// Use with createWallet from @murai-wallet/core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- PostgreSQL via `drizzle-orm/postgres-js`
|
|
27
|
+
- `SELECT FOR UPDATE` row-level locking for atomic balance operations
|
|
28
|
+
- BIGINT storage for precise monetary amounts
|
|
29
|
+
- Paginated transaction and checkout queries with filtering
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
Full docs: [ebuntario.github.io/murai](https://ebuntario.github.io/murai)
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
[MIT](../../LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var core = require('@murai-wallet/core');
|
|
5
|
+
var drizzleOrm = require('drizzle-orm');
|
|
6
|
+
var pgCore = require('drizzle-orm/pg-core');
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
var wallets = pgCore.pgTable("wallets", {
|
|
10
|
+
userId: pgCore.text("user_id").primaryKey(),
|
|
11
|
+
balance: pgCore.bigint("balance", { mode: "number" }).notNull().default(0)
|
|
12
|
+
});
|
|
13
|
+
var transactions = pgCore.pgTable("transactions", {
|
|
14
|
+
id: pgCore.uuid("id").primaryKey(),
|
|
15
|
+
userId: pgCore.text("user_id").notNull(),
|
|
16
|
+
amount: pgCore.bigint("amount", { mode: "number" }).notNull(),
|
|
17
|
+
idempotencyKey: pgCore.text("idempotency_key").notNull().unique(),
|
|
18
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).notNull(),
|
|
19
|
+
expiresAt: pgCore.timestamp("expires_at", { withTimezone: true }),
|
|
20
|
+
remaining: pgCore.bigint("remaining", { mode: "number" }),
|
|
21
|
+
expiredAt: pgCore.timestamp("expired_at", { withTimezone: true }),
|
|
22
|
+
metadata: pgCore.text("metadata")
|
|
23
|
+
});
|
|
24
|
+
var checkouts = pgCore.pgTable("checkouts", {
|
|
25
|
+
id: pgCore.text("id").primaryKey(),
|
|
26
|
+
userId: pgCore.text("user_id").notNull(),
|
|
27
|
+
amount: pgCore.bigint("amount", { mode: "number" }).notNull(),
|
|
28
|
+
redirectUrl: pgCore.text("redirect_url").notNull(),
|
|
29
|
+
status: pgCore.text("status").notNull(),
|
|
30
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).notNull(),
|
|
31
|
+
updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).notNull()
|
|
32
|
+
});
|
|
33
|
+
var PG_UNIQUE_VIOLATION = "23505";
|
|
34
|
+
function isUniqueViolation(error) {
|
|
35
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === PG_UNIQUE_VIOLATION;
|
|
36
|
+
}
|
|
37
|
+
function toLedgerEntry(row) {
|
|
38
|
+
return {
|
|
39
|
+
id: row.id,
|
|
40
|
+
userId: row.userId,
|
|
41
|
+
amount: row.amount,
|
|
42
|
+
idempotencyKey: row.idempotencyKey,
|
|
43
|
+
createdAt: row.createdAt,
|
|
44
|
+
expiresAt: row.expiresAt,
|
|
45
|
+
remaining: row.remaining,
|
|
46
|
+
expiredAt: row.expiredAt,
|
|
47
|
+
metadata: row.metadata
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createDrizzleStorage(db) {
|
|
51
|
+
async function getBalance(userId) {
|
|
52
|
+
const result = await db.select({ balance: wallets.balance }).from(wallets).where(drizzleOrm.eq(wallets.userId, userId));
|
|
53
|
+
return result[0]?.balance ?? 0;
|
|
54
|
+
}
|
|
55
|
+
async function appendEntry(entry) {
|
|
56
|
+
try {
|
|
57
|
+
return await db.transaction(async (tx) => {
|
|
58
|
+
await tx.insert(wallets).values({ userId: entry.userId, balance: 0 }).onConflictDoNothing();
|
|
59
|
+
const walletRows = await tx.select().from(wallets).where(drizzleOrm.eq(wallets.userId, entry.userId)).for("update");
|
|
60
|
+
const wallet = walletRows[0];
|
|
61
|
+
if (!wallet) {
|
|
62
|
+
throw new Error(`Wallet row missing for userId: ${entry.userId}`);
|
|
63
|
+
}
|
|
64
|
+
if (entry.amount < 0) {
|
|
65
|
+
const debitAmount = Math.abs(entry.amount);
|
|
66
|
+
const buckets = await tx.select().from(transactions).where(
|
|
67
|
+
drizzleOrm.and(
|
|
68
|
+
drizzleOrm.eq(transactions.userId, entry.userId),
|
|
69
|
+
drizzleOrm.gt(transactions.amount, 0),
|
|
70
|
+
drizzleOrm.gt(transactions.remaining, 0),
|
|
71
|
+
drizzleOrm.or(drizzleOrm.isNull(transactions.expiresAt), drizzleOrm.gt(transactions.expiresAt, /* @__PURE__ */ new Date()))
|
|
72
|
+
)
|
|
73
|
+
).orderBy(
|
|
74
|
+
// NULLS LAST: entries with expiresAt first, then by createdAt
|
|
75
|
+
drizzleOrm.sql`${transactions.expiresAt} ASC NULLS LAST`,
|
|
76
|
+
drizzleOrm.asc(transactions.createdAt)
|
|
77
|
+
).for("update");
|
|
78
|
+
if (buckets.length > 0) {
|
|
79
|
+
let spendableBalance = 0;
|
|
80
|
+
for (const b of buckets) {
|
|
81
|
+
spendableBalance += b.remaining ?? 0;
|
|
82
|
+
}
|
|
83
|
+
if (spendableBalance < debitAmount) {
|
|
84
|
+
throw new core.InsufficientBalanceError(entry.userId, debitAmount, spendableBalance);
|
|
85
|
+
}
|
|
86
|
+
let remaining = debitAmount;
|
|
87
|
+
for (const bucket of buckets) {
|
|
88
|
+
if (remaining <= 0) break;
|
|
89
|
+
const bucketRemaining = bucket.remaining ?? 0;
|
|
90
|
+
const consume = Math.min(bucketRemaining, remaining);
|
|
91
|
+
await tx.update(transactions).set({ remaining: bucketRemaining - consume }).where(drizzleOrm.eq(transactions.id, bucket.id));
|
|
92
|
+
remaining -= consume;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
if (wallet.balance < debitAmount) {
|
|
96
|
+
throw new core.InsufficientBalanceError(entry.userId, debitAmount, wallet.balance);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await tx.update(wallets).set({ balance: wallet.balance + entry.amount }).where(drizzleOrm.eq(wallets.userId, entry.userId));
|
|
101
|
+
const [inserted] = await tx.insert(transactions).values({
|
|
102
|
+
id: crypto.randomUUID(),
|
|
103
|
+
userId: entry.userId,
|
|
104
|
+
amount: entry.amount,
|
|
105
|
+
idempotencyKey: entry.idempotencyKey,
|
|
106
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
107
|
+
expiresAt: entry.expiresAt ?? null,
|
|
108
|
+
remaining: entry.remaining ?? null,
|
|
109
|
+
expiredAt: entry.expiredAt ?? null,
|
|
110
|
+
metadata: entry.metadata ?? null
|
|
111
|
+
}).returning();
|
|
112
|
+
if (!inserted) {
|
|
113
|
+
throw new Error("Failed to insert ledger entry");
|
|
114
|
+
}
|
|
115
|
+
return toLedgerEntry(inserted);
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (isUniqueViolation(error)) {
|
|
119
|
+
throw new core.IdempotencyConflictError(entry.idempotencyKey);
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function findEntry(idempotencyKey) {
|
|
125
|
+
const result = await db.select().from(transactions).where(drizzleOrm.eq(transactions.idempotencyKey, idempotencyKey));
|
|
126
|
+
const row = result[0];
|
|
127
|
+
if (!row) return null;
|
|
128
|
+
return toLedgerEntry(row);
|
|
129
|
+
}
|
|
130
|
+
async function saveCheckout(session) {
|
|
131
|
+
const [inserted] = await db.insert(checkouts).values({
|
|
132
|
+
id: session.id,
|
|
133
|
+
userId: session.userId,
|
|
134
|
+
amount: session.amount,
|
|
135
|
+
redirectUrl: session.redirectUrl,
|
|
136
|
+
status: session.status,
|
|
137
|
+
createdAt: session.createdAt,
|
|
138
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
139
|
+
}).returning();
|
|
140
|
+
if (!inserted) {
|
|
141
|
+
throw new Error("Failed to insert checkout session");
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
id: inserted.id,
|
|
145
|
+
userId: inserted.userId,
|
|
146
|
+
amount: inserted.amount,
|
|
147
|
+
redirectUrl: inserted.redirectUrl,
|
|
148
|
+
status: inserted.status,
|
|
149
|
+
createdAt: inserted.createdAt
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function findCheckout(id) {
|
|
153
|
+
const result = await db.select().from(checkouts).where(drizzleOrm.eq(checkouts.id, id));
|
|
154
|
+
const row = result[0];
|
|
155
|
+
if (!row) return null;
|
|
156
|
+
return {
|
|
157
|
+
id: row.id,
|
|
158
|
+
userId: row.userId,
|
|
159
|
+
amount: row.amount,
|
|
160
|
+
redirectUrl: row.redirectUrl,
|
|
161
|
+
status: row.status,
|
|
162
|
+
createdAt: row.createdAt
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function updateCheckoutStatus(id, status) {
|
|
166
|
+
await db.update(checkouts).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(drizzleOrm.eq(checkouts.id, id));
|
|
167
|
+
}
|
|
168
|
+
async function getTransactions(userId, query) {
|
|
169
|
+
const limit = query?.limit ?? 50;
|
|
170
|
+
const offset = query?.offset ?? 0;
|
|
171
|
+
const conditions = [drizzleOrm.eq(transactions.userId, userId)];
|
|
172
|
+
if (query?.type === "credit") {
|
|
173
|
+
conditions.push(drizzleOrm.gt(transactions.amount, 0));
|
|
174
|
+
} else if (query?.type === "debit") {
|
|
175
|
+
conditions.push(drizzleOrm.lt(transactions.amount, 0));
|
|
176
|
+
}
|
|
177
|
+
if (query?.from) {
|
|
178
|
+
conditions.push(drizzleOrm.gte(transactions.createdAt, query.from));
|
|
179
|
+
}
|
|
180
|
+
if (query?.to) {
|
|
181
|
+
conditions.push(drizzleOrm.lte(transactions.createdAt, query.to));
|
|
182
|
+
}
|
|
183
|
+
const rows = await db.select().from(transactions).where(drizzleOrm.and(...conditions)).orderBy(drizzleOrm.desc(transactions.createdAt)).limit(limit).offset(offset);
|
|
184
|
+
return rows.map(toLedgerEntry);
|
|
185
|
+
}
|
|
186
|
+
async function getCheckouts(userId, query) {
|
|
187
|
+
const limit = query?.limit ?? 50;
|
|
188
|
+
const offset = query?.offset ?? 0;
|
|
189
|
+
const conditions = [drizzleOrm.eq(checkouts.userId, userId)];
|
|
190
|
+
if (query?.status) {
|
|
191
|
+
conditions.push(drizzleOrm.eq(checkouts.status, query.status));
|
|
192
|
+
}
|
|
193
|
+
const rows = await db.select().from(checkouts).where(drizzleOrm.and(...conditions)).orderBy(drizzleOrm.desc(checkouts.createdAt)).limit(limit).offset(offset);
|
|
194
|
+
return rows.map((row) => ({
|
|
195
|
+
id: row.id,
|
|
196
|
+
userId: row.userId,
|
|
197
|
+
amount: row.amount,
|
|
198
|
+
redirectUrl: row.redirectUrl,
|
|
199
|
+
status: row.status,
|
|
200
|
+
createdAt: row.createdAt
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
async function expireCredits(userId, now) {
|
|
204
|
+
return db.transaction(async (tx) => {
|
|
205
|
+
await tx.select().from(wallets).where(drizzleOrm.eq(wallets.userId, userId)).for("update");
|
|
206
|
+
const expiredBuckets = await tx.select().from(transactions).where(
|
|
207
|
+
drizzleOrm.and(
|
|
208
|
+
drizzleOrm.eq(transactions.userId, userId),
|
|
209
|
+
drizzleOrm.gt(transactions.amount, 0),
|
|
210
|
+
drizzleOrm.gt(transactions.remaining, 0),
|
|
211
|
+
drizzleOrm.isNotNull(transactions.expiresAt),
|
|
212
|
+
drizzleOrm.lt(transactions.expiresAt, now)
|
|
213
|
+
)
|
|
214
|
+
).for("update");
|
|
215
|
+
let expiredCount = 0;
|
|
216
|
+
let expiredAmount = 0;
|
|
217
|
+
for (const bucket of expiredBuckets) {
|
|
218
|
+
const amount = bucket.remaining ?? 0;
|
|
219
|
+
if (amount <= 0) continue;
|
|
220
|
+
expiredAmount += amount;
|
|
221
|
+
expiredCount++;
|
|
222
|
+
await tx.insert(transactions).values({
|
|
223
|
+
id: crypto.randomUUID(),
|
|
224
|
+
userId,
|
|
225
|
+
amount: -amount,
|
|
226
|
+
idempotencyKey: `expire:${bucket.idempotencyKey}`,
|
|
227
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
228
|
+
metadata: null
|
|
229
|
+
});
|
|
230
|
+
await tx.update(transactions).set({ remaining: 0, expiredAt: now }).where(drizzleOrm.eq(transactions.id, bucket.id));
|
|
231
|
+
}
|
|
232
|
+
if (expiredAmount > 0) {
|
|
233
|
+
const walletRows = await tx.select().from(wallets).where(drizzleOrm.eq(wallets.userId, userId));
|
|
234
|
+
const wallet = walletRows[0];
|
|
235
|
+
if (wallet) {
|
|
236
|
+
await tx.update(wallets).set({ balance: wallet.balance - expiredAmount }).where(drizzleOrm.eq(wallets.userId, userId));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { expiredCount, expiredAmount };
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async function getUsersWithExpirableCredits(now) {
|
|
243
|
+
const rows = await db.selectDistinct({ userId: transactions.userId }).from(transactions).where(
|
|
244
|
+
drizzleOrm.and(
|
|
245
|
+
drizzleOrm.gt(transactions.amount, 0),
|
|
246
|
+
drizzleOrm.gt(transactions.remaining, 0),
|
|
247
|
+
drizzleOrm.isNotNull(transactions.expiresAt),
|
|
248
|
+
drizzleOrm.lt(transactions.expiresAt, now)
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
return rows.map((r) => r.userId);
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
getBalance,
|
|
255
|
+
appendEntry,
|
|
256
|
+
findEntry,
|
|
257
|
+
saveCheckout,
|
|
258
|
+
findCheckout,
|
|
259
|
+
updateCheckoutStatus,
|
|
260
|
+
getTransactions,
|
|
261
|
+
getCheckouts,
|
|
262
|
+
expireCredits,
|
|
263
|
+
getUsersWithExpirableCredits
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
exports.createDrizzleStorage = createDrizzleStorage;
|
|
268
|
+
//# sourceMappingURL=index.cjs.map
|
|
269
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["pgTable","text","bigint","uuid","timestamp","eq","and","gt","or","isNull","sql","asc","InsufficientBalanceError","randomUUID","IdempotencyConflictError","lt","gte","lte","desc","isNotNull"],"mappings":"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","file":"index.cjs","sourcesContent":["// @murai-wallet/storage-drizzle\n// Drizzle ORM storage adapter — PostgreSQL (postgres.js or node-postgres)\n// Peer dependency: drizzle-orm >=0.30.0\n\nimport { randomUUID } from 'node:crypto';\nimport { IdempotencyConflictError, InsufficientBalanceError } from '@murai-wallet/core';\nimport type {\n\tCheckoutQuery,\n\tCheckoutSession,\n\tExpireResult,\n\tLedgerEntry,\n\tStorageAdapter,\n\tTransactionQuery,\n} from '@murai-wallet/core';\nimport { and, asc, desc, eq, gt, gte, isNotNull, isNull, lt, lte, or, sql } from 'drizzle-orm';\nimport {\n\ttype PgDatabase,\n\ttype PgQueryResultHKT,\n\tbigint,\n\tpgTable,\n\ttext,\n\ttimestamp,\n\tuuid,\n} from 'drizzle-orm/pg-core';\n\n// ---------------------------------------------------------------------------\n// Schema — BIGINT for money (IDR amounts can exceed INT max ~2.1B)\n// ---------------------------------------------------------------------------\n\nconst wallets = pgTable('wallets', {\n\tuserId: text('user_id').primaryKey(),\n\tbalance: bigint('balance', { mode: 'number' }).notNull().default(0),\n});\n\nconst transactions = pgTable('transactions', {\n\tid: uuid('id').primaryKey(),\n\tuserId: text('user_id').notNull(),\n\tamount: bigint('amount', { mode: 'number' }).notNull(),\n\tidempotencyKey: text('idempotency_key').notNull().unique(),\n\tcreatedAt: timestamp('created_at', { withTimezone: true }).notNull(),\n\texpiresAt: timestamp('expires_at', { withTimezone: true }),\n\tremaining: bigint('remaining', { mode: 'number' }),\n\texpiredAt: timestamp('expired_at', { withTimezone: true }),\n\tmetadata: text('metadata'),\n});\n\nconst checkouts = pgTable('checkouts', {\n\tid: text('id').primaryKey(),\n\tuserId: text('user_id').notNull(),\n\tamount: bigint('amount', { mode: 'number' }).notNull(),\n\tredirectUrl: text('redirect_url').notNull(),\n\tstatus: text('status').notNull(),\n\tcreatedAt: timestamp('created_at', { withTimezone: true }).notNull(),\n\tupdatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),\n});\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** PostgreSQL unique constraint violation error code */\nconst PG_UNIQUE_VIOLATION = '23505';\n\nfunction isUniqueViolation(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' &&\n\t\terror !== null &&\n\t\t'code' in error &&\n\t\t(error as { code: unknown }).code === PG_UNIQUE_VIOLATION\n\t);\n}\n\nfunction toLedgerEntry(row: typeof transactions.$inferSelect): LedgerEntry {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.userId,\n\t\tamount: row.amount,\n\t\tidempotencyKey: row.idempotencyKey,\n\t\tcreatedAt: row.createdAt,\n\t\texpiresAt: row.expiresAt,\n\t\tremaining: row.remaining,\n\t\texpiredAt: row.expiredAt,\n\t\tmetadata: row.metadata,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a StorageAdapter backed by a Drizzle ORM PostgreSQL database.\n * Accepts any Drizzle PG database: PostgresJsDatabase or NodePgDatabase.\n *\n * @example\n * import { drizzle } from 'drizzle-orm/postgres-js';\n * import postgres from 'postgres';\n * const client = postgres(process.env.DATABASE_URL);\n * const db = drizzle(client);\n * const storage = createDrizzleStorage(db);\n */\nexport function createDrizzleStorage<HKT extends PgQueryResultHKT>(\n\tdb: PgDatabase<HKT>,\n): StorageAdapter {\n\tasync function getBalance(userId: string): Promise<number> {\n\t\tconst result = await db\n\t\t\t.select({ balance: wallets.balance })\n\t\t\t.from(wallets)\n\t\t\t.where(eq(wallets.userId, userId));\n\t\treturn result[0]?.balance ?? 0;\n\t}\n\n\tasync function appendEntry(entry: Omit<LedgerEntry, 'id' | 'createdAt'>): Promise<LedgerEntry> {\n\t\ttry {\n\t\t\treturn await db.transaction(async (tx) => {\n\t\t\t\t// 1. Upsert wallet row (safe for concurrent first-writes)\n\t\t\t\tawait tx.insert(wallets).values({ userId: entry.userId, balance: 0 }).onConflictDoNothing();\n\n\t\t\t\t// 2. Lock the wallet row exclusively before reading balance\n\t\t\t\tconst walletRows = await tx\n\t\t\t\t\t.select()\n\t\t\t\t\t.from(wallets)\n\t\t\t\t\t.where(eq(wallets.userId, entry.userId))\n\t\t\t\t\t.for('update');\n\n\t\t\t\tconst wallet = walletRows[0];\n\t\t\t\tif (!wallet) {\n\t\t\t\t\tthrow new Error(`Wallet row missing for userId: ${entry.userId}`);\n\t\t\t\t}\n\n\t\t\t\t// 3. For debits: FIFO bucket consumption\n\t\t\t\tif (entry.amount < 0) {\n\t\t\t\t\tconst debitAmount = Math.abs(entry.amount);\n\n\t\t\t\t\t// Query active credit buckets (not expired, remaining > 0)\n\t\t\t\t\tconst buckets = await tx\n\t\t\t\t\t\t.select()\n\t\t\t\t\t\t.from(transactions)\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\tand(\n\t\t\t\t\t\t\t\teq(transactions.userId, entry.userId),\n\t\t\t\t\t\t\t\tgt(transactions.amount, 0),\n\t\t\t\t\t\t\t\tgt(transactions.remaining, 0),\n\t\t\t\t\t\t\t\tor(isNull(transactions.expiresAt), gt(transactions.expiresAt, new Date())),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.orderBy(\n\t\t\t\t\t\t\t// NULLS LAST: entries with expiresAt first, then by createdAt\n\t\t\t\t\t\t\tsql`${transactions.expiresAt} ASC NULLS LAST`,\n\t\t\t\t\t\t\tasc(transactions.createdAt),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.for('update');\n\n\t\t\t\t\tif (buckets.length > 0) {\n\t\t\t\t\t\t// Calculate spendable balance from buckets\n\t\t\t\t\t\tlet spendableBalance = 0;\n\t\t\t\t\t\tfor (const b of buckets) {\n\t\t\t\t\t\t\tspendableBalance += b.remaining ?? 0;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (spendableBalance < debitAmount) {\n\t\t\t\t\t\t\tthrow new InsufficientBalanceError(entry.userId, debitAmount, spendableBalance);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Consume FIFO\n\t\t\t\t\t\tlet remaining = debitAmount;\n\t\t\t\t\t\tfor (const bucket of buckets) {\n\t\t\t\t\t\t\tif (remaining <= 0) break;\n\t\t\t\t\t\t\tconst bucketRemaining = bucket.remaining ?? 0;\n\t\t\t\t\t\t\tconst consume = Math.min(bucketRemaining, remaining);\n\t\t\t\t\t\t\tawait tx\n\t\t\t\t\t\t\t\t.update(transactions)\n\t\t\t\t\t\t\t\t.set({ remaining: bucketRemaining - consume })\n\t\t\t\t\t\t\t\t.where(eq(transactions.id, bucket.id));\n\t\t\t\t\t\t\tremaining -= consume;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No FIFO buckets — fall back to materialized balance check\n\t\t\t\t\t\tif (wallet.balance < debitAmount) {\n\t\t\t\t\t\t\tthrow new InsufficientBalanceError(entry.userId, debitAmount, wallet.balance);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 4. Update balance atomically\n\t\t\t\tawait tx\n\t\t\t\t\t.update(wallets)\n\t\t\t\t\t.set({ balance: wallet.balance + entry.amount })\n\t\t\t\t\t.where(eq(wallets.userId, entry.userId));\n\n\t\t\t\t// 5. Insert ledger entry — unique constraint enforces idempotency\n\t\t\t\tconst [inserted] = await tx\n\t\t\t\t\t.insert(transactions)\n\t\t\t\t\t.values({\n\t\t\t\t\t\tid: randomUUID(),\n\t\t\t\t\t\tuserId: entry.userId,\n\t\t\t\t\t\tamount: entry.amount,\n\t\t\t\t\t\tidempotencyKey: entry.idempotencyKey,\n\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\texpiresAt: entry.expiresAt ?? null,\n\t\t\t\t\t\tremaining: entry.remaining ?? null,\n\t\t\t\t\t\texpiredAt: entry.expiredAt ?? null,\n\t\t\t\t\t\tmetadata: entry.metadata ?? null,\n\t\t\t\t\t})\n\t\t\t\t\t.returning();\n\n\t\t\t\tif (!inserted) {\n\t\t\t\t\tthrow new Error('Failed to insert ledger entry');\n\t\t\t\t}\n\n\t\t\t\treturn toLedgerEntry(inserted);\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (isUniqueViolation(error)) {\n\t\t\t\tthrow new IdempotencyConflictError(entry.idempotencyKey);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync function findEntry(idempotencyKey: string): Promise<LedgerEntry | null> {\n\t\tconst result = await db\n\t\t\t.select()\n\t\t\t.from(transactions)\n\t\t\t.where(eq(transactions.idempotencyKey, idempotencyKey));\n\t\tconst row = result[0];\n\t\tif (!row) return null;\n\t\treturn toLedgerEntry(row);\n\t}\n\n\tasync function saveCheckout(session: CheckoutSession): Promise<CheckoutSession> {\n\t\tconst [inserted] = await db\n\t\t\t.insert(checkouts)\n\t\t\t.values({\n\t\t\t\tid: session.id,\n\t\t\t\tuserId: session.userId,\n\t\t\t\tamount: session.amount,\n\t\t\t\tredirectUrl: session.redirectUrl,\n\t\t\t\tstatus: session.status,\n\t\t\t\tcreatedAt: session.createdAt,\n\t\t\t\tupdatedAt: new Date(),\n\t\t\t})\n\t\t\t.returning();\n\n\t\tif (!inserted) {\n\t\t\tthrow new Error('Failed to insert checkout session');\n\t\t}\n\n\t\treturn {\n\t\t\tid: inserted.id,\n\t\t\tuserId: inserted.userId,\n\t\t\tamount: inserted.amount,\n\t\t\tredirectUrl: inserted.redirectUrl,\n\t\t\tstatus: inserted.status as CheckoutSession['status'],\n\t\t\tcreatedAt: inserted.createdAt,\n\t\t};\n\t}\n\n\tasync function findCheckout(id: string): Promise<CheckoutSession | null> {\n\t\tconst result = await db.select().from(checkouts).where(eq(checkouts.id, id));\n\t\tconst row = result[0];\n\t\tif (!row) return null;\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tuserId: row.userId,\n\t\t\tamount: row.amount,\n\t\t\tredirectUrl: row.redirectUrl,\n\t\t\tstatus: row.status as CheckoutSession['status'],\n\t\t\tcreatedAt: row.createdAt,\n\t\t};\n\t}\n\n\tasync function updateCheckoutStatus(\n\t\tid: string,\n\t\tstatus: CheckoutSession['status'],\n\t): Promise<void> {\n\t\tawait db.update(checkouts).set({ status, updatedAt: new Date() }).where(eq(checkouts.id, id));\n\t}\n\n\tasync function getTransactions(userId: string, query?: TransactionQuery): Promise<LedgerEntry[]> {\n\t\tconst limit = query?.limit ?? 50;\n\t\tconst offset = query?.offset ?? 0;\n\n\t\tconst conditions = [eq(transactions.userId, userId)];\n\t\tif (query?.type === 'credit') {\n\t\t\tconditions.push(gt(transactions.amount, 0));\n\t\t} else if (query?.type === 'debit') {\n\t\t\tconditions.push(lt(transactions.amount, 0));\n\t\t}\n\t\tif (query?.from) {\n\t\t\tconditions.push(gte(transactions.createdAt, query.from));\n\t\t}\n\t\tif (query?.to) {\n\t\t\tconditions.push(lte(transactions.createdAt, query.to));\n\t\t}\n\n\t\tconst rows = await db\n\t\t\t.select()\n\t\t\t.from(transactions)\n\t\t\t.where(and(...conditions))\n\t\t\t.orderBy(desc(transactions.createdAt))\n\t\t\t.limit(limit)\n\t\t\t.offset(offset);\n\n\t\treturn rows.map(toLedgerEntry);\n\t}\n\n\tasync function getCheckouts(userId: string, query?: CheckoutQuery): Promise<CheckoutSession[]> {\n\t\tconst limit = query?.limit ?? 50;\n\t\tconst offset = query?.offset ?? 0;\n\n\t\tconst conditions = [eq(checkouts.userId, userId)];\n\t\tif (query?.status) {\n\t\t\tconditions.push(eq(checkouts.status, query.status));\n\t\t}\n\n\t\tconst rows = await db\n\t\t\t.select()\n\t\t\t.from(checkouts)\n\t\t\t.where(and(...conditions))\n\t\t\t.orderBy(desc(checkouts.createdAt))\n\t\t\t.limit(limit)\n\t\t\t.offset(offset);\n\n\t\treturn rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tuserId: row.userId,\n\t\t\tamount: row.amount,\n\t\t\tredirectUrl: row.redirectUrl,\n\t\t\tstatus: row.status as CheckoutSession['status'],\n\t\t\tcreatedAt: row.createdAt,\n\t\t}));\n\t}\n\n\tasync function expireCredits(userId: string, now: Date): Promise<ExpireResult> {\n\t\treturn db.transaction(async (tx) => {\n\t\t\t// Lock wallet row\n\t\t\tawait tx.select().from(wallets).where(eq(wallets.userId, userId)).for('update');\n\n\t\t\t// Find expired credit buckets with remaining > 0\n\t\t\tconst expiredBuckets = await tx\n\t\t\t\t.select()\n\t\t\t\t.from(transactions)\n\t\t\t\t.where(\n\t\t\t\t\tand(\n\t\t\t\t\t\teq(transactions.userId, userId),\n\t\t\t\t\t\tgt(transactions.amount, 0),\n\t\t\t\t\t\tgt(transactions.remaining, 0),\n\t\t\t\t\t\tisNotNull(transactions.expiresAt),\n\t\t\t\t\t\tlt(transactions.expiresAt, now),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\t.for('update');\n\n\t\t\tlet expiredCount = 0;\n\t\t\tlet expiredAmount = 0;\n\n\t\t\tfor (const bucket of expiredBuckets) {\n\t\t\t\tconst amount = bucket.remaining ?? 0;\n\t\t\t\tif (amount <= 0) continue;\n\n\t\t\t\texpiredAmount += amount;\n\t\t\t\texpiredCount++;\n\n\t\t\t\t// Insert debit entry for expiration\n\t\t\t\tawait tx.insert(transactions).values({\n\t\t\t\t\tid: randomUUID(),\n\t\t\t\t\tuserId,\n\t\t\t\t\tamount: -amount,\n\t\t\t\t\tidempotencyKey: `expire:${bucket.idempotencyKey}`,\n\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\tmetadata: null,\n\t\t\t\t});\n\n\t\t\t\t// Zero out the bucket and mark expired\n\t\t\t\tawait tx\n\t\t\t\t\t.update(transactions)\n\t\t\t\t\t.set({ remaining: 0, expiredAt: now })\n\t\t\t\t\t.where(eq(transactions.id, bucket.id));\n\t\t\t}\n\n\t\t\t// Update wallet balance\n\t\t\tif (expiredAmount > 0) {\n\t\t\t\tconst walletRows = await tx.select().from(wallets).where(eq(wallets.userId, userId));\n\t\t\t\tconst wallet = walletRows[0];\n\t\t\t\tif (wallet) {\n\t\t\t\t\tawait tx\n\t\t\t\t\t\t.update(wallets)\n\t\t\t\t\t\t.set({ balance: wallet.balance - expiredAmount })\n\t\t\t\t\t\t.where(eq(wallets.userId, userId));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { expiredCount, expiredAmount };\n\t\t});\n\t}\n\n\tasync function getUsersWithExpirableCredits(now: Date): Promise<string[]> {\n\t\tconst rows = await db\n\t\t\t.selectDistinct({ userId: transactions.userId })\n\t\t\t.from(transactions)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\tgt(transactions.amount, 0),\n\t\t\t\t\tgt(transactions.remaining, 0),\n\t\t\t\t\tisNotNull(transactions.expiresAt),\n\t\t\t\t\tlt(transactions.expiresAt, now),\n\t\t\t\t),\n\t\t\t);\n\t\treturn rows.map((r) => r.userId);\n\t}\n\n\treturn {\n\t\tgetBalance,\n\t\tappendEntry,\n\t\tfindEntry,\n\t\tsaveCheckout,\n\t\tfindCheckout,\n\t\tupdateCheckoutStatus,\n\t\tgetTransactions,\n\t\tgetCheckouts,\n\t\texpireCredits,\n\t\tgetUsersWithExpirableCredits,\n\t};\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StorageAdapter } from '@murai-wallet/core';
|
|
2
|
+
import { PgQueryResultHKT, PgDatabase } from 'drizzle-orm/pg-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a StorageAdapter backed by a Drizzle ORM PostgreSQL database.
|
|
6
|
+
* Accepts any Drizzle PG database: PostgresJsDatabase or NodePgDatabase.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { drizzle } from 'drizzle-orm/postgres-js';
|
|
10
|
+
* import postgres from 'postgres';
|
|
11
|
+
* const client = postgres(process.env.DATABASE_URL);
|
|
12
|
+
* const db = drizzle(client);
|
|
13
|
+
* const storage = createDrizzleStorage(db);
|
|
14
|
+
*/
|
|
15
|
+
declare function createDrizzleStorage<HKT extends PgQueryResultHKT>(db: PgDatabase<HKT>): StorageAdapter;
|
|
16
|
+
|
|
17
|
+
export { createDrizzleStorage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StorageAdapter } from '@murai-wallet/core';
|
|
2
|
+
import { PgQueryResultHKT, PgDatabase } from 'drizzle-orm/pg-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a StorageAdapter backed by a Drizzle ORM PostgreSQL database.
|
|
6
|
+
* Accepts any Drizzle PG database: PostgresJsDatabase or NodePgDatabase.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { drizzle } from 'drizzle-orm/postgres-js';
|
|
10
|
+
* import postgres from 'postgres';
|
|
11
|
+
* const client = postgres(process.env.DATABASE_URL);
|
|
12
|
+
* const db = drizzle(client);
|
|
13
|
+
* const storage = createDrizzleStorage(db);
|
|
14
|
+
*/
|
|
15
|
+
declare function createDrizzleStorage<HKT extends PgQueryResultHKT>(db: PgDatabase<HKT>): StorageAdapter;
|
|
16
|
+
|
|
17
|
+
export { createDrizzleStorage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { InsufficientBalanceError, IdempotencyConflictError } from '@murai-wallet/core';
|
|
3
|
+
import { eq, and, gt, or, isNull, sql, asc, lt, gte, lte, desc, isNotNull } from 'drizzle-orm';
|
|
4
|
+
import { pgTable, bigint, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
var wallets = pgTable("wallets", {
|
|
8
|
+
userId: text("user_id").primaryKey(),
|
|
9
|
+
balance: bigint("balance", { mode: "number" }).notNull().default(0)
|
|
10
|
+
});
|
|
11
|
+
var transactions = pgTable("transactions", {
|
|
12
|
+
id: uuid("id").primaryKey(),
|
|
13
|
+
userId: text("user_id").notNull(),
|
|
14
|
+
amount: bigint("amount", { mode: "number" }).notNull(),
|
|
15
|
+
idempotencyKey: text("idempotency_key").notNull().unique(),
|
|
16
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
|
17
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
18
|
+
remaining: bigint("remaining", { mode: "number" }),
|
|
19
|
+
expiredAt: timestamp("expired_at", { withTimezone: true }),
|
|
20
|
+
metadata: text("metadata")
|
|
21
|
+
});
|
|
22
|
+
var checkouts = pgTable("checkouts", {
|
|
23
|
+
id: text("id").primaryKey(),
|
|
24
|
+
userId: text("user_id").notNull(),
|
|
25
|
+
amount: bigint("amount", { mode: "number" }).notNull(),
|
|
26
|
+
redirectUrl: text("redirect_url").notNull(),
|
|
27
|
+
status: text("status").notNull(),
|
|
28
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
|
29
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
|
|
30
|
+
});
|
|
31
|
+
var PG_UNIQUE_VIOLATION = "23505";
|
|
32
|
+
function isUniqueViolation(error) {
|
|
33
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === PG_UNIQUE_VIOLATION;
|
|
34
|
+
}
|
|
35
|
+
function toLedgerEntry(row) {
|
|
36
|
+
return {
|
|
37
|
+
id: row.id,
|
|
38
|
+
userId: row.userId,
|
|
39
|
+
amount: row.amount,
|
|
40
|
+
idempotencyKey: row.idempotencyKey,
|
|
41
|
+
createdAt: row.createdAt,
|
|
42
|
+
expiresAt: row.expiresAt,
|
|
43
|
+
remaining: row.remaining,
|
|
44
|
+
expiredAt: row.expiredAt,
|
|
45
|
+
metadata: row.metadata
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createDrizzleStorage(db) {
|
|
49
|
+
async function getBalance(userId) {
|
|
50
|
+
const result = await db.select({ balance: wallets.balance }).from(wallets).where(eq(wallets.userId, userId));
|
|
51
|
+
return result[0]?.balance ?? 0;
|
|
52
|
+
}
|
|
53
|
+
async function appendEntry(entry) {
|
|
54
|
+
try {
|
|
55
|
+
return await db.transaction(async (tx) => {
|
|
56
|
+
await tx.insert(wallets).values({ userId: entry.userId, balance: 0 }).onConflictDoNothing();
|
|
57
|
+
const walletRows = await tx.select().from(wallets).where(eq(wallets.userId, entry.userId)).for("update");
|
|
58
|
+
const wallet = walletRows[0];
|
|
59
|
+
if (!wallet) {
|
|
60
|
+
throw new Error(`Wallet row missing for userId: ${entry.userId}`);
|
|
61
|
+
}
|
|
62
|
+
if (entry.amount < 0) {
|
|
63
|
+
const debitAmount = Math.abs(entry.amount);
|
|
64
|
+
const buckets = await tx.select().from(transactions).where(
|
|
65
|
+
and(
|
|
66
|
+
eq(transactions.userId, entry.userId),
|
|
67
|
+
gt(transactions.amount, 0),
|
|
68
|
+
gt(transactions.remaining, 0),
|
|
69
|
+
or(isNull(transactions.expiresAt), gt(transactions.expiresAt, /* @__PURE__ */ new Date()))
|
|
70
|
+
)
|
|
71
|
+
).orderBy(
|
|
72
|
+
// NULLS LAST: entries with expiresAt first, then by createdAt
|
|
73
|
+
sql`${transactions.expiresAt} ASC NULLS LAST`,
|
|
74
|
+
asc(transactions.createdAt)
|
|
75
|
+
).for("update");
|
|
76
|
+
if (buckets.length > 0) {
|
|
77
|
+
let spendableBalance = 0;
|
|
78
|
+
for (const b of buckets) {
|
|
79
|
+
spendableBalance += b.remaining ?? 0;
|
|
80
|
+
}
|
|
81
|
+
if (spendableBalance < debitAmount) {
|
|
82
|
+
throw new InsufficientBalanceError(entry.userId, debitAmount, spendableBalance);
|
|
83
|
+
}
|
|
84
|
+
let remaining = debitAmount;
|
|
85
|
+
for (const bucket of buckets) {
|
|
86
|
+
if (remaining <= 0) break;
|
|
87
|
+
const bucketRemaining = bucket.remaining ?? 0;
|
|
88
|
+
const consume = Math.min(bucketRemaining, remaining);
|
|
89
|
+
await tx.update(transactions).set({ remaining: bucketRemaining - consume }).where(eq(transactions.id, bucket.id));
|
|
90
|
+
remaining -= consume;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
if (wallet.balance < debitAmount) {
|
|
94
|
+
throw new InsufficientBalanceError(entry.userId, debitAmount, wallet.balance);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await tx.update(wallets).set({ balance: wallet.balance + entry.amount }).where(eq(wallets.userId, entry.userId));
|
|
99
|
+
const [inserted] = await tx.insert(transactions).values({
|
|
100
|
+
id: randomUUID(),
|
|
101
|
+
userId: entry.userId,
|
|
102
|
+
amount: entry.amount,
|
|
103
|
+
idempotencyKey: entry.idempotencyKey,
|
|
104
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
105
|
+
expiresAt: entry.expiresAt ?? null,
|
|
106
|
+
remaining: entry.remaining ?? null,
|
|
107
|
+
expiredAt: entry.expiredAt ?? null,
|
|
108
|
+
metadata: entry.metadata ?? null
|
|
109
|
+
}).returning();
|
|
110
|
+
if (!inserted) {
|
|
111
|
+
throw new Error("Failed to insert ledger entry");
|
|
112
|
+
}
|
|
113
|
+
return toLedgerEntry(inserted);
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (isUniqueViolation(error)) {
|
|
117
|
+
throw new IdempotencyConflictError(entry.idempotencyKey);
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function findEntry(idempotencyKey) {
|
|
123
|
+
const result = await db.select().from(transactions).where(eq(transactions.idempotencyKey, idempotencyKey));
|
|
124
|
+
const row = result[0];
|
|
125
|
+
if (!row) return null;
|
|
126
|
+
return toLedgerEntry(row);
|
|
127
|
+
}
|
|
128
|
+
async function saveCheckout(session) {
|
|
129
|
+
const [inserted] = await db.insert(checkouts).values({
|
|
130
|
+
id: session.id,
|
|
131
|
+
userId: session.userId,
|
|
132
|
+
amount: session.amount,
|
|
133
|
+
redirectUrl: session.redirectUrl,
|
|
134
|
+
status: session.status,
|
|
135
|
+
createdAt: session.createdAt,
|
|
136
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
137
|
+
}).returning();
|
|
138
|
+
if (!inserted) {
|
|
139
|
+
throw new Error("Failed to insert checkout session");
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
id: inserted.id,
|
|
143
|
+
userId: inserted.userId,
|
|
144
|
+
amount: inserted.amount,
|
|
145
|
+
redirectUrl: inserted.redirectUrl,
|
|
146
|
+
status: inserted.status,
|
|
147
|
+
createdAt: inserted.createdAt
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function findCheckout(id) {
|
|
151
|
+
const result = await db.select().from(checkouts).where(eq(checkouts.id, id));
|
|
152
|
+
const row = result[0];
|
|
153
|
+
if (!row) return null;
|
|
154
|
+
return {
|
|
155
|
+
id: row.id,
|
|
156
|
+
userId: row.userId,
|
|
157
|
+
amount: row.amount,
|
|
158
|
+
redirectUrl: row.redirectUrl,
|
|
159
|
+
status: row.status,
|
|
160
|
+
createdAt: row.createdAt
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function updateCheckoutStatus(id, status) {
|
|
164
|
+
await db.update(checkouts).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(checkouts.id, id));
|
|
165
|
+
}
|
|
166
|
+
async function getTransactions(userId, query) {
|
|
167
|
+
const limit = query?.limit ?? 50;
|
|
168
|
+
const offset = query?.offset ?? 0;
|
|
169
|
+
const conditions = [eq(transactions.userId, userId)];
|
|
170
|
+
if (query?.type === "credit") {
|
|
171
|
+
conditions.push(gt(transactions.amount, 0));
|
|
172
|
+
} else if (query?.type === "debit") {
|
|
173
|
+
conditions.push(lt(transactions.amount, 0));
|
|
174
|
+
}
|
|
175
|
+
if (query?.from) {
|
|
176
|
+
conditions.push(gte(transactions.createdAt, query.from));
|
|
177
|
+
}
|
|
178
|
+
if (query?.to) {
|
|
179
|
+
conditions.push(lte(transactions.createdAt, query.to));
|
|
180
|
+
}
|
|
181
|
+
const rows = await db.select().from(transactions).where(and(...conditions)).orderBy(desc(transactions.createdAt)).limit(limit).offset(offset);
|
|
182
|
+
return rows.map(toLedgerEntry);
|
|
183
|
+
}
|
|
184
|
+
async function getCheckouts(userId, query) {
|
|
185
|
+
const limit = query?.limit ?? 50;
|
|
186
|
+
const offset = query?.offset ?? 0;
|
|
187
|
+
const conditions = [eq(checkouts.userId, userId)];
|
|
188
|
+
if (query?.status) {
|
|
189
|
+
conditions.push(eq(checkouts.status, query.status));
|
|
190
|
+
}
|
|
191
|
+
const rows = await db.select().from(checkouts).where(and(...conditions)).orderBy(desc(checkouts.createdAt)).limit(limit).offset(offset);
|
|
192
|
+
return rows.map((row) => ({
|
|
193
|
+
id: row.id,
|
|
194
|
+
userId: row.userId,
|
|
195
|
+
amount: row.amount,
|
|
196
|
+
redirectUrl: row.redirectUrl,
|
|
197
|
+
status: row.status,
|
|
198
|
+
createdAt: row.createdAt
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
async function expireCredits(userId, now) {
|
|
202
|
+
return db.transaction(async (tx) => {
|
|
203
|
+
await tx.select().from(wallets).where(eq(wallets.userId, userId)).for("update");
|
|
204
|
+
const expiredBuckets = await tx.select().from(transactions).where(
|
|
205
|
+
and(
|
|
206
|
+
eq(transactions.userId, userId),
|
|
207
|
+
gt(transactions.amount, 0),
|
|
208
|
+
gt(transactions.remaining, 0),
|
|
209
|
+
isNotNull(transactions.expiresAt),
|
|
210
|
+
lt(transactions.expiresAt, now)
|
|
211
|
+
)
|
|
212
|
+
).for("update");
|
|
213
|
+
let expiredCount = 0;
|
|
214
|
+
let expiredAmount = 0;
|
|
215
|
+
for (const bucket of expiredBuckets) {
|
|
216
|
+
const amount = bucket.remaining ?? 0;
|
|
217
|
+
if (amount <= 0) continue;
|
|
218
|
+
expiredAmount += amount;
|
|
219
|
+
expiredCount++;
|
|
220
|
+
await tx.insert(transactions).values({
|
|
221
|
+
id: randomUUID(),
|
|
222
|
+
userId,
|
|
223
|
+
amount: -amount,
|
|
224
|
+
idempotencyKey: `expire:${bucket.idempotencyKey}`,
|
|
225
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
226
|
+
metadata: null
|
|
227
|
+
});
|
|
228
|
+
await tx.update(transactions).set({ remaining: 0, expiredAt: now }).where(eq(transactions.id, bucket.id));
|
|
229
|
+
}
|
|
230
|
+
if (expiredAmount > 0) {
|
|
231
|
+
const walletRows = await tx.select().from(wallets).where(eq(wallets.userId, userId));
|
|
232
|
+
const wallet = walletRows[0];
|
|
233
|
+
if (wallet) {
|
|
234
|
+
await tx.update(wallets).set({ balance: wallet.balance - expiredAmount }).where(eq(wallets.userId, userId));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { expiredCount, expiredAmount };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function getUsersWithExpirableCredits(now) {
|
|
241
|
+
const rows = await db.selectDistinct({ userId: transactions.userId }).from(transactions).where(
|
|
242
|
+
and(
|
|
243
|
+
gt(transactions.amount, 0),
|
|
244
|
+
gt(transactions.remaining, 0),
|
|
245
|
+
isNotNull(transactions.expiresAt),
|
|
246
|
+
lt(transactions.expiresAt, now)
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
return rows.map((r) => r.userId);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
getBalance,
|
|
253
|
+
appendEntry,
|
|
254
|
+
findEntry,
|
|
255
|
+
saveCheckout,
|
|
256
|
+
findCheckout,
|
|
257
|
+
updateCheckoutStatus,
|
|
258
|
+
getTransactions,
|
|
259
|
+
getCheckouts,
|
|
260
|
+
expireCredits,
|
|
261
|
+
getUsersWithExpirableCredits
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { createDrizzleStorage };
|
|
266
|
+
//# sourceMappingURL=index.js.map
|
|
267
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// @murai-wallet/storage-drizzle\n// Drizzle ORM storage adapter — PostgreSQL (postgres.js or node-postgres)\n// Peer dependency: drizzle-orm >=0.30.0\n\nimport { randomUUID } from 'node:crypto';\nimport { IdempotencyConflictError, InsufficientBalanceError } from '@murai-wallet/core';\nimport type {\n\tCheckoutQuery,\n\tCheckoutSession,\n\tExpireResult,\n\tLedgerEntry,\n\tStorageAdapter,\n\tTransactionQuery,\n} from '@murai-wallet/core';\nimport { and, asc, desc, eq, gt, gte, isNotNull, isNull, lt, lte, or, sql } from 'drizzle-orm';\nimport {\n\ttype PgDatabase,\n\ttype PgQueryResultHKT,\n\tbigint,\n\tpgTable,\n\ttext,\n\ttimestamp,\n\tuuid,\n} from 'drizzle-orm/pg-core';\n\n// ---------------------------------------------------------------------------\n// Schema — BIGINT for money (IDR amounts can exceed INT max ~2.1B)\n// ---------------------------------------------------------------------------\n\nconst wallets = pgTable('wallets', {\n\tuserId: text('user_id').primaryKey(),\n\tbalance: bigint('balance', { mode: 'number' }).notNull().default(0),\n});\n\nconst transactions = pgTable('transactions', {\n\tid: uuid('id').primaryKey(),\n\tuserId: text('user_id').notNull(),\n\tamount: bigint('amount', { mode: 'number' }).notNull(),\n\tidempotencyKey: text('idempotency_key').notNull().unique(),\n\tcreatedAt: timestamp('created_at', { withTimezone: true }).notNull(),\n\texpiresAt: timestamp('expires_at', { withTimezone: true }),\n\tremaining: bigint('remaining', { mode: 'number' }),\n\texpiredAt: timestamp('expired_at', { withTimezone: true }),\n\tmetadata: text('metadata'),\n});\n\nconst checkouts = pgTable('checkouts', {\n\tid: text('id').primaryKey(),\n\tuserId: text('user_id').notNull(),\n\tamount: bigint('amount', { mode: 'number' }).notNull(),\n\tredirectUrl: text('redirect_url').notNull(),\n\tstatus: text('status').notNull(),\n\tcreatedAt: timestamp('created_at', { withTimezone: true }).notNull(),\n\tupdatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),\n});\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** PostgreSQL unique constraint violation error code */\nconst PG_UNIQUE_VIOLATION = '23505';\n\nfunction isUniqueViolation(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' &&\n\t\terror !== null &&\n\t\t'code' in error &&\n\t\t(error as { code: unknown }).code === PG_UNIQUE_VIOLATION\n\t);\n}\n\nfunction toLedgerEntry(row: typeof transactions.$inferSelect): LedgerEntry {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.userId,\n\t\tamount: row.amount,\n\t\tidempotencyKey: row.idempotencyKey,\n\t\tcreatedAt: row.createdAt,\n\t\texpiresAt: row.expiresAt,\n\t\tremaining: row.remaining,\n\t\texpiredAt: row.expiredAt,\n\t\tmetadata: row.metadata,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a StorageAdapter backed by a Drizzle ORM PostgreSQL database.\n * Accepts any Drizzle PG database: PostgresJsDatabase or NodePgDatabase.\n *\n * @example\n * import { drizzle } from 'drizzle-orm/postgres-js';\n * import postgres from 'postgres';\n * const client = postgres(process.env.DATABASE_URL);\n * const db = drizzle(client);\n * const storage = createDrizzleStorage(db);\n */\nexport function createDrizzleStorage<HKT extends PgQueryResultHKT>(\n\tdb: PgDatabase<HKT>,\n): StorageAdapter {\n\tasync function getBalance(userId: string): Promise<number> {\n\t\tconst result = await db\n\t\t\t.select({ balance: wallets.balance })\n\t\t\t.from(wallets)\n\t\t\t.where(eq(wallets.userId, userId));\n\t\treturn result[0]?.balance ?? 0;\n\t}\n\n\tasync function appendEntry(entry: Omit<LedgerEntry, 'id' | 'createdAt'>): Promise<LedgerEntry> {\n\t\ttry {\n\t\t\treturn await db.transaction(async (tx) => {\n\t\t\t\t// 1. Upsert wallet row (safe for concurrent first-writes)\n\t\t\t\tawait tx.insert(wallets).values({ userId: entry.userId, balance: 0 }).onConflictDoNothing();\n\n\t\t\t\t// 2. Lock the wallet row exclusively before reading balance\n\t\t\t\tconst walletRows = await tx\n\t\t\t\t\t.select()\n\t\t\t\t\t.from(wallets)\n\t\t\t\t\t.where(eq(wallets.userId, entry.userId))\n\t\t\t\t\t.for('update');\n\n\t\t\t\tconst wallet = walletRows[0];\n\t\t\t\tif (!wallet) {\n\t\t\t\t\tthrow new Error(`Wallet row missing for userId: ${entry.userId}`);\n\t\t\t\t}\n\n\t\t\t\t// 3. For debits: FIFO bucket consumption\n\t\t\t\tif (entry.amount < 0) {\n\t\t\t\t\tconst debitAmount = Math.abs(entry.amount);\n\n\t\t\t\t\t// Query active credit buckets (not expired, remaining > 0)\n\t\t\t\t\tconst buckets = await tx\n\t\t\t\t\t\t.select()\n\t\t\t\t\t\t.from(transactions)\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\tand(\n\t\t\t\t\t\t\t\teq(transactions.userId, entry.userId),\n\t\t\t\t\t\t\t\tgt(transactions.amount, 0),\n\t\t\t\t\t\t\t\tgt(transactions.remaining, 0),\n\t\t\t\t\t\t\t\tor(isNull(transactions.expiresAt), gt(transactions.expiresAt, new Date())),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.orderBy(\n\t\t\t\t\t\t\t// NULLS LAST: entries with expiresAt first, then by createdAt\n\t\t\t\t\t\t\tsql`${transactions.expiresAt} ASC NULLS LAST`,\n\t\t\t\t\t\t\tasc(transactions.createdAt),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.for('update');\n\n\t\t\t\t\tif (buckets.length > 0) {\n\t\t\t\t\t\t// Calculate spendable balance from buckets\n\t\t\t\t\t\tlet spendableBalance = 0;\n\t\t\t\t\t\tfor (const b of buckets) {\n\t\t\t\t\t\t\tspendableBalance += b.remaining ?? 0;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (spendableBalance < debitAmount) {\n\t\t\t\t\t\t\tthrow new InsufficientBalanceError(entry.userId, debitAmount, spendableBalance);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Consume FIFO\n\t\t\t\t\t\tlet remaining = debitAmount;\n\t\t\t\t\t\tfor (const bucket of buckets) {\n\t\t\t\t\t\t\tif (remaining <= 0) break;\n\t\t\t\t\t\t\tconst bucketRemaining = bucket.remaining ?? 0;\n\t\t\t\t\t\t\tconst consume = Math.min(bucketRemaining, remaining);\n\t\t\t\t\t\t\tawait tx\n\t\t\t\t\t\t\t\t.update(transactions)\n\t\t\t\t\t\t\t\t.set({ remaining: bucketRemaining - consume })\n\t\t\t\t\t\t\t\t.where(eq(transactions.id, bucket.id));\n\t\t\t\t\t\t\tremaining -= consume;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No FIFO buckets — fall back to materialized balance check\n\t\t\t\t\t\tif (wallet.balance < debitAmount) {\n\t\t\t\t\t\t\tthrow new InsufficientBalanceError(entry.userId, debitAmount, wallet.balance);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 4. Update balance atomically\n\t\t\t\tawait tx\n\t\t\t\t\t.update(wallets)\n\t\t\t\t\t.set({ balance: wallet.balance + entry.amount })\n\t\t\t\t\t.where(eq(wallets.userId, entry.userId));\n\n\t\t\t\t// 5. Insert ledger entry — unique constraint enforces idempotency\n\t\t\t\tconst [inserted] = await tx\n\t\t\t\t\t.insert(transactions)\n\t\t\t\t\t.values({\n\t\t\t\t\t\tid: randomUUID(),\n\t\t\t\t\t\tuserId: entry.userId,\n\t\t\t\t\t\tamount: entry.amount,\n\t\t\t\t\t\tidempotencyKey: entry.idempotencyKey,\n\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\texpiresAt: entry.expiresAt ?? null,\n\t\t\t\t\t\tremaining: entry.remaining ?? null,\n\t\t\t\t\t\texpiredAt: entry.expiredAt ?? null,\n\t\t\t\t\t\tmetadata: entry.metadata ?? null,\n\t\t\t\t\t})\n\t\t\t\t\t.returning();\n\n\t\t\t\tif (!inserted) {\n\t\t\t\t\tthrow new Error('Failed to insert ledger entry');\n\t\t\t\t}\n\n\t\t\t\treturn toLedgerEntry(inserted);\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (isUniqueViolation(error)) {\n\t\t\t\tthrow new IdempotencyConflictError(entry.idempotencyKey);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync function findEntry(idempotencyKey: string): Promise<LedgerEntry | null> {\n\t\tconst result = await db\n\t\t\t.select()\n\t\t\t.from(transactions)\n\t\t\t.where(eq(transactions.idempotencyKey, idempotencyKey));\n\t\tconst row = result[0];\n\t\tif (!row) return null;\n\t\treturn toLedgerEntry(row);\n\t}\n\n\tasync function saveCheckout(session: CheckoutSession): Promise<CheckoutSession> {\n\t\tconst [inserted] = await db\n\t\t\t.insert(checkouts)\n\t\t\t.values({\n\t\t\t\tid: session.id,\n\t\t\t\tuserId: session.userId,\n\t\t\t\tamount: session.amount,\n\t\t\t\tredirectUrl: session.redirectUrl,\n\t\t\t\tstatus: session.status,\n\t\t\t\tcreatedAt: session.createdAt,\n\t\t\t\tupdatedAt: new Date(),\n\t\t\t})\n\t\t\t.returning();\n\n\t\tif (!inserted) {\n\t\t\tthrow new Error('Failed to insert checkout session');\n\t\t}\n\n\t\treturn {\n\t\t\tid: inserted.id,\n\t\t\tuserId: inserted.userId,\n\t\t\tamount: inserted.amount,\n\t\t\tredirectUrl: inserted.redirectUrl,\n\t\t\tstatus: inserted.status as CheckoutSession['status'],\n\t\t\tcreatedAt: inserted.createdAt,\n\t\t};\n\t}\n\n\tasync function findCheckout(id: string): Promise<CheckoutSession | null> {\n\t\tconst result = await db.select().from(checkouts).where(eq(checkouts.id, id));\n\t\tconst row = result[0];\n\t\tif (!row) return null;\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tuserId: row.userId,\n\t\t\tamount: row.amount,\n\t\t\tredirectUrl: row.redirectUrl,\n\t\t\tstatus: row.status as CheckoutSession['status'],\n\t\t\tcreatedAt: row.createdAt,\n\t\t};\n\t}\n\n\tasync function updateCheckoutStatus(\n\t\tid: string,\n\t\tstatus: CheckoutSession['status'],\n\t): Promise<void> {\n\t\tawait db.update(checkouts).set({ status, updatedAt: new Date() }).where(eq(checkouts.id, id));\n\t}\n\n\tasync function getTransactions(userId: string, query?: TransactionQuery): Promise<LedgerEntry[]> {\n\t\tconst limit = query?.limit ?? 50;\n\t\tconst offset = query?.offset ?? 0;\n\n\t\tconst conditions = [eq(transactions.userId, userId)];\n\t\tif (query?.type === 'credit') {\n\t\t\tconditions.push(gt(transactions.amount, 0));\n\t\t} else if (query?.type === 'debit') {\n\t\t\tconditions.push(lt(transactions.amount, 0));\n\t\t}\n\t\tif (query?.from) {\n\t\t\tconditions.push(gte(transactions.createdAt, query.from));\n\t\t}\n\t\tif (query?.to) {\n\t\t\tconditions.push(lte(transactions.createdAt, query.to));\n\t\t}\n\n\t\tconst rows = await db\n\t\t\t.select()\n\t\t\t.from(transactions)\n\t\t\t.where(and(...conditions))\n\t\t\t.orderBy(desc(transactions.createdAt))\n\t\t\t.limit(limit)\n\t\t\t.offset(offset);\n\n\t\treturn rows.map(toLedgerEntry);\n\t}\n\n\tasync function getCheckouts(userId: string, query?: CheckoutQuery): Promise<CheckoutSession[]> {\n\t\tconst limit = query?.limit ?? 50;\n\t\tconst offset = query?.offset ?? 0;\n\n\t\tconst conditions = [eq(checkouts.userId, userId)];\n\t\tif (query?.status) {\n\t\t\tconditions.push(eq(checkouts.status, query.status));\n\t\t}\n\n\t\tconst rows = await db\n\t\t\t.select()\n\t\t\t.from(checkouts)\n\t\t\t.where(and(...conditions))\n\t\t\t.orderBy(desc(checkouts.createdAt))\n\t\t\t.limit(limit)\n\t\t\t.offset(offset);\n\n\t\treturn rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tuserId: row.userId,\n\t\t\tamount: row.amount,\n\t\t\tredirectUrl: row.redirectUrl,\n\t\t\tstatus: row.status as CheckoutSession['status'],\n\t\t\tcreatedAt: row.createdAt,\n\t\t}));\n\t}\n\n\tasync function expireCredits(userId: string, now: Date): Promise<ExpireResult> {\n\t\treturn db.transaction(async (tx) => {\n\t\t\t// Lock wallet row\n\t\t\tawait tx.select().from(wallets).where(eq(wallets.userId, userId)).for('update');\n\n\t\t\t// Find expired credit buckets with remaining > 0\n\t\t\tconst expiredBuckets = await tx\n\t\t\t\t.select()\n\t\t\t\t.from(transactions)\n\t\t\t\t.where(\n\t\t\t\t\tand(\n\t\t\t\t\t\teq(transactions.userId, userId),\n\t\t\t\t\t\tgt(transactions.amount, 0),\n\t\t\t\t\t\tgt(transactions.remaining, 0),\n\t\t\t\t\t\tisNotNull(transactions.expiresAt),\n\t\t\t\t\t\tlt(transactions.expiresAt, now),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\t.for('update');\n\n\t\t\tlet expiredCount = 0;\n\t\t\tlet expiredAmount = 0;\n\n\t\t\tfor (const bucket of expiredBuckets) {\n\t\t\t\tconst amount = bucket.remaining ?? 0;\n\t\t\t\tif (amount <= 0) continue;\n\n\t\t\t\texpiredAmount += amount;\n\t\t\t\texpiredCount++;\n\n\t\t\t\t// Insert debit entry for expiration\n\t\t\t\tawait tx.insert(transactions).values({\n\t\t\t\t\tid: randomUUID(),\n\t\t\t\t\tuserId,\n\t\t\t\t\tamount: -amount,\n\t\t\t\t\tidempotencyKey: `expire:${bucket.idempotencyKey}`,\n\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\tmetadata: null,\n\t\t\t\t});\n\n\t\t\t\t// Zero out the bucket and mark expired\n\t\t\t\tawait tx\n\t\t\t\t\t.update(transactions)\n\t\t\t\t\t.set({ remaining: 0, expiredAt: now })\n\t\t\t\t\t.where(eq(transactions.id, bucket.id));\n\t\t\t}\n\n\t\t\t// Update wallet balance\n\t\t\tif (expiredAmount > 0) {\n\t\t\t\tconst walletRows = await tx.select().from(wallets).where(eq(wallets.userId, userId));\n\t\t\t\tconst wallet = walletRows[0];\n\t\t\t\tif (wallet) {\n\t\t\t\t\tawait tx\n\t\t\t\t\t\t.update(wallets)\n\t\t\t\t\t\t.set({ balance: wallet.balance - expiredAmount })\n\t\t\t\t\t\t.where(eq(wallets.userId, userId));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { expiredCount, expiredAmount };\n\t\t});\n\t}\n\n\tasync function getUsersWithExpirableCredits(now: Date): Promise<string[]> {\n\t\tconst rows = await db\n\t\t\t.selectDistinct({ userId: transactions.userId })\n\t\t\t.from(transactions)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\tgt(transactions.amount, 0),\n\t\t\t\t\tgt(transactions.remaining, 0),\n\t\t\t\t\tisNotNull(transactions.expiresAt),\n\t\t\t\t\tlt(transactions.expiresAt, now),\n\t\t\t\t),\n\t\t\t);\n\t\treturn rows.map((r) => r.userId);\n\t}\n\n\treturn {\n\t\tgetBalance,\n\t\tappendEntry,\n\t\tfindEntry,\n\t\tsaveCheckout,\n\t\tfindCheckout,\n\t\tupdateCheckoutStatus,\n\t\tgetTransactions,\n\t\tgetCheckouts,\n\t\texpireCredits,\n\t\tgetUsersWithExpirableCredits,\n\t};\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@murai-wallet/storage-drizzle",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Drizzle ORM storage adapter for murai (PostgreSQL)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/ebuntario/murai.git",
|
|
29
|
+
"directory": "packages/storage-drizzle"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://ebuntario.github.io/murai",
|
|
32
|
+
"bugs": "https://github.com/ebuntario/murai/issues",
|
|
33
|
+
"keywords": [
|
|
34
|
+
"drizzle",
|
|
35
|
+
"orm",
|
|
36
|
+
"storage",
|
|
37
|
+
"postgresql",
|
|
38
|
+
"database"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@murai-wallet/core": "1.0.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"drizzle-orm": ">=0.30.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"drizzle-orm": ">=0.30.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"dev": "tsup --watch",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "vitest run --passWithNoTests",
|
|
57
|
+
"clean": "rm -rf dist"
|
|
58
|
+
}
|
|
59
|
+
}
|