@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/index.js CHANGED
@@ -84,11 +84,17 @@ var InMemoryPurchaseStore = class {
84
84
  }
85
85
  this.byTransactionId.set(purchase.transactionId, purchase);
86
86
  const list = this.byUserId.get(purchase.userId) ?? [];
87
- list.push(purchase);
87
+ const at = Date.parse(purchase.purchasedAt);
88
+ const idx = list.findIndex((p) => Date.parse(p.purchasedAt) < at);
89
+ if (idx === -1) list.push(purchase);
90
+ else list.splice(idx, 0, purchase);
88
91
  this.byUserId.set(purchase.userId, list);
89
92
  }
90
93
  async getPurchasesByUserId(userId) {
91
- return this.byUserId.get(userId) ?? [];
94
+ return [...this.byUserId.get(userId) ?? []];
95
+ }
96
+ async getPurchasesForProduct(userId, productId) {
97
+ return (this.byUserId.get(userId) ?? []).filter((p) => p.productId === productId);
92
98
  }
93
99
  async getPurchaseByTransactionId(txId) {
94
100
  return this.byTransactionId.get(txId) ?? null;
@@ -1974,6 +1980,13 @@ var purchaseStatusQuerySchema = z.object({
1974
1980
  userId: z.string().min(1).max(256),
1975
1981
  productId: z.string().min(1).max(256).optional()
1976
1982
  });
1983
+ async function purchasesForProduct(purchaseStore, userId, productId) {
1984
+ if (purchaseStore.getPurchasesForProduct) {
1985
+ return purchaseStore.getPurchasesForProduct(userId, productId);
1986
+ }
1987
+ const all = await purchaseStore.getPurchasesByUserId(userId);
1988
+ return all.filter((p) => p.productId === productId);
1989
+ }
1977
1990
  function createPurchaseRouter(config, purchaseStore) {
1978
1991
  const router = Router();
1979
1992
  const registry = getAppRegistry(config);
@@ -1995,9 +2008,7 @@ function createPurchaseRouter(config, purchaseStore) {
1995
2008
  });
1996
2009
  try {
1997
2010
  if (type === PURCHASE_TYPE.NON_CONSUMABLE) {
1998
- const owned = (await purchaseStore.getPurchasesByUserId(userId)).find(
1999
- (p) => p.productId === productId
2000
- );
2011
+ const owned = (await purchasesForProduct(purchaseStore, userId, productId))[0];
2001
2012
  if (owned) {
2002
2013
  const response2 = {
2003
2014
  valid: true,
@@ -2127,10 +2138,7 @@ function createPurchaseRouter(config, purchaseStore) {
2127
2138
  }
2128
2139
  const { userId, productId } = query;
2129
2140
  try {
2130
- let purchases = await purchaseStore.getPurchasesByUserId(userId);
2131
- if (productId !== void 0) {
2132
- purchases = purchases.filter((p) => p.productId === productId);
2133
- }
2141
+ const purchases = productId !== void 0 ? await purchasesForProduct(purchaseStore, userId, productId) : await purchaseStore.getPurchasesByUserId(userId);
2134
2142
  const response = { purchases };
2135
2143
  res.status(200).json(response);
2136
2144
  } catch (err2) {
@@ -3106,6 +3114,26 @@ var PostgresPurchaseStore = class {
3106
3114
  );
3107
3115
  return result.rows.map(rowToPurchaseInfo);
3108
3116
  }
3117
+ /**
3118
+ * Purchases for one user + product, most-recent-first.
3119
+ *
3120
+ * Same shape and ordering as `getPurchasesByUserId`, with `product_id` pushed
3121
+ * into the WHERE clause instead of filtered in process. Non-consumables also
3122
+ * have a partial unique index on `(user_id, product_id)`, so the ownership
3123
+ * check this backs is a single index lookup rather than a full read of the
3124
+ * user's purchase history.
3125
+ */
3126
+ async getPurchasesForProduct(userId, productId) {
3127
+ const pool = await this.getPool();
3128
+ const result = await pool.query(
3129
+ `SELECT *
3130
+ FROM onesub_purchases
3131
+ WHERE user_id = $1 AND product_id = $2
3132
+ ORDER BY purchased_at DESC`,
3133
+ [userId, productId]
3134
+ );
3135
+ return result.rows.map(rowToPurchaseInfo);
3136
+ }
3109
3137
  async getPurchaseByTransactionId(txId) {
3110
3138
  const pool = await this.getPool();
3111
3139
  const result = await pool.query(
@@ -3316,6 +3344,23 @@ var RedisPurchaseStore = class {
3316
3344
  const raws = await this.redis.mget(...ids.map((id) => PUR_TX_PREFIX + id));
3317
3345
  return raws.filter((r) => r != null).map((r) => JSON.parse(r));
3318
3346
  }
3347
+ /**
3348
+ * Purchases for one user + product, most-recent-first.
3349
+ *
3350
+ * Served from the `user_product` set that `savePurchase` already maintains for
3351
+ * `hasPurchased`, so this reads only the rows for this product rather than the
3352
+ * user's whole purchase history.
3353
+ *
3354
+ * That set is unordered — unlike the per-user sorted set — so the
3355
+ * most-recent-first contract is restored by an explicit sort here. The row
3356
+ * count is per-product, so this is small.
3357
+ */
3358
+ async getPurchasesForProduct(userId, productId) {
3359
+ const ids = await this.redis.smembers(PUR_USER_PRODUCT_PREFIX + userId + ":" + productId);
3360
+ if (ids.length === 0) return [];
3361
+ const raws = await this.redis.mget(...ids.map((id) => PUR_TX_PREFIX + id));
3362
+ return raws.filter((r) => r != null).map((r) => JSON.parse(r)).sort((a, b) => Date.parse(b.purchasedAt) - Date.parse(a.purchasedAt));
3363
+ }
3319
3364
  async getPurchaseByTransactionId(txId) {
3320
3365
  const raw = await this.redis.get(PUR_TX_PREFIX + txId);
3321
3366
  return raw ? JSON.parse(raw) : null;