@coursebuilder/adapter-drizzle 0.5.0 → 0.5.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.
Files changed (190) hide show
  1. package/dist/{chunk-BM5UKWXI.js → chunk-27UC2HEP.js} +2 -2
  2. package/dist/{chunk-QTGVGIRO.js → chunk-6SLQW6OM.js} +2 -2
  3. package/dist/{chunk-TBM3AK3P.js → chunk-EFVZ7IN4.js} +2 -2
  4. package/dist/{chunk-FRA4ZCRA.js → chunk-JW2PFVLO.js} +485 -223
  5. package/dist/{chunk-O6JECJRY.js → chunk-QQA7STVO.js} +2 -2
  6. package/dist/{chunk-CQNKTFXE.js → chunk-XJXCDN7B.js} +2 -2
  7. package/dist/{chunk-JZQ666KH.js → chunk-YMFPIZMO.js} +2 -2
  8. package/dist/{chunk-Z245DYMP.js → chunk-ZYQYJNLK.js} +569 -198
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +12 -12
  11. package/dist/lib/mysql/index.cjs +1349 -721
  12. package/dist/lib/mysql/index.cjs.map +1 -1
  13. package/dist/lib/mysql/index.d.cts +3147 -126
  14. package/dist/lib/mysql/index.d.ts +3147 -126
  15. package/dist/lib/mysql/index.js +10 -8
  16. package/dist/lib/mysql/schemas/auth/accounts.cjs +87 -71
  17. package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
  18. package/dist/lib/mysql/schemas/auth/accounts.d.cts +58 -1
  19. package/dist/lib/mysql/schemas/auth/accounts.d.ts +58 -1
  20. package/dist/lib/mysql/schemas/auth/accounts.js +1 -1
  21. package/dist/lib/mysql/schemas/auth/device-access-token.cjs +79 -63
  22. package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
  23. package/dist/lib/mysql/schemas/auth/device-access-token.d.cts +12 -0
  24. package/dist/lib/mysql/schemas/auth/device-access-token.d.ts +12 -0
  25. package/dist/lib/mysql/schemas/auth/device-access-token.js +2 -2
  26. package/dist/lib/mysql/schemas/auth/device-verification.cjs +81 -65
  27. package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
  28. package/dist/lib/mysql/schemas/auth/device-verification.d.cts +24 -0
  29. package/dist/lib/mysql/schemas/auth/device-verification.d.ts +24 -0
  30. package/dist/lib/mysql/schemas/auth/device-verification.js +2 -2
  31. package/dist/lib/mysql/schemas/auth/permissions.d.cts +28 -0
  32. package/dist/lib/mysql/schemas/auth/permissions.d.ts +28 -0
  33. package/dist/lib/mysql/schemas/auth/role-permissions.cjs +76 -60
  34. package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
  35. package/dist/lib/mysql/schemas/auth/role-permissions.d.cts +24 -0
  36. package/dist/lib/mysql/schemas/auth/role-permissions.d.ts +24 -0
  37. package/dist/lib/mysql/schemas/auth/role-permissions.js +2 -2
  38. package/dist/lib/mysql/schemas/auth/roles.cjs +76 -60
  39. package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
  40. package/dist/lib/mysql/schemas/auth/roles.d.cts +28 -0
  41. package/dist/lib/mysql/schemas/auth/roles.d.ts +28 -0
  42. package/dist/lib/mysql/schemas/auth/roles.js +1 -1
  43. package/dist/lib/mysql/schemas/auth/sessions.cjs +77 -61
  44. package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
  45. package/dist/lib/mysql/schemas/auth/sessions.d.cts +12 -0
  46. package/dist/lib/mysql/schemas/auth/sessions.d.ts +12 -0
  47. package/dist/lib/mysql/schemas/auth/sessions.js +2 -2
  48. package/dist/lib/mysql/schemas/auth/user-permissions.cjs +78 -62
  49. package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
  50. package/dist/lib/mysql/schemas/auth/user-permissions.d.cts +24 -0
  51. package/dist/lib/mysql/schemas/auth/user-permissions.d.ts +24 -0
  52. package/dist/lib/mysql/schemas/auth/user-permissions.js +1 -1
  53. package/dist/lib/mysql/schemas/auth/user-prefs.cjs +82 -66
  54. package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
  55. package/dist/lib/mysql/schemas/auth/user-prefs.d.cts +28 -0
  56. package/dist/lib/mysql/schemas/auth/user-prefs.d.ts +28 -0
  57. package/dist/lib/mysql/schemas/auth/user-prefs.js +1 -1
  58. package/dist/lib/mysql/schemas/auth/user-roles.cjs +78 -62
  59. package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
  60. package/dist/lib/mysql/schemas/auth/user-roles.d.cts +24 -0
  61. package/dist/lib/mysql/schemas/auth/user-roles.d.ts +24 -0
  62. package/dist/lib/mysql/schemas/auth/user-roles.js +1 -1
  63. package/dist/lib/mysql/schemas/auth/users.cjs +179 -159
  64. package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
  65. package/dist/lib/mysql/schemas/auth/users.d.cts +32 -0
  66. package/dist/lib/mysql/schemas/auth/users.d.ts +32 -0
  67. package/dist/lib/mysql/schemas/auth/users.js +1 -1
  68. package/dist/lib/mysql/schemas/auth/verification-tokens.d.cts +16 -0
  69. package/dist/lib/mysql/schemas/auth/verification-tokens.d.ts +16 -0
  70. package/dist/lib/mysql/schemas/commerce/coupon.cjs +57 -51
  71. package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
  72. package/dist/lib/mysql/schemas/commerce/coupon.d.cts +50 -14
  73. package/dist/lib/mysql/schemas/commerce/coupon.d.ts +50 -14
  74. package/dist/lib/mysql/schemas/commerce/coupon.js +1 -1
  75. package/dist/lib/mysql/schemas/commerce/merchant-account.d.cts +20 -0
  76. package/dist/lib/mysql/schemas/commerce/merchant-account.d.ts +20 -0
  77. package/dist/lib/mysql/schemas/commerce/merchant-charge.d.cts +32 -0
  78. package/dist/lib/mysql/schemas/commerce/merchant-charge.d.ts +32 -0
  79. package/dist/lib/mysql/schemas/commerce/merchant-coupon.d.cts +24 -0
  80. package/dist/lib/mysql/schemas/commerce/merchant-coupon.d.ts +24 -0
  81. package/dist/lib/mysql/schemas/commerce/merchant-customer.d.cts +24 -0
  82. package/dist/lib/mysql/schemas/commerce/merchant-customer.d.ts +24 -0
  83. package/dist/lib/mysql/schemas/commerce/merchant-price.d.cts +28 -0
  84. package/dist/lib/mysql/schemas/commerce/merchant-price.d.ts +28 -0
  85. package/dist/lib/mysql/schemas/commerce/merchant-product.d.cts +24 -0
  86. package/dist/lib/mysql/schemas/commerce/merchant-product.d.ts +24 -0
  87. package/dist/lib/mysql/schemas/commerce/merchant-session.d.cts +12 -0
  88. package/dist/lib/mysql/schemas/commerce/merchant-session.d.ts +12 -0
  89. package/dist/lib/mysql/schemas/commerce/price.cjs +44 -28
  90. package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
  91. package/dist/lib/mysql/schemas/commerce/price.d.cts +28 -0
  92. package/dist/lib/mysql/schemas/commerce/price.d.ts +28 -0
  93. package/dist/lib/mysql/schemas/commerce/price.js +1 -1
  94. package/dist/lib/mysql/schemas/commerce/product.cjs +58 -42
  95. package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
  96. package/dist/lib/mysql/schemas/commerce/product.d.cts +32 -0
  97. package/dist/lib/mysql/schemas/commerce/product.d.ts +32 -0
  98. package/dist/lib/mysql/schemas/commerce/product.js +1 -1
  99. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs +106 -90
  100. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
  101. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.d.cts +40 -0
  102. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.d.ts +40 -0
  103. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +2 -2
  104. package/dist/lib/mysql/schemas/commerce/purchase.cjs +110 -98
  105. package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
  106. package/dist/lib/mysql/schemas/commerce/purchase.d.cts +68 -0
  107. package/dist/lib/mysql/schemas/commerce/purchase.d.ts +68 -0
  108. package/dist/lib/mysql/schemas/commerce/purchase.js +1 -1
  109. package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs +50 -34
  110. package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
  111. package/dist/lib/mysql/schemas/commerce/upgradable-products.d.cts +28 -0
  112. package/dist/lib/mysql/schemas/commerce/upgradable-products.d.ts +28 -0
  113. package/dist/lib/mysql/schemas/commerce/upgradable-products.js +2 -2
  114. package/dist/lib/mysql/schemas/communication/comment.cjs +82 -66
  115. package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
  116. package/dist/lib/mysql/schemas/communication/comment.d.cts +28 -0
  117. package/dist/lib/mysql/schemas/communication/comment.d.ts +28 -0
  118. package/dist/lib/mysql/schemas/communication/comment.js +1 -1
  119. package/dist/lib/mysql/schemas/communication/communication-channel.d.cts +28 -0
  120. package/dist/lib/mysql/schemas/communication/communication-channel.d.ts +28 -0
  121. package/dist/lib/mysql/schemas/communication/communication-preference-types.d.cts +28 -0
  122. package/dist/lib/mysql/schemas/communication/communication-preference-types.d.ts +28 -0
  123. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs +100 -84
  124. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
  125. package/dist/lib/mysql/schemas/communication/communication-preferences.d.cts +44 -0
  126. package/dist/lib/mysql/schemas/communication/communication-preferences.d.ts +44 -0
  127. package/dist/lib/mysql/schemas/communication/communication-preferences.js +1 -1
  128. package/dist/lib/mysql/schemas/content/content-contributions.cjs +112 -92
  129. package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
  130. package/dist/lib/mysql/schemas/content/content-contributions.d.cts +32 -0
  131. package/dist/lib/mysql/schemas/content/content-contributions.d.ts +32 -0
  132. package/dist/lib/mysql/schemas/content/content-contributions.js +1 -1
  133. package/dist/lib/mysql/schemas/content/content-resource-product.cjs +49 -29
  134. package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
  135. package/dist/lib/mysql/schemas/content/content-resource-product.d.cts +28 -0
  136. package/dist/lib/mysql/schemas/content/content-resource-product.d.ts +28 -0
  137. package/dist/lib/mysql/schemas/content/content-resource-product.js +1 -1
  138. package/dist/lib/mysql/schemas/content/content-resource-resource.cjs +49 -29
  139. package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
  140. package/dist/lib/mysql/schemas/content/content-resource-resource.d.cts +28 -0
  141. package/dist/lib/mysql/schemas/content/content-resource-resource.d.ts +28 -0
  142. package/dist/lib/mysql/schemas/content/content-resource-resource.js +1 -1
  143. package/dist/lib/mysql/schemas/content/content-resource-tag.cjs +274 -0
  144. package/dist/lib/mysql/schemas/content/content-resource-tag.cjs.map +1 -0
  145. package/dist/lib/mysql/schemas/content/content-resource-tag.d.cts +97 -0
  146. package/dist/lib/mysql/schemas/content/content-resource-tag.d.ts +97 -0
  147. package/dist/lib/mysql/schemas/content/content-resource-tag.js +21 -0
  148. package/dist/lib/mysql/schemas/content/content-resource-tag.js.map +1 -0
  149. package/dist/lib/mysql/schemas/content/content-resource-version.cjs +288 -0
  150. package/dist/lib/mysql/schemas/content/content-resource-version.cjs.map +1 -0
  151. package/dist/lib/mysql/schemas/content/content-resource-version.d.cts +130 -0
  152. package/dist/lib/mysql/schemas/content/content-resource-version.d.ts +130 -0
  153. package/dist/lib/mysql/schemas/content/content-resource-version.js +21 -0
  154. package/dist/lib/mysql/schemas/content/content-resource-version.js.map +1 -0
  155. package/dist/lib/mysql/schemas/content/content-resource.cjs +141 -15
  156. package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
  157. package/dist/lib/mysql/schemas/content/content-resource.d.cts +47 -0
  158. package/dist/lib/mysql/schemas/content/content-resource.d.ts +47 -0
  159. package/dist/lib/mysql/schemas/content/content-resource.js +1 -1
  160. package/dist/lib/mysql/schemas/content/contribution-types.d.cts +32 -0
  161. package/dist/lib/mysql/schemas/content/contribution-types.d.ts +32 -0
  162. package/dist/lib/mysql/schemas/content/lesson-progress.d.cts +40 -0
  163. package/dist/lib/mysql/schemas/content/lesson-progress.d.ts +40 -0
  164. package/dist/lib/mysql/schemas/content/resource-progress.d.cts +24 -0
  165. package/dist/lib/mysql/schemas/content/resource-progress.d.ts +24 -0
  166. package/dist/lib/mysql/schemas/content/tag-tag.cjs +241 -0
  167. package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -0
  168. package/dist/lib/mysql/schemas/content/tag-tag.d.cts +129 -0
  169. package/dist/lib/mysql/schemas/content/tag-tag.d.ts +129 -0
  170. package/dist/lib/mysql/schemas/content/tag-tag.js +21 -0
  171. package/dist/lib/mysql/schemas/content/tag-tag.js.map +1 -0
  172. package/dist/lib/mysql/schemas/content/tag.cjs +263 -0
  173. package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -0
  174. package/dist/lib/mysql/schemas/content/tag.d.cts +114 -0
  175. package/dist/lib/mysql/schemas/content/tag.d.ts +114 -0
  176. package/dist/lib/mysql/schemas/content/tag.js +21 -0
  177. package/dist/lib/mysql/schemas/content/tag.js.map +1 -0
  178. package/dist/lib/mysql/utils.d.cts +1 -0
  179. package/dist/lib/mysql/utils.d.ts +1 -0
  180. package/dist/lib/utils.d.cts +2 -0
  181. package/dist/lib/utils.d.ts +2 -0
  182. package/package.json +8 -6
  183. package/src/index.ts +14 -4
  184. package/src/lib/mysql/index.ts +718 -182
  185. package/src/lib/mysql/schemas/commerce/coupon.ts +3 -9
  186. package/src/lib/mysql/schemas/content/content-resource-tag.ts +59 -0
  187. package/src/lib/mysql/schemas/content/content-resource-version.ts +69 -0
  188. package/src/lib/mysql/schemas/content/content-resource.ts +17 -0
  189. package/src/lib/mysql/schemas/content/tag-tag.ts +59 -0
  190. package/src/lib/mysql/schemas/content/tag.ts +48 -0
