@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.
@@ -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
- list.push(purchase);
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.getPurchasesByUserId(userId)).find(
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
- let purchases = await purchaseStore.getPurchasesByUserId(userId);
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;