@onesub/server 0.20.0 → 0.21.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/dist/__tests__/purchase-product-lookup.test.d.ts +17 -0
- package/dist/__tests__/purchase-product-lookup.test.d.ts.map +1 -0
- package/dist/index.cjs +54 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +54 -9
- package/dist/index.js.map +1 -1
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/store.d.ts +22 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/stores/postgres.d.ts +10 -0
- package/dist/stores/postgres.d.ts.map +1 -1
- package/dist/stores/redis.d.ts +12 -0
- package/dist/stores/redis.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product-scoped purchase lookup — `PurchaseStore.getPurchasesForProduct`.
|
|
3
|
+
*
|
|
4
|
+
* Non-consumable validation used to read a user's entire purchase history and
|
|
5
|
+
* filter it in process, so an account with a long consumable history paid for
|
|
6
|
+
* every one of those rows on each lifetime-product purchase. The optional
|
|
7
|
+
* store method pushes the filter into an index instead.
|
|
8
|
+
*
|
|
9
|
+
* Covered here:
|
|
10
|
+
* - the store contract, including the most-recent-first ordering all three
|
|
11
|
+
* built-in stores must agree on
|
|
12
|
+
* - that the route actually uses the index-backed method when a store has one
|
|
13
|
+
* (responses are identical either way, so only a call count shows it)
|
|
14
|
+
* - that a store without the method still works, since it is optional
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=purchase-product-lookup.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purchase-product-lookup.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/purchase-product-lookup.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
package/dist/index.cjs
CHANGED
|
@@ -91,11 +91,17 @@ var InMemoryPurchaseStore = class {
|
|
|
91
91
|
}
|
|
92
92
|
this.byTransactionId.set(purchase.transactionId, purchase);
|
|
93
93
|
const list = this.byUserId.get(purchase.userId) ?? [];
|
|
94
|
-
|
|
94
|
+
const at = Date.parse(purchase.purchasedAt);
|
|
95
|
+
const idx = list.findIndex((p) => Date.parse(p.purchasedAt) < at);
|
|
96
|
+
if (idx === -1) list.push(purchase);
|
|
97
|
+
else list.splice(idx, 0, purchase);
|
|
95
98
|
this.byUserId.set(purchase.userId, list);
|
|
96
99
|
}
|
|
97
100
|
async getPurchasesByUserId(userId) {
|
|
98
|
-
return this.byUserId.get(userId) ?? [];
|
|
101
|
+
return [...this.byUserId.get(userId) ?? []];
|
|
102
|
+
}
|
|
103
|
+
async getPurchasesForProduct(userId, productId) {
|
|
104
|
+
return (this.byUserId.get(userId) ?? []).filter((p) => p.productId === productId);
|
|
99
105
|
}
|
|
100
106
|
async getPurchaseByTransactionId(txId) {
|
|
101
107
|
return this.byTransactionId.get(txId) ?? null;
|
|
@@ -1981,6 +1987,13 @@ var purchaseStatusQuerySchema = zod.z.object({
|
|
|
1981
1987
|
userId: zod.z.string().min(1).max(256),
|
|
1982
1988
|
productId: zod.z.string().min(1).max(256).optional()
|
|
1983
1989
|
});
|
|
1990
|
+
async function purchasesForProduct(purchaseStore, userId, productId) {
|
|
1991
|
+
if (purchaseStore.getPurchasesForProduct) {
|
|
1992
|
+
return purchaseStore.getPurchasesForProduct(userId, productId);
|
|
1993
|
+
}
|
|
1994
|
+
const all = await purchaseStore.getPurchasesByUserId(userId);
|
|
1995
|
+
return all.filter((p) => p.productId === productId);
|
|
1996
|
+
}
|
|
1984
1997
|
function createPurchaseRouter(config, purchaseStore) {
|
|
1985
1998
|
const router = express.Router();
|
|
1986
1999
|
const registry = getAppRegistry(config);
|
|
@@ -2002,9 +2015,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2002
2015
|
});
|
|
2003
2016
|
try {
|
|
2004
2017
|
if (type === shared.PURCHASE_TYPE.NON_CONSUMABLE) {
|
|
2005
|
-
const owned = (await purchaseStore
|
|
2006
|
-
(p) => p.productId === productId
|
|
2007
|
-
);
|
|
2018
|
+
const owned = (await purchasesForProduct(purchaseStore, userId, productId))[0];
|
|
2008
2019
|
if (owned) {
|
|
2009
2020
|
const response2 = {
|
|
2010
2021
|
valid: true,
|
|
@@ -2134,10 +2145,7 @@ function createPurchaseRouter(config, purchaseStore) {
|
|
|
2134
2145
|
}
|
|
2135
2146
|
const { userId, productId } = query;
|
|
2136
2147
|
try {
|
|
2137
|
-
|
|
2138
|
-
if (productId !== void 0) {
|
|
2139
|
-
purchases = purchases.filter((p) => p.productId === productId);
|
|
2140
|
-
}
|
|
2148
|
+
const purchases = productId !== void 0 ? await purchasesForProduct(purchaseStore, userId, productId) : await purchaseStore.getPurchasesByUserId(userId);
|
|
2141
2149
|
const response = { purchases };
|
|
2142
2150
|
res.status(200).json(response);
|
|
2143
2151
|
} catch (err2) {
|
|
@@ -3113,6 +3121,26 @@ var PostgresPurchaseStore = class {
|
|
|
3113
3121
|
);
|
|
3114
3122
|
return result.rows.map(rowToPurchaseInfo);
|
|
3115
3123
|
}
|
|
3124
|
+
/**
|
|
3125
|
+
* Purchases for one user + product, most-recent-first.
|
|
3126
|
+
*
|
|
3127
|
+
* Same shape and ordering as `getPurchasesByUserId`, with `product_id` pushed
|
|
3128
|
+
* into the WHERE clause instead of filtered in process. Non-consumables also
|
|
3129
|
+
* have a partial unique index on `(user_id, product_id)`, so the ownership
|
|
3130
|
+
* check this backs is a single index lookup rather than a full read of the
|
|
3131
|
+
* user's purchase history.
|
|
3132
|
+
*/
|
|
3133
|
+
async getPurchasesForProduct(userId, productId) {
|
|
3134
|
+
const pool = await this.getPool();
|
|
3135
|
+
const result = await pool.query(
|
|
3136
|
+
`SELECT *
|
|
3137
|
+
FROM onesub_purchases
|
|
3138
|
+
WHERE user_id = $1 AND product_id = $2
|
|
3139
|
+
ORDER BY purchased_at DESC`,
|
|
3140
|
+
[userId, productId]
|
|
3141
|
+
);
|
|
3142
|
+
return result.rows.map(rowToPurchaseInfo);
|
|
3143
|
+
}
|
|
3116
3144
|
async getPurchaseByTransactionId(txId) {
|
|
3117
3145
|
const pool = await this.getPool();
|
|
3118
3146
|
const result = await pool.query(
|
|
@@ -3323,6 +3351,23 @@ var RedisPurchaseStore = class {
|
|
|
3323
3351
|
const raws = await this.redis.mget(...ids.map((id) => PUR_TX_PREFIX + id));
|
|
3324
3352
|
return raws.filter((r) => r != null).map((r) => JSON.parse(r));
|
|
3325
3353
|
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Purchases for one user + product, most-recent-first.
|
|
3356
|
+
*
|
|
3357
|
+
* Served from the `user_product` set that `savePurchase` already maintains for
|
|
3358
|
+
* `hasPurchased`, so this reads only the rows for this product rather than the
|
|
3359
|
+
* user's whole purchase history.
|
|
3360
|
+
*
|
|
3361
|
+
* That set is unordered — unlike the per-user sorted set — so the
|
|
3362
|
+
* most-recent-first contract is restored by an explicit sort here. The row
|
|
3363
|
+
* count is per-product, so this is small.
|
|
3364
|
+
*/
|
|
3365
|
+
async getPurchasesForProduct(userId, productId) {
|
|
3366
|
+
const ids = await this.redis.smembers(PUR_USER_PRODUCT_PREFIX + userId + ":" + productId);
|
|
3367
|
+
if (ids.length === 0) return [];
|
|
3368
|
+
const raws = await this.redis.mget(...ids.map((id) => PUR_TX_PREFIX + id));
|
|
3369
|
+
return raws.filter((r) => r != null).map((r) => JSON.parse(r)).sort((a, b) => Date.parse(b.purchasedAt) - Date.parse(a.purchasedAt));
|
|
3370
|
+
}
|
|
3326
3371
|
async getPurchaseByTransactionId(txId) {
|
|
3327
3372
|
const raw = await this.redis.get(PUR_TX_PREFIX + txId);
|
|
3328
3373
|
return raw ? JSON.parse(raw) : null;
|