@@ -1,4 +1,5 @@
1
1
  import type { AdapterSession, AdapterUser } from '@auth/core/adapters'
2
+ import slugify from '@sindresorhus/slugify'
2
3
  import { addSeconds, isAfter } from 'date-fns'
3
4
  import {
4
5
  and,
@@ -19,6 +20,7 @@ import {
19
20
  MySqlDatabase,
20
21
  MySqlTableFn,
21
22
  } from 'drizzle-orm/mysql-core'
23
+ import { customAlphabet } from 'nanoid'
22
24
  import { v4 } from 'uuid'
23
25
  import { z } from 'zod'
24
26
 
@@ -34,6 +36,7 @@ import {
34
36
  merchantPriceSchema,
35
37
  MerchantProduct,
36
38
  merchantProductSchema,
39
+ NewProduct,
37
40
  Price,
38
41
  priceSchema,
39
42
  Product,
@@ -51,15 +54,16 @@ import {
51
54
  userSchema,
52
55
  } from '@coursebuilder/core/schemas'
53
56
  import {
57
+ ContentResourceProductSchema,
54
58
  ContentResourceResourceSchema,
55
59
  ContentResourceSchema,
56
60
  type ContentResource,
57
- type ContentResourceResource,
58
61
  } from '@coursebuilder/core/schemas/content-resource-schema'
59
62
  import { merchantAccountSchema } from '@coursebuilder/core/schemas/merchant-account-schema'
60
63
  import { merchantCustomerSchema } from '@coursebuilder/core/schemas/merchant-customer-schema'
61
64
  import { type ModuleProgress } from '@coursebuilder/core/schemas/resource-progress-schema'
62
65
  import { VideoResourceSchema } from '@coursebuilder/core/schemas/video-resource'
66
+ import { PaymentsProviderConfig } from '@coursebuilder/core/types'
63
67
  import { logger } from '@coursebuilder/core/utils/logger'
64
68
  import { validateCoupon } from '@coursebuilder/core/utils/validate-coupon'
65
69
 
@@ -161,6 +165,14 @@ import {
161
165
  getContentResourceResourceRelationsSchema,
162
166
  getContentResourceResourceSchema,
163
167
  } from './schemas/content/content-resource-resource.js'
168
+ import {
169
+ getContentResourceTagRelationsSchema,
170
+ getContentResourceTagSchema,
171
+ } from './schemas/content/content-resource-tag.js'
172
+ import {
173
+ getContentResourceVersionRelationsSchema,
174
+ getContentResourceVersionSchema,
175
+ } from './schemas/content/content-resource-version.js'
164
176
  import {
165
177
  getContentResourceRelationsSchema,
166
178
  getContentResourceSchema,
@@ -171,6 +183,13 @@ import {
171
183
  } from './schemas/content/contribution-types.js'
172
184
  import { getLessonProgressSchema } from './schemas/content/lesson-progress.js'
173
185
  import { getResourceProgressSchema } from './schemas/content/resource-progress.js'
186
+ import {
187
+ getTagTagRelationsSchema,
188
+ getTagTagSchema,
189
+ } from './schemas/content/tag-tag.js'
190
+ import { getTagRelationsSchema, getTagSchema } from './schemas/content/tag.js'
191
+
192
+ export const guid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 5)
174
193
 
175
194
  export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
176
195
  return {
@@ -219,10 +238,16 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
219
238
  contentContributionRelations:
220
239
  getContentContributionRelationsSchema(mysqlTable),
221
240
  contentResource: getContentResourceSchema(mysqlTable),
241
+ contentResourceVersion: getContentResourceVersionSchema(mysqlTable),
242
+ contentResourceVersionRelations:
243
+ getContentResourceVersionRelationsSchema(mysqlTable),
222
244
  contentResourceRelations: getContentResourceRelationsSchema(mysqlTable),
223
245
  contentResourceResource: getContentResourceResourceSchema(mysqlTable),
224
246
  contentResourceResourceRelations:
225
247
  getContentResourceResourceRelationsSchema(mysqlTable),
248
+ contentResourceTag: getContentResourceTagSchema(mysqlTable),
249
+ contentResourceTagRelations:
250
+ getContentResourceTagRelationsSchema(mysqlTable),
226
251
  contributionTypes: getContributionTypesSchema(mysqlTable),
227
252
  contributionTypesRelations: getContributionTypesRelationsSchema(mysqlTable),
228
253
  resourceProgress: getResourceProgressSchema(mysqlTable),
@@ -240,6 +265,10 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
240
265
  getDeviceVerificationRelationsSchema(mysqlTable),
241
266
  deviceAccessToken: getDeviceAccessTokenSchema(mysqlTable),
242
267
  deviceAccessTokenRelations: getDeviceAccessTokenRelationsSchema(mysqlTable),
268
+ tag: getTagSchema(mysqlTable),
269
+ tagRelations: getTagRelationsSchema(mysqlTable),
270
+ tagTag: getTagTagSchema(mysqlTable),
271
+ tagTagRelations: getTagTagRelationsSchema(mysqlTable),
243
272
  userPrefs: getUserPrefsSchema(mysqlTable),
244
273
  userPrefsRelations: getUserPrefsRelationsSchema(mysqlTable),
245
274
  } as const
@@ -254,6 +283,7 @@ export type DefaultSchema = ReturnType<typeof createTables>
254
283
  export function mySqlDrizzleAdapter(
255
284
  client: InstanceType<typeof MySqlDatabase>,
256
285
  tableFn = defaultMySqlTableFn,
286
+ paymentProvider?: PaymentsProviderConfig,
257
287
  ): CourseBuilderAdapter<typeof MySqlDatabase> {
258
288
  const {
259
289
  users,
@@ -262,6 +292,7 @@ export function mySqlDrizzleAdapter(
262
292
  verificationTokens,
263
293
  contentResource,
264
294
  contentResourceResource,
295
+ contentResourceProduct,
265
296
  purchases: purchaseTable,
266
297
  purchaseUserTransfer,
267
298
  coupon,
@@ -304,7 +335,7 @@ export function mySqlDrizzleAdapter(
304
335
  if (coupon && couponValidation.isRedeemable) {
305
336
  // if the Coupon is the Bulk Coupon of a Bulk Purchase,
306
337
  // then a bulk coupon is being redeemed
307
- const bulkCouponRedemption = Boolean(coupon.bulkPurchase?.bulkCouponId)
338
+ const bulkCouponRedemption = Boolean(coupon.maxUses > 1)
308
339
 
309
340
  const { user } = await adapter.findOrCreateUser(email)
310
341
 
@@ -325,8 +356,18 @@ export function mySqlDrizzleAdapter(
325
356
  productId,
326
357
  })
327
358
 
328
- if (existingPurchases.length > 0)
329
- throw new Error(`already-purchased-${email}`)
359
+ if (existingPurchases.length > 0) {
360
+ const errorMessage = `already-purchased-${email}`
361
+ console.error(errorMessage)
362
+ return {
363
+ error: {
364
+ message: errorMessage,
365
+ },
366
+ redeemingForCurrentUser,
367
+ purchase: null,
368
+ }
369
+ throw new Error(errorMessage)
370
+ }
330
371
 
331
372
  const purchaseId = `purchase-${v4()}`
332
373
 
@@ -410,7 +451,10 @@ export function mySqlDrizzleAdapter(
410
451
  },
411
452
  getMerchantPriceForProductId: async (productId) => {
412
453
  const merchantPriceData = await client.query.merchantPrice.findFirst({
413
- where: eq(merchantPrice.merchantProductId, productId),
454
+ where: and(
455
+ eq(merchantPrice.merchantProductId, productId),
456
+ eq(merchantPrice.status, 1),
457
+ ),
414
458
  })
415
459
 
416
460
  const parsedMerchantPrice =
@@ -463,7 +507,7 @@ export function mySqlDrizzleAdapter(
463
507
  )
464
508
 
465
509
  if (previousPurchaseProductIds.length > 0) {
466
- return await client.query.upgradableProducts.findMany({
510
+ return client.query.upgradableProducts.findMany({
467
511
  where: and(
468
512
  eq(upgradableProducts.upgradableToId, productId),
469
513
  inArray(
@@ -871,20 +915,21 @@ export function mySqlDrizzleAdapter(
871
915
  },
872
916
  async getCouponWithBulkPurchases(couponId: string): Promise<
873
917
  | (Coupon & {
874
- bulkPurchase?: Purchase | null
875
- bulkCouponPurchases: { bulkCouponId?: string | null }[]
918
+ bulkPurchases?: Purchase[] | null
919
+ redeemedBulkCouponPurchases: { bulkCouponId?: string | null }[]
876
920
  })
877
921
  | null
878
922
  > {
879
923
  logger.debug('getCouponWithBulkPurchases', { couponId })
880
924
  let couponData
925
+ let bulkCouponPurchases
881
926
  try {
882
927
  couponData =
883
928
  (await client.query.coupon.findFirst({
884
929
  where: eq(coupon.id, couponId),
885
930
  with: {
886
- bulkPurchase: true,
887
- bulkCouponPurchases: true,
931
+ bulkPurchases: true,
932
+ redeemedBulkCouponPurchases: true,
888
933
  },
889
934
  })) || null
890
935
  } catch (e) {
@@ -892,6 +937,19 @@ export function mySqlDrizzleAdapter(
892
937
  logger.error(e as Error)
893
938
  }
894
939
 
940
+ try {
941
+ bulkCouponPurchases = await client.query.purchases.findMany({
942
+ where: eq(purchaseTable.redeemedBulkCouponId, couponId),
943
+ with: {
944
+ user: true,
945
+ },
946
+ })
947
+ console.log('purchases with redeemedBulkCouponId', bulkCouponPurchases)
948
+ } catch (e) {
949
+ console.log('getCouponWithBulkPurchases')
950
+ logger.error(e as Error)
951
+ }
952
+
895
953
  if (!couponData) {
896
954
  logger.debug('getCouponWithBulkPurchases', {
897
955
  couponId,
@@ -900,20 +958,29 @@ export function mySqlDrizzleAdapter(
900
958
  return null
901
959
  }
902
960
 
961
+ const couponWithBulkPurchases = {
962
+ ...couponData,
963
+ redeemedBulkCouponPurchases: bulkCouponPurchases || [],
964
+ }
965
+
903
966
  const parsedCoupon = couponSchema
904
967
  .merge(
905
968
  z.object({
906
- bulkCouponPurchases: z.array(purchaseSchema),
907
- bulkPurchase: purchaseSchema.nullable().optional(),
969
+ redeemedBulkCouponPurchases: z.array(purchaseSchema),
908
970
  }),
909
971
  )
910
972
  .nullable()
911
- .safeParse(couponData)
973
+ .safeParse(couponWithBulkPurchases)
912
974
 
913
975
  if (!parsedCoupon.success) {
914
- console.error('Error parsing coupon', couponData)
976
+ console.error(
977
+ 'Error parsing coupon',
978
+ JSON.stringify(parsedCoupon.error),
979
+ couponData,
980
+ )
915
981
  return null
916
982
  }
983
+
917
984
  return parsedCoupon.data
918
985
  },
919
986
  async getDefaultCoupon(productIds?: string[]): Promise<{
@@ -982,189 +1049,114 @@ export function mySqlDrizzleAdapter(
982
1049
  async getModuleProgressForUser(
983
1050
  userIdOrEmail: string,
984
1051
  moduleIdOrSlug: string,
985
- ): Promise<ModuleProgress> {
986
- const ALLOWED_MODULE_RESOURCE_TYPES = ['lesson', 'exercise', 'solution']
987
- const COUNTABLE_MODULE_RESOURCE_TYPES = ['lesson', 'exercise']
988
-
989
- const module = await client.query.contentResource.findFirst({
990
- where: or(
991
- eq(contentResource.id, moduleIdOrSlug),
992
- eq(
993
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
994
- moduleIdOrSlug,
995
- ),
996
- ),
997
- with: {
998
- resources: {
999
- with: {
1000
- resource: {
1001
- with: {
1002
- resources: {
1003
- with: {
1004
- resource: {
1005
- with: {
1006
- resources: {
1007
- with: {
1008
- resource: true,
1009
- },
1010
- },
1011
- },
1012
- },
1013
- },
1014
- orderBy: asc(contentResourceResource.position),
1015
- },
1016
- },
1017
- },
1018
- },
1019
- orderBy: asc(contentResourceResource.position),
1020
- },
1052
+ ): Promise<ModuleProgress | null> {
1053
+ // First, get the user ID
1054
+ const user = await client.query.users.findFirst({
1055
+ where: or(eq(users.id, userIdOrEmail), eq(users.email, userIdOrEmail)),
1056
+ columns: {
1057
+ id: true,
1021
1058
  },
1022
1059
  })
1023
1060
 
1024
- const parsedModule = ContentResourceSchema.parse(module)
1025
-
1026
- function flattenResources(
1027
- resources: ContentResourceResource[],
1028
- ): ContentResource[] {
1029
- const result: ContentResource[] = []
1030
-
1031
- function recurse(resources: ContentResourceResource[]) {
1032
- for (const nestedResource of resources) {
1033
- const resource = nestedResource.resource
1034
- result.push(resource)
1035
- if (resource.resources) {
1036
- recurse(resource.resources)
1037
- }
1038
- }
1039
- }
1040
-
1041
- recurse(resources)
1042
- return result
1061
+ if (!user) {
1062
+ return null
1043
1063
  }
1044
1064
 
1045
- const allResources =
1046
- parsedModule?.resources && flattenResources(parsedModule.resources)
1047
- const filteredResources = allResources?.filter((resource) => {
1048
- return ALLOWED_MODULE_RESOURCE_TYPES.includes(resource.type)
1065
+ const ResultRowSchema = z.object({
1066
+ resource_id: z.string(),
1067
+ resource_type: z.enum(['lesson', 'exercise']),
1068
+ resource_slug: z.string().nullable(),
1069
+ completed_at: z
1070
+ .string()
1071
+ .nullable()
1072
+ .transform((val) => (val ? new Date(val) : null)),
1049
1073
  })
1050
1074
 
1051
- const parsedResources = z
1052
- .array(ContentResourceSchema)
1053
- .safeParse(filteredResources)
1054
-
1055
- if (!parsedResources.success) {
1056
- console.error('Error parsing module resources', parsedResources.error)
1075
+ // Execute the optimized query
1076
+ const results: any = await client.execute(sql`
1077
+ SELECT
1078
+ cr.id AS resource_id,
1079
+ cr.type AS resource_type,
1080
+ cr.fields->>'$.slug' AS resource_slug,
1081
+ rp.completedAt AS completed_at
1082
+ FROM
1083
+ (SELECT id, fields->>'$.slug' AS slug
1084
+ FROM ${contentResource}
1085
+ WHERE id = ${moduleIdOrSlug}
1086
+ OR fields->>'$.slug' = ${moduleIdOrSlug}) AS w
1087
+ LEFT JOIN
1088
+ (SELECT
1089
+ crr.resourceOfId AS workshop_id,
1090
+ crr.resourceId,
1091
+ crr.position
1092
+ FROM ${contentResourceResource} crr) AS tlr ON w.id = tlr.workshop_id
1093
+ LEFT JOIN ${contentResource} sections ON sections.id = tlr.resourceId AND sections.type = 'section'
1094
+ LEFT JOIN
1095
+ (SELECT
1096
+ crr.resourceOfId AS section_id,
1097
+ crr.resourceId AS resource_id,
1098
+ crr.position AS position_in_section,
1099
+ section_crr.position AS section_position
1100
+ FROM ${contentResourceResource} crr
1101
+ JOIN ${contentResourceResource} section_crr ON crr.resourceOfId = section_crr.resourceId) AS sr
1102
+ ON sr.section_id = sections.id
1103
+ JOIN ${contentResource} cr ON cr.id = COALESCE(sr.resource_id, tlr.resourceId)
1104
+ LEFT JOIN ${resourceProgress} rp ON rp.resourceId = cr.id
1105
+ AND rp.userId = ${user.id}
1106
+ WHERE
1107
+ cr.type IN ('lesson', 'exercise')
1108
+
1109
+ `)
1110
+
1111
+ // Process the results
1112
+ const completedLessons: ResourceProgress[] = []
1113
+ let nextResource: Partial<ContentResource> | null = null
1114
+ let completedLessonsCount = 0
1115
+ let totalLessonsCount = results.rows.length
1116
+
1117
+ const parsedRows = z.array(ResultRowSchema).safeParse(results.rows)
1118
+
1119
+ if (!parsedRows.success) {
1120
+ console.error('Error parsing rows', parsedRows.error)
1057
1121
  return {
1058
1122
  completedLessons: [],
1059
1123
  nextResource: null,
1060
1124
  percentCompleted: 0,
1061
1125
  completedLessonsCount: 0,
1062
- totalLessonsCount: 0,
1126
+ totalLessonsCount,
1063
1127
  }
1064
1128
  }
1065
1129
 
1066
- const progressResources = parsedResources.data.filter((r) => {
1067
- return COUNTABLE_MODULE_RESOURCE_TYPES.includes(r.type)
1068
- })
1069
-
1070
- const totalLessonsCount = progressResources.length
1071
-
1072
- const user = await client.query.users
1073
- .findFirst({
1074
- where: or(
1075
- eq(users.id, userIdOrEmail),
1076
- eq(users.email, userIdOrEmail),
1077
- ),
1078
- with: {
1079
- roles: {
1080
- with: {
1081
- role: true,
1082
- },
1130
+ for (const row of parsedRows.data) {
1131
+ if (row.completed_at) {
1132
+ completedLessonsCount++
1133
+ completedLessons.push({
1134
+ userId: user.id as string,
1135
+ resourceId: row.resource_id,
1136
+ completedAt: new Date(row.completed_at),
1137
+ // Add other fields as needed
1138
+ })
1139
+ } else if (!nextResource) {
1140
+ nextResource = {
1141
+ id: row.resource_id,
1142
+ type: row.resource_type,
1143
+ fields: {
1144
+ slug: row.resource_slug,
1083
1145
  },
1084
- },
1085
- })
1086
- .then(async (res) => {
1087
- if (res) {
1088
- return {
1089
- ...res,
1090
- roles: res.roles.map((r) => r.role),
1091
- }
1092
1146
  }
1093
- })
1094
-
1095
- if (!user) {
1096
- console.error('User not found', userIdOrEmail)
1097
- return {
1098
- completedLessons: [],
1099
- nextResource: null,
1100
- percentCompleted: 0,
1101
- completedLessonsCount: 0,
1102
- totalLessonsCount,
1103
1147
  }
1104
1148
  }
1105
1149
 
1106
- const parsedUser = userSchema.safeParse(user)
1107
-
1108
- if (!parsedUser.success) {
1109
- console.error('Error parsing user', parsedUser.error)
1110
- return {
1111
- completedLessons: [],
1112
- nextResource: null,
1113
- percentCompleted: 0,
1114
- completedLessonsCount: 0,
1115
- totalLessonsCount,
1116
- }
1117
- }
1118
-
1119
- const userProgress = await client.query.resourceProgress.findMany({
1120
- where: and(
1121
- eq(resourceProgress.userId, parsedUser.data.id),
1122
- isNotNull(resourceProgress.completedAt),
1123
- inArray(
1124
- resourceProgress.resourceId,
1125
- progressResources.map((r) => r.id),
1126
- ),
1127
- ),
1128
- orderBy: asc(resourceProgress.completedAt),
1129
- })
1130
-
1131
- const nextResourceId = parsedResources.data
1132
- .filter((r) => {
1133
- return COUNTABLE_MODULE_RESOURCE_TYPES.includes(r.type)
1134
- })
1135
- .find((r) => !userProgress.find((p) => p.resourceId === r.id))?.id
1136
-
1137
- const nextResource = await client.query.contentResource.findFirst({
1138
- where: eq(contentResource.id, nextResourceId as string),
1139
- })
1140
-
1141
- const parsedNextResource = ContentResourceSchema.nullable()
1142
- .optional()
1143
- .default(null)
1144
- .parse(nextResource)
1145
-
1146
- const parsedProgress = z
1147
- .array(resourceProgressSchema)
1148
- .safeParse(userProgress)
1149
- if (!parsedProgress.success) {
1150
- console.error('Error parsing user progress', parsedProgress.error)
1151
- return {
1152
- completedLessons: [],
1153
- nextResource: null,
1154
- percentCompleted: 0,
1155
- completedLessonsCount: 0,
1156
- totalLessonsCount,
1157
- }
1158
- }
1159
- const percentCompleted = Math.round(
1160
- (parsedProgress.data.length / parsedResources.data.length) * 100,
1161
- )
1150
+ const percentCompleted =
1151
+ totalLessonsCount > 0
1152
+ ? Math.ceil((completedLessonsCount / totalLessonsCount) * 100)
1153
+ : 0
1162
1154
 
1163
1155
  return {
1164
- completedLessons: parsedProgress.data,
1165
- nextResource: parsedNextResource,
1156
+ completedLessons,
1157
+ nextResource,
1166
1158
  percentCompleted,
1167
- completedLessonsCount: parsedProgress.data.length,
1159
+ completedLessonsCount,
1168
1160
  totalLessonsCount,
1169
1161
  }
1170
1162
  },
@@ -1179,7 +1171,7 @@ export function mySqlDrizzleAdapter(
1179
1171
  })
1180
1172
  const parsed = merchantChargeSchema.safeParse(mCharge)
1181
1173
  if (!parsed.success) {
1182
- console.error('Error parsing merchantCharge', mCharge)
1174
+ console.info('Error parsing merchantCharge', mCharge)
1183
1175
  return null
1184
1176
  }
1185
1177
  return parsed.data
@@ -1258,12 +1250,356 @@ export function mySqlDrizzleAdapter(
1258
1250
  }),
1259
1251
  )
1260
1252
  },
1261
- async getProduct(productId: string): Promise<Product | null> {
1262
- return productSchema.nullable().parse(
1263
- await client.query.products.findFirst({
1264
- where: eq(products.id, productId),
1253
+ async archiveProduct(productId) {
1254
+ if (!paymentProvider) throw new Error('Payment provider not found')
1255
+ const product = await adapter.getProduct(productId)
1256
+
1257
+ if (!product) {
1258
+ throw new Error(`Product not found for id (${productId})`)
1259
+ }
1260
+
1261
+ if (!product.price) {
1262
+ throw new Error(`Product has no price`)
1263
+ }
1264
+
1265
+ await client
1266
+ .update(products)
1267
+ .set({ status: 0, name: `${product.name} (Archived)` })
1268
+ .where(eq(products.id, productId))
1269
+
1270
+ await client
1271
+ .update(prices)
1272
+ .set({ status: 0, nickname: `${product.name} (Archived)` })
1273
+ .where(eq(prices.productId, productId))
1274
+
1275
+ await client
1276
+ .update(merchantProduct)
1277
+ .set({ status: 0 })
1278
+ .where(eq(merchantProduct.productId, productId))
1279
+
1280
+ await client
1281
+ .update(merchantPrice)
1282
+ .set({ status: 0 })
1283
+ .where(eq(merchantPrice.priceId, product.price.id))
1284
+
1285
+ const currentMerchantProduct = merchantProductSchema.nullish().parse(
1286
+ await client.query.merchantProduct.findFirst({
1287
+ where: eq(merchantProduct.productId, productId),
1265
1288
  }),
1266
1289
  )
1290
+
1291
+ if (!currentMerchantProduct || !currentMerchantProduct.identifier) {
1292
+ throw new Error(`Merchant product not found for id (${productId})`)
1293
+ }
1294
+
1295
+ await paymentProvider.updateProduct(currentMerchantProduct.identifier, {
1296
+ active: false,
1297
+ })
1298
+
1299
+ const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1300
+ await client.query.merchantPrice.findFirst({
1301
+ where: and(
1302
+ eq(merchantPrice.priceId, product.price.id),
1303
+ eq(merchantPrice.status, 1),
1304
+ ),
1305
+ }),
1306
+ )
1307
+
1308
+ if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1309
+ throw new Error(`Merchant price not found for id (${productId})`)
1310
+ }
1311
+
1312
+ await paymentProvider.updatePrice(currentMerchantPrice.identifier, {
1313
+ active: false,
1314
+ })
1315
+
1316
+ return adapter.getProduct(productId)
1317
+ },
1318
+ async updateProduct(input: Product) {
1319
+ if (!paymentProvider) throw new Error('Payment provider not found')
1320
+ const currentProduct = await adapter.getProduct(input.id)
1321
+ if (!currentProduct) {
1322
+ throw new Error(`Product not found`)
1323
+ }
1324
+ if (!currentProduct.price) {
1325
+ throw new Error(`Product has no price`)
1326
+ }
1327
+
1328
+ const merchantProduct = merchantProductSchema.nullish().parse(
1329
+ await client.query.merchantProduct.findFirst({
1330
+ where: (merchantProduct, { eq }) =>
1331
+ eq(merchantProduct.productId, input.id),
1332
+ }),
1333
+ )
1334
+
1335
+ if (!merchantProduct || !merchantProduct.identifier) {
1336
+ throw new Error(`Merchant product not found`)
1337
+ }
1338
+
1339
+ // TODO: handle upgrades
1340
+
1341
+ const stripeProduct = await paymentProvider.getProduct(
1342
+ merchantProduct.identifier,
1343
+ )
1344
+
1345
+ const priceChanged =
1346
+ currentProduct.price.unitAmount.toString() !==
1347
+ input.price?.unitAmount.toString()
1348
+
1349
+ if (priceChanged) {
1350
+ const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1351
+ await client.query.merchantPrice.findFirst({
1352
+ where: (merchantPrice, { eq, and }) =>
1353
+ and(
1354
+ eq(merchantPrice.merchantProductId, merchantProduct.id),
1355
+ eq(merchantPrice.status, 1),
1356
+ ),
1357
+ }),
1358
+ )
1359
+
1360
+ if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1361
+ throw new Error(`Merchant price not found`)
1362
+ }
1363
+
1364
+ const currentStripePrice = await paymentProvider.getPrice(
1365
+ currentMerchantPrice.identifier,
1366
+ )
1367
+
1368
+ const newStripePrice = await paymentProvider.createPrice({
1369
+ product: stripeProduct.id,
1370
+ unit_amount: Math.floor(Number(input.price?.unitAmount || 0) * 100),
1371
+ currency: 'usd',
1372
+ metadata: {
1373
+ slug: input.fields.slug,
1374
+ },
1375
+ active: true,
1376
+ })
1377
+
1378
+ await paymentProvider.updateProduct(stripeProduct.id, {
1379
+ default_price: newStripePrice.id,
1380
+ })
1381
+
1382
+ const newMerchantPriceId = `mprice_${v4()}`
1383
+ await client.insert(merchantPrice).values({
1384
+ id: newMerchantPriceId,
1385
+ merchantProductId: merchantProduct.id,
1386
+ merchantAccountId: merchantProduct.merchantAccountId,
1387
+ priceId: currentProduct.price.id,
1388
+ status: 1,
1389
+ identifier: newStripePrice.id,
1390
+ })
1391
+
1392
+ if (currentMerchantPrice) {
1393
+ await client
1394
+ .update(merchantPrice)
1395
+ .set({
1396
+ status: 0,
1397
+ })
1398
+ .where(eq(merchantPrice.id, currentMerchantPrice.id))
1399
+ }
1400
+
1401
+ await client
1402
+ .update(prices)
1403
+ .set({
1404
+ unitAmount: Math.floor(
1405
+ Number(input.price?.unitAmount || 0),
1406
+ ).toString(),
1407
+ nickname: input.name,
1408
+ })
1409
+ .where(eq(prices.id, currentProduct.price.id))
1410
+
1411
+ if (currentStripePrice) {
1412
+ await paymentProvider.updatePrice(currentStripePrice.id, {
1413
+ active: false,
1414
+ })
1415
+ }
1416
+ }
1417
+
1418
+ await paymentProvider.updateProduct(stripeProduct.id, {
1419
+ name: input.name,
1420
+ active: true,
1421
+ images: input.fields.image?.url ? [input.fields.image.url] : undefined,
1422
+ description: input.fields.description || '',
1423
+ metadata: {
1424
+ slug: input.fields.slug,
1425
+ },
1426
+ })
1427
+
1428
+ const { image, ...fieldsNoImage } = input.fields
1429
+
1430
+ await client
1431
+ .update(products)
1432
+ .set({
1433
+ name: input.name,
1434
+ quantityAvailable: input.quantityAvailable,
1435
+ status: 1,
1436
+ fields: {
1437
+ ...fieldsNoImage,
1438
+ ...(image?.url && { image }),
1439
+ },
1440
+ })
1441
+ .where(eq(products.id, currentProduct.id))
1442
+
1443
+ return adapter.getProduct(currentProduct.id)
1444
+ },
1445
+ async createProduct(input: NewProduct) {
1446
+ if (!paymentProvider) throw new Error('Payment provider not found')
1447
+ const merchantAccount = merchantAccountSchema.nullish().parse(
1448
+ await client.query.merchantAccount.findFirst({
1449
+ where: (merchantAccount, { eq }) =>
1450
+ eq(merchantAccount.label, 'stripe'),
1451
+ }),
1452
+ )
1453
+
1454
+ if (!merchantAccount) {
1455
+ throw new Error('Merchant account not found')
1456
+ }
1457
+
1458
+ const hash = guid()
1459
+ const newProductId = slugify(`product-${hash}`)
1460
+
1461
+ const newProduct = {
1462
+ id: newProductId,
1463
+ name: input.name,
1464
+ status: 1,
1465
+ type: 'self-paced',
1466
+ quantityAvailable: input.quantityAvailable,
1467
+ fields: {
1468
+ state: 'draft',
1469
+ visibility: 'unlisted',
1470
+ slug: slugify(`${input.name}-${hash}`),
1471
+ },
1472
+ }
1473
+
1474
+ await client.insert(products).values(newProduct)
1475
+
1476
+ const priceHash = guid()
1477
+ const newPriceId = `price-${priceHash}`
1478
+
1479
+ await client.insert(prices).values({
1480
+ id: newPriceId,
1481
+ productId: newProductId,
1482
+ unitAmount: input.price.toString(),
1483
+ status: 1,
1484
+ })
1485
+
1486
+ const product = await adapter.getProduct(newProductId)
1487
+
1488
+ const stripeProduct = await paymentProvider.createProduct({
1489
+ name: input.name,
1490
+ metadata: {
1491
+ slug: product?.fields?.slug || null,
1492
+ },
1493
+ })
1494
+
1495
+ const stripePrice = await paymentProvider.createPrice({
1496
+ product: stripeProduct.id,
1497
+ unit_amount: Math.floor(Number(input.price) * 100),
1498
+ currency: 'usd',
1499
+ nickname: input.name,
1500
+ metadata: {
1501
+ slug: product?.fields?.slug || null,
1502
+ },
1503
+ })
1504
+
1505
+ const newMerchantProductId = `mproduct_${v4()}`
1506
+
1507
+ await client.insert(merchantProduct).values({
1508
+ id: newMerchantProductId,
1509
+ merchantAccountId: merchantAccount.id,
1510
+ productId: newProductId,
1511
+ identifier: stripeProduct.id,
1512
+ status: 1,
1513
+ })
1514
+
1515
+ const newMerchantPriceId = `mprice_${v4()}`
1516
+ await client.insert(merchantPrice).values({
1517
+ id: newMerchantPriceId,
1518
+ merchantAccountId: merchantAccount.id,
1519
+ merchantProductId: newMerchantProductId,
1520
+ priceId: newPriceId,
1521
+ identifier: stripePrice.id,
1522
+ status: 1,
1523
+ })
1524
+
1525
+ // TODO: handle upgrades
1526
+
1527
+ return product
1528
+ },
1529
+ async getProduct(
1530
+ productSlugOrId?: string,
1531
+ withResources: boolean = true,
1532
+ ): Promise<Product | null> {
1533
+ if (!productSlugOrId) {
1534
+ return null
1535
+ }
1536
+
1537
+ try {
1538
+ const productData = await client.query.products.findFirst({
1539
+ where: and(
1540
+ or(
1541
+ eq(
1542
+ sql`JSON_EXTRACT (${products.fields}, "$.slug")`,
1543
+ `${productSlugOrId}`,
1544
+ ),
1545
+ eq(products.id, productSlugOrId),
1546
+ ),
1547
+ ),
1548
+ with: {
1549
+ price: true,
1550
+ ...(withResources && {
1551
+ resources: {
1552
+ with: {
1553
+ resource: {
1554
+ with: {
1555
+ resources: true,
1556
+ },
1557
+ },
1558
+ },
1559
+ },
1560
+ }),
1561
+ },
1562
+ })
1563
+ const parsedProduct = productSchema.safeParse(productData)
1564
+ if (!parsedProduct.success) {
1565
+ console.error(
1566
+ 'Error parsing product',
1567
+ JSON.stringify(parsedProduct.error),
1568
+ JSON.stringify(productData),
1569
+ )
1570
+ return null
1571
+ }
1572
+ return parsedProduct.data
1573
+ } catch (e) {
1574
+ console.log('getProduct error', e)
1575
+ return null
1576
+ }
1577
+ },
1578
+ async getProductResources(
1579
+ productId: string,
1580
+ ): Promise<ContentResource[] | null> {
1581
+ const contentResourceProductsForProduct = z
1582
+ .array(ContentResourceProductSchema)
1583
+ .nullable()
1584
+ .parse(
1585
+ await client.query.contentResourceProduct.findMany({
1586
+ where: eq(contentResourceProduct.productId, productId),
1587
+ }),
1588
+ )
1589
+
1590
+ if (!contentResourceProductsForProduct) {
1591
+ return null
1592
+ } else {
1593
+ const contentResources = z.array(ContentResourceSchema).parse(
1594
+ await client.query.contentResource.findMany({
1595
+ where: inArray(
1596
+ contentResource.id,
1597
+ contentResourceProductsForProduct.map((crp) => crp.resourceId),
1598
+ ),
1599
+ }),
1600
+ )
1601
+ return contentResources
1602
+ }
1267
1603
  },
1268
1604
  async getPurchaseCountForProduct(productId: string): Promise<number> {
1269
1605
  return await client.query.purchases
@@ -1306,7 +1642,11 @@ export function mySqlDrizzleAdapter(
1306
1642
  )
1307
1643
 
1308
1644
  if (!chargeForPurchase) {
1309
- logger.error(new Error('No charge found for purchase'))
1645
+ logger.error(
1646
+ new Error(
1647
+ `No charge found for purchase: Stripe Charge ID: ${stripeChargeId}`,
1648
+ ),
1649
+ )
1310
1650
  return null
1311
1651
  }
1312
1652
 
@@ -1516,6 +1856,34 @@ export function mySqlDrizzleAdapter(
1516
1856
  purchase: parsedPurchase.data,
1517
1857
  }
1518
1858
  },
1859
+ async getUserWithPurchasersByEmail(email: string): Promise<any | null> {
1860
+ const user = await client.query.users.findFirst({
1861
+ where: eq(users.email, email.trim().toLowerCase()),
1862
+ with: {
1863
+ // merchantCustomer: true,
1864
+ roles: {
1865
+ with: {
1866
+ role: true,
1867
+ },
1868
+ },
1869
+ purchases: {
1870
+ with: {
1871
+ bulkCoupon: true,
1872
+ merchantCharge: true,
1873
+ product: {
1874
+ columns: {
1875
+ id: true,
1876
+ name: true,
1877
+ key: true,
1878
+ },
1879
+ },
1880
+ },
1881
+ },
1882
+ },
1883
+ })
1884
+
1885
+ return user
1886
+ },
1519
1887
  async getUserById(userId: string): Promise<User | null> {
1520
1888
  const user = await client.query.users
1521
1889
  .findFirst({
@@ -1632,17 +2000,156 @@ export function mySqlDrizzleAdapter(
1632
2000
 
1633
2001
  return parsedLessonProgress.data
1634
2002
  },
2003
+ transferPurchaseToUser: async (options: {
2004
+ purchaseId: string
2005
+ targetUserId: string
2006
+ sourceUserId: string
2007
+ }) => {
2008
+ const { purchaseId, targetUserId, sourceUserId } = options
2009
+ const transferId = `put_${v4()}`
2010
+ let purchase = await adapter.getPurchase(purchaseId)
2011
+ if (!purchase) throw new Error('No purchase found')
2012
+
2013
+ if (purchase.userId !== sourceUserId)
2014
+ throw new Error('Invalid source user')
2015
+
2016
+ await client.transaction(async (trx) => {
2017
+ if (!purchase) throw new Error('No purchase found')
2018
+ await trx
2019
+ .update(purchaseTable)
2020
+ .set({ userId: targetUserId })
2021
+ .where(eq(purchaseTable.id, purchase.id))
2022
+
2023
+ await trx.insert(purchaseUserTransfer).values({
2024
+ id: transferId,
2025
+ purchaseId: purchase.id,
2026
+ sourceUserId: sourceUserId,
2027
+ targetUserId: targetUserId,
2028
+ transferState: 'COMPLETED',
2029
+ completedAt: new Date(),
2030
+ })
2031
+
2032
+ purchase = await adapter.getPurchase(purchaseId)
2033
+
2034
+ if (!purchase) throw new Error('No purchase found')
2035
+
2036
+ if (paymentProvider && purchase.merchantChargeId) {
2037
+ await trx
2038
+ .update(merchantCharge)
2039
+ .set({
2040
+ userId: targetUserId,
2041
+ })
2042
+ .where(eq(merchantCharge.id, purchase.merchantChargeId))
2043
+
2044
+ const updatedMerchantCharge = await adapter.getMerchantCharge(
2045
+ purchase.merchantChargeId,
2046
+ )
2047
+
2048
+ if (!updatedMerchantCharge)
2049
+ throw new Error('No merchant charge found')
2050
+
2051
+ await trx
2052
+ .update(merchantCustomer)
2053
+ .set({ userId: targetUserId })
2054
+ .where(
2055
+ eq(merchantCustomer.id, updatedMerchantCharge.merchantCustomerId),
2056
+ )
2057
+
2058
+ const updatedMerchantCustomer = merchantCustomerSchema.parse(
2059
+ await client.query.merchantCustomer.findFirst({
2060
+ where: eq(
2061
+ merchantCustomer.id,
2062
+ updatedMerchantCharge.merchantCustomerId,
2063
+ ),
2064
+ }),
2065
+ )
2066
+
2067
+ const targetUser = userSchema.parse(
2068
+ await client.query.users.findFirst({
2069
+ where: eq(users.id, targetUserId),
2070
+ }),
2071
+ )
2072
+
2073
+ const sourceUser = userSchema.parse(
2074
+ await client.query.users.findFirst({
2075
+ where: eq(users.id, sourceUserId),
2076
+ }),
2077
+ )
2078
+
2079
+ await paymentProvider.updateCustomer(
2080
+ updatedMerchantCustomer.identifier,
2081
+ {
2082
+ email: targetUser.email,
2083
+ name: targetUser.name || sourceUser.name || '',
2084
+ metadata: {
2085
+ transferId,
2086
+ transferredFrom: sourceUser.email,
2087
+ transferredOn: new Date().toISOString(),
2088
+ },
2089
+ },
2090
+ )
2091
+ }
2092
+ })
2093
+ return await adapter.getPurchaseUserTransferById({
2094
+ id: transferId,
2095
+ })
2096
+ },
1635
2097
  transferPurchasesToNewUser(options: {
1636
2098
  merchantCustomerId: string
1637
2099
  userId: string
1638
2100
  }): Promise<unknown> {
1639
2101
  throw new Error('Method not implemented.')
1640
2102
  },
1641
- updatePurchaseStatusForCharge(
2103
+ async updatePurchaseStatusForCharge(
1642
2104
  chargeId: string,
1643
2105
  status: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',
1644
2106
  ): Promise<Purchase | undefined> {
1645
- throw new Error('updatePurchaseStatusForCharge Method not implemented.')
2107
+ try {
2108
+ console.log('updatePurchaseStatusForCharge', { chargeId, status })
2109
+ const merchantChargeForPurchase =
2110
+ await client.query.merchantCharge.findFirst({
2111
+ where: or(
2112
+ eq(merchantCharge.identifier, chargeId),
2113
+ eq(merchantCharge.id, chargeId),
2114
+ ),
2115
+ })
2116
+
2117
+ console.log('merchantChargeForPurchase', { merchantChargeForPurchase })
2118
+
2119
+ const parsedMerchantChargeForPurchase = merchantChargeSchema.parse(
2120
+ merchantChargeForPurchase,
2121
+ )
2122
+
2123
+ if (!parsedMerchantChargeForPurchase)
2124
+ throw new Error(`no-charge-found-for-purchase ${chargeId}`)
2125
+
2126
+ const purchase = await client.query.purchases.findFirst({
2127
+ where: eq(
2128
+ purchaseTable.merchantChargeId,
2129
+ parsedMerchantChargeForPurchase.id,
2130
+ ),
2131
+ })
2132
+
2133
+ const parsedPurchase = purchaseSchema.nullable().parse(purchase)
2134
+
2135
+ if (parsedPurchase) {
2136
+ await client
2137
+ .update(purchaseTable)
2138
+ .set({ status: status })
2139
+ .where(eq(purchaseTable.id, parsedPurchase.id))
2140
+
2141
+ const newPurchase = await client.query.purchases.findFirst({
2142
+ where: eq(purchaseTable.id, parsedPurchase.id),
2143
+ })
2144
+
2145
+ return purchaseSchema.optional().parse(newPurchase)
2146
+ } else {
2147
+ throw new Error(`no-purchase-found-for-charge ${chargeId}`)
2148
+ }
2149
+ } catch (e) {
2150
+ console.log('error updating purchase status', e)
2151
+ throw e
2152
+ }
1646
2153
  },
1647
2154
  async updatePurchaseUserTransferTransferState(options: {
1648
2155
  id: string
@@ -1706,6 +2213,35 @@ export function mySqlDrizzleAdapter(
1706
2213
 
1707
2214
  return parsedResourceJoin.data
1708
2215
  },
2216
+ async removeResourceFromResource(options) {
2217
+ const { childResourceId, parentResourceId } = options
2218
+
2219
+ const resourceJoin = await client.query.contentResourceResource.findFirst(
2220
+ {
2221
+ where: and(
2222
+ eq(contentResourceResource.resourceOfId, parentResourceId),
2223
+ eq(contentResourceResource.resourceId, childResourceId),
2224
+ ),
2225
+ },
2226
+ )
2227
+
2228
+ const parsedResourceJoin =
2229
+ ContentResourceResourceSchema.safeParse(resourceJoin)
2230
+ if (!parsedResourceJoin.success) {
2231
+ return null
2232
+ }
2233
+
2234
+ await client
2235
+ .delete(contentResourceResource)
2236
+ .where(
2237
+ and(
2238
+ eq(contentResourceResource.resourceOfId, parentResourceId),
2239
+ eq(contentResourceResource.resourceId, childResourceId),
2240
+ ),
2241
+ )
2242
+
2243
+ return parsedResourceJoin.data.resource
2244
+ },
1709
2245
  async updateContentResourceFields(options) {
1710
2246
  if (!options.id) {
1711
2247
  throw new Error('No content resource id.')