@coursebuilder/adapter-drizzle 2.1.0 → 3.0.0-canary.0.0c9beb2ae

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 (307) hide show
  1. package/dist/{chunk-KIQLUFJX.js → chunk-2GW3BJE2.js} +2 -2
  2. package/dist/{chunk-M3WM5Q7W.js → chunk-33ZN7FN7.js} +2 -2
  3. package/dist/chunk-4VZJQAPV.js +74 -0
  4. package/dist/chunk-5FLS4ZBX.js +109 -0
  5. package/dist/chunk-5YR5M22S.js +147 -0
  6. package/dist/chunk-6XQ6C3IX.js +18 -0
  7. package/dist/chunk-AIDM2CH6.js +146 -0
  8. package/dist/chunk-ALTODDUX.js +173 -0
  9. package/dist/chunk-AZY2VRAE.js +63 -0
  10. package/dist/chunk-BS7T27VL.js +239 -0
  11. package/dist/chunk-CN3FCRLR.js +55 -0
  12. package/dist/chunk-EAXC4CQD.js +245 -0
  13. package/dist/chunk-EDDG2YFS.js +249 -0
  14. package/dist/chunk-EDKWCMA7.js +210 -0
  15. package/dist/{chunk-CFSRMUNC.js → chunk-EDM6DKA5.js} +2 -2
  16. package/dist/{chunk-EZEQQJNH.js → chunk-EIQBTF5J.js} +2 -2
  17. package/dist/chunk-FYT7RTVW.js +248 -0
  18. package/dist/{chunk-DUFH5L2F.js → chunk-INZTF6QV.js} +2 -2
  19. package/dist/{chunk-3AF6MZQ7.js → chunk-JVHX4JHA.js} +2 -2
  20. package/dist/chunk-LWEKWDLC.js +300 -0
  21. package/dist/chunk-OJPEBGGE.js +139 -0
  22. package/dist/chunk-QKOSV34V.js +16268 -0
  23. package/dist/chunk-QO3QE3LE.js +330 -0
  24. package/dist/chunk-QQKNNFFB.js +399 -0
  25. package/dist/chunk-RBHCV3UW.js +20 -0
  26. package/dist/chunk-TNH2C27F.js +252 -0
  27. package/dist/chunk-TNWNW666.js +133 -0
  28. package/dist/chunk-VBEXYQ6L.js +156 -0
  29. package/dist/chunk-W3ILNMLD.js +265 -0
  30. package/dist/chunk-WJ2ZUXL3.js +71 -0
  31. package/dist/{chunk-4SWTV7Z3.js → chunk-WUMDFFN3.js} +2 -2
  32. package/dist/{chunk-FJGNLVE4.js → chunk-X7DRYXB7.js} +2 -2
  33. package/dist/{chunk-PNPTKVJ4.js → chunk-XUBZKYH6.js} +24 -15
  34. package/dist/{chunk-MYG6PCE3.js → chunk-ZY4AFKUQ.js} +2 -2
  35. package/dist/index.d.ts +5 -11
  36. package/dist/index.js +42 -19
  37. package/dist/lib/mysql/cms-resource-actions.cjs +15932 -4129
  38. package/dist/lib/mysql/cms-resource-actions.cjs.map +1 -1
  39. package/dist/lib/mysql/cms-resource-actions.d.cts +10 -25
  40. package/dist/lib/mysql/cms-resource-actions.d.ts +10 -25
  41. package/dist/lib/mysql/cms-resource-actions.js +2 -1
  42. package/dist/lib/mysql/context.cjs +3380 -0
  43. package/dist/lib/mysql/context.cjs.map +1 -0
  44. package/dist/lib/mysql/context.d.cts +15 -0
  45. package/dist/lib/mysql/context.d.ts +15 -0
  46. package/dist/lib/mysql/context.js +34 -0
  47. package/dist/lib/mysql/context.js.map +1 -0
  48. package/dist/lib/mysql/index.cjs +21648 -15013
  49. package/dist/lib/mysql/index.cjs.map +1 -1
  50. package/dist/lib/mysql/index.d.cts +24 -14300
  51. package/dist/lib/mysql/index.d.ts +24 -14300
  52. package/dist/lib/mysql/index.js +47 -23
  53. package/dist/lib/mysql/ports/auth-storage.cjs +84 -0
  54. package/dist/lib/mysql/ports/auth-storage.cjs.map +1 -0
  55. package/dist/lib/mysql/ports/auth-storage.d.cts +22 -0
  56. package/dist/lib/mysql/ports/auth-storage.d.ts +22 -0
  57. package/dist/lib/mysql/ports/auth-storage.js +8 -0
  58. package/dist/lib/mysql/ports/auth-storage.js.map +1 -0
  59. package/dist/lib/mysql/ports/catalog.cjs +16688 -0
  60. package/dist/lib/mysql/ports/catalog.cjs.map +1 -0
  61. package/dist/lib/mysql/ports/catalog.d.cts +24 -0
  62. package/dist/lib/mysql/ports/catalog.d.ts +24 -0
  63. package/dist/lib/mysql/ports/catalog.js +9 -0
  64. package/dist/lib/mysql/ports/catalog.js.map +1 -0
  65. package/dist/lib/mysql/ports/content-resource.cjs +321 -0
  66. package/dist/lib/mysql/ports/content-resource.cjs.map +1 -0
  67. package/dist/lib/mysql/ports/content-resource.d.cts +23 -0
  68. package/dist/lib/mysql/ports/content-resource.d.ts +23 -0
  69. package/dist/lib/mysql/ports/content-resource.js +8 -0
  70. package/dist/lib/mysql/ports/content-resource.js.map +1 -0
  71. package/dist/lib/mysql/ports/coupon.cjs +16524 -0
  72. package/dist/lib/mysql/ports/coupon.cjs.map +1 -0
  73. package/dist/lib/mysql/ports/coupon.d.cts +22 -0
  74. package/dist/lib/mysql/ports/coupon.d.ts +22 -0
  75. package/dist/lib/mysql/ports/coupon.js +9 -0
  76. package/dist/lib/mysql/ports/coupon.js.map +1 -0
  77. package/dist/lib/mysql/ports/entitlement.cjs +16350 -0
  78. package/dist/lib/mysql/ports/entitlement.cjs.map +1 -0
  79. package/dist/lib/mysql/ports/entitlement.d.cts +22 -0
  80. package/dist/lib/mysql/ports/entitlement.d.ts +22 -0
  81. package/dist/lib/mysql/ports/entitlement.js +9 -0
  82. package/dist/lib/mysql/ports/entitlement.js.map +1 -0
  83. package/dist/lib/mysql/ports/identity.cjs +154 -0
  84. package/dist/lib/mysql/ports/identity.cjs.map +1 -0
  85. package/dist/lib/mysql/ports/identity.d.cts +22 -0
  86. package/dist/lib/mysql/ports/identity.d.ts +22 -0
  87. package/dist/lib/mysql/ports/identity.js +8 -0
  88. package/dist/lib/mysql/ports/identity.js.map +1 -0
  89. package/dist/lib/mysql/ports/merchant.cjs +194 -0
  90. package/dist/lib/mysql/ports/merchant.cjs.map +1 -0
  91. package/dist/lib/mysql/ports/merchant.d.cts +22 -0
  92. package/dist/lib/mysql/ports/merchant.d.ts +22 -0
  93. package/dist/lib/mysql/ports/merchant.js +8 -0
  94. package/dist/lib/mysql/ports/merchant.js.map +1 -0
  95. package/dist/lib/mysql/ports/organization.cjs +16426 -0
  96. package/dist/lib/mysql/ports/organization.cjs.map +1 -0
  97. package/dist/lib/mysql/ports/organization.d.cts +22 -0
  98. package/dist/lib/mysql/ports/organization.d.ts +22 -0
  99. package/dist/lib/mysql/ports/organization.js +9 -0
  100. package/dist/lib/mysql/ports/organization.js.map +1 -0
  101. package/dist/lib/mysql/ports/progress.cjs +16544 -0
  102. package/dist/lib/mysql/ports/progress.cjs.map +1 -0
  103. package/dist/lib/mysql/ports/progress.d.cts +22 -0
  104. package/dist/lib/mysql/ports/progress.d.ts +22 -0
  105. package/dist/lib/mysql/ports/progress.js +9 -0
  106. package/dist/lib/mysql/ports/progress.js.map +1 -0
  107. package/dist/lib/mysql/ports/purchase.cjs +16609 -0
  108. package/dist/lib/mysql/ports/purchase.cjs.map +1 -0
  109. package/dist/lib/mysql/ports/purchase.d.cts +23 -0
  110. package/dist/lib/mysql/ports/purchase.d.ts +23 -0
  111. package/dist/lib/mysql/ports/purchase.js +9 -0
  112. package/dist/lib/mysql/ports/purchase.js.map +1 -0
  113. package/dist/lib/mysql/ports/subscription.cjs +76 -0
  114. package/dist/lib/mysql/ports/subscription.cjs.map +1 -0
  115. package/dist/lib/mysql/ports/subscription.d.cts +22 -0
  116. package/dist/lib/mysql/ports/subscription.d.ts +22 -0
  117. package/dist/lib/mysql/ports/subscription.js +8 -0
  118. package/dist/lib/mysql/ports/subscription.js.map +1 -0
  119. package/dist/lib/mysql/schema.cjs +3373 -0
  120. package/dist/lib/mysql/schema.cjs.map +1 -0
  121. package/dist/lib/mysql/schema.d.cts +14385 -0
  122. package/dist/lib/mysql/schema.d.ts +14385 -0
  123. package/dist/lib/mysql/schema.js +35 -0
  124. package/dist/lib/mysql/schema.js.map +1 -0
  125. package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
  126. package/dist/lib/mysql/schemas/auth/accounts.js +6 -6
  127. package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
  128. package/dist/lib/mysql/schemas/auth/device-access-token.js +7 -7
  129. package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
  130. package/dist/lib/mysql/schemas/auth/device-verification.js +7 -7
  131. package/dist/lib/mysql/schemas/auth/personal-access-token.cjs.map +1 -1
  132. package/dist/lib/mysql/schemas/auth/personal-access-token.js +7 -7
  133. package/dist/lib/mysql/schemas/auth/profiles.cjs.map +1 -1
  134. package/dist/lib/mysql/schemas/auth/profiles.js +6 -6
  135. package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
  136. package/dist/lib/mysql/schemas/auth/role-permissions.js +7 -7
  137. package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
  138. package/dist/lib/mysql/schemas/auth/roles.js +6 -6
  139. package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
  140. package/dist/lib/mysql/schemas/auth/sessions.js +7 -7
  141. package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
  142. package/dist/lib/mysql/schemas/auth/user-permissions.js +6 -6
  143. package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
  144. package/dist/lib/mysql/schemas/auth/user-prefs.js +6 -6
  145. package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
  146. package/dist/lib/mysql/schemas/auth/user-roles.js +6 -6
  147. package/dist/lib/mysql/schemas/auth/users.cjs +5 -1
  148. package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
  149. package/dist/lib/mysql/schemas/auth/users.js +6 -6
  150. package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
  151. package/dist/lib/mysql/schemas/commerce/coupon.js +6 -6
  152. package/dist/lib/mysql/schemas/commerce/merchant-charge.cjs.map +1 -1
  153. package/dist/lib/mysql/schemas/commerce/merchant-charge.js +6 -6
  154. package/dist/lib/mysql/schemas/commerce/merchant-subscription.cjs.map +1 -1
  155. package/dist/lib/mysql/schemas/commerce/merchant-subscription.js +6 -6
  156. package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
  157. package/dist/lib/mysql/schemas/commerce/price.js +6 -6
  158. package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
  159. package/dist/lib/mysql/schemas/commerce/product.js +6 -6
  160. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
  161. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +7 -7
  162. package/dist/lib/mysql/schemas/commerce/purchase.cjs +11 -2
  163. package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
  164. package/dist/lib/mysql/schemas/commerce/purchase.js +6 -6
  165. package/dist/lib/mysql/schemas/commerce/subscription.cjs +6 -1
  166. package/dist/lib/mysql/schemas/commerce/subscription.cjs.map +1 -1
  167. package/dist/lib/mysql/schemas/commerce/subscription.js +6 -6
  168. package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
  169. package/dist/lib/mysql/schemas/commerce/upgradable-products.js +7 -7
  170. package/dist/lib/mysql/schemas/communication/comment.cjs +5 -1
  171. package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
  172. package/dist/lib/mysql/schemas/communication/comment.js +6 -6
  173. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs +5 -1
  174. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
  175. package/dist/lib/mysql/schemas/communication/communication-preferences.js +6 -6
  176. package/dist/lib/mysql/schemas/communication/question-response.cjs.map +1 -1
  177. package/dist/lib/mysql/schemas/communication/question-response.js +7 -7
  178. package/dist/lib/mysql/schemas/content/content-contributions.cjs +5 -1
  179. package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
  180. package/dist/lib/mysql/schemas/content/content-contributions.js +6 -6
  181. package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
  182. package/dist/lib/mysql/schemas/content/content-resource-product.js +6 -6
  183. package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
  184. package/dist/lib/mysql/schemas/content/content-resource-resource.js +6 -6
  185. package/dist/lib/mysql/schemas/content/content-resource-tag.cjs.map +1 -1
  186. package/dist/lib/mysql/schemas/content/content-resource-tag.js +6 -6
  187. package/dist/lib/mysql/schemas/content/content-resource-version.cjs.map +1 -1
  188. package/dist/lib/mysql/schemas/content/content-resource-version.js +6 -6
  189. package/dist/lib/mysql/schemas/content/content-resource.cjs +5 -1
  190. package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
  191. package/dist/lib/mysql/schemas/content/content-resource.js +6 -6
  192. package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -1
  193. package/dist/lib/mysql/schemas/content/tag-tag.js +6 -6
  194. package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -1
  195. package/dist/lib/mysql/schemas/content/tag.js +6 -6
  196. package/dist/lib/mysql/schemas/entitlements/entitlement.cjs +11 -2
  197. package/dist/lib/mysql/schemas/entitlements/entitlement.cjs.map +1 -1
  198. package/dist/lib/mysql/schemas/entitlements/entitlement.js +7 -7
  199. package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs +11 -2
  200. package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs.map +1 -1
  201. package/dist/lib/mysql/schemas/org/organization-membership-roles.js +6 -6
  202. package/dist/lib/mysql/schemas/org/organization-memberships.cjs +11 -2
  203. package/dist/lib/mysql/schemas/org/organization-memberships.cjs.map +1 -1
  204. package/dist/lib/mysql/schemas/org/organization-memberships.d.cts +16 -0
  205. package/dist/lib/mysql/schemas/org/organization-memberships.d.ts +16 -0
  206. package/dist/lib/mysql/schemas/org/organization-memberships.js +6 -6
  207. package/dist/lib/mysql/schemas/org/organizations.cjs +11 -2
  208. package/dist/lib/mysql/schemas/org/organizations.cjs.map +1 -1
  209. package/dist/lib/mysql/schemas/org/organizations.d.cts +16 -0
  210. package/dist/lib/mysql/schemas/org/organizations.d.ts +16 -0
  211. package/dist/lib/mysql/schemas/org/organizations.js +6 -6
  212. package/dist/lib/mysql/workflows/content-resource-workflow-utils.cjs +40 -0
  213. package/dist/lib/mysql/workflows/content-resource-workflow-utils.cjs.map +1 -0
  214. package/dist/lib/mysql/workflows/content-resource-workflow-utils.d.cts +4 -0
  215. package/dist/lib/mysql/workflows/content-resource-workflow-utils.d.ts +4 -0
  216. package/dist/lib/mysql/workflows/content-resource-workflow-utils.js +10 -0
  217. package/dist/lib/mysql/workflows/content-resource-workflow-utils.js.map +1 -0
  218. package/dist/lib/mysql/workflows/create-cohort.cjs +184 -0
  219. package/dist/lib/mysql/workflows/create-cohort.cjs.map +1 -0
  220. package/dist/lib/mysql/workflows/create-cohort.d.cts +24 -0
  221. package/dist/lib/mysql/workflows/create-cohort.d.ts +24 -0
  222. package/dist/lib/mysql/workflows/create-cohort.js +9 -0
  223. package/dist/lib/mysql/workflows/create-cohort.js.map +1 -0
  224. package/dist/lib/mysql/workflows/create-event-series.cjs +283 -0
  225. package/dist/lib/mysql/workflows/create-event-series.cjs.map +1 -0
  226. package/dist/lib/mysql/workflows/create-event-series.d.cts +24 -0
  227. package/dist/lib/mysql/workflows/create-event-series.d.ts +24 -0
  228. package/dist/lib/mysql/workflows/create-event-series.js +9 -0
  229. package/dist/lib/mysql/workflows/create-event-series.js.map +1 -0
  230. package/dist/lib/mysql/workflows/create-event.cjs +194 -0
  231. package/dist/lib/mysql/workflows/create-event.cjs.map +1 -0
  232. package/dist/lib/mysql/workflows/create-event.d.cts +24 -0
  233. package/dist/lib/mysql/workflows/create-event.d.ts +24 -0
  234. package/dist/lib/mysql/workflows/create-event.js +9 -0
  235. package/dist/lib/mysql/workflows/create-event.js.map +1 -0
  236. package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.cjs +231 -0
  237. package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.cjs.map +1 -0
  238. package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.d.cts +22 -0
  239. package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.d.ts +22 -0
  240. package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.js +8 -0
  241. package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.js.map +1 -0
  242. package/dist/lib/mysql/workflows/create-workshop.cjs +286 -0
  243. package/dist/lib/mysql/workflows/create-workshop.cjs.map +1 -0
  244. package/dist/lib/mysql/workflows/create-workshop.d.cts +24 -0
  245. package/dist/lib/mysql/workflows/create-workshop.d.ts +24 -0
  246. package/dist/lib/mysql/workflows/create-workshop.js +9 -0
  247. package/dist/lib/mysql/workflows/create-workshop.js.map +1 -0
  248. package/dist/lib/mysql/workflows/redeem-full-price-coupon.cjs +130 -0
  249. package/dist/lib/mysql/workflows/redeem-full-price-coupon.cjs.map +1 -0
  250. package/dist/lib/mysql/workflows/redeem-full-price-coupon.d.cts +24 -0
  251. package/dist/lib/mysql/workflows/redeem-full-price-coupon.d.ts +24 -0
  252. package/dist/lib/mysql/workflows/redeem-full-price-coupon.js +8 -0
  253. package/dist/lib/mysql/workflows/redeem-full-price-coupon.js.map +1 -0
  254. package/dist/lib/mysql/workflows/transfer-purchase-to-user.cjs +95 -0
  255. package/dist/lib/mysql/workflows/transfer-purchase-to-user.cjs.map +1 -0
  256. package/dist/lib/mysql/workflows/transfer-purchase-to-user.d.cts +30 -0
  257. package/dist/lib/mysql/workflows/transfer-purchase-to-user.d.ts +30 -0
  258. package/dist/lib/mysql/workflows/transfer-purchase-to-user.js +8 -0
  259. package/dist/lib/mysql/workflows/transfer-purchase-to-user.js.map +1 -0
  260. package/dist/lib/utils.d.cts +1 -10
  261. package/dist/lib/utils.d.ts +1 -10
  262. package/dist/types.d.ts +14 -0
  263. package/dist/types.js +1 -0
  264. package/package.json +9 -5
  265. package/src/index.ts +3 -2
  266. package/src/lib/mysql/cms-resource-actions.ts +37 -0
  267. package/src/lib/mysql/context.ts +20 -0
  268. package/src/lib/mysql/index.ts +118 -4872
  269. package/src/lib/mysql/ports/auth-storage.ts +139 -0
  270. package/src/lib/mysql/ports/catalog.ts +610 -0
  271. package/src/lib/mysql/ports/content-resource.ts +442 -0
  272. package/src/lib/mysql/ports/coupon.ts +387 -0
  273. package/src/lib/mysql/ports/entitlement.ts +96 -0
  274. package/src/lib/mysql/ports/identity.ts +160 -0
  275. package/src/lib/mysql/ports/merchant.ts +274 -0
  276. package/src/lib/mysql/ports/organization.ts +216 -0
  277. package/src/lib/mysql/ports/progress.ts +366 -0
  278. package/src/lib/mysql/ports/purchase.ts +517 -0
  279. package/src/lib/mysql/ports/subscription.ts +93 -0
  280. package/src/lib/mysql/schema.ts +374 -0
  281. package/src/lib/mysql/schemas/org/organization-memberships.ts +7 -0
  282. package/src/lib/mysql/schemas/org/organizations.ts +8 -0
  283. package/src/lib/mysql/workflows/content-resource-workflow-utils.ts +22 -0
  284. package/src/lib/mysql/workflows/create-cohort.ts +239 -0
  285. package/src/lib/mysql/workflows/create-event-series.ts +384 -0
  286. package/src/lib/mysql/workflows/create-event.ts +232 -0
  287. package/src/lib/mysql/workflows/create-merchant-charge-and-purchase.ts +365 -0
  288. package/src/lib/mysql/workflows/create-workshop.ts +372 -0
  289. package/src/lib/mysql/workflows/redeem-full-price-coupon.ts +179 -0
  290. package/src/lib/mysql/workflows/transfer-purchase-to-user.ts +142 -0
  291. package/src/types.ts +33 -0
  292. package/dist/authjs.d.ts +0 -27
  293. package/dist/authjs.js +0 -1
  294. package/dist/chunk-25RKHMRM.js +0 -8729
  295. package/dist/chunk-2AW5KYAK.js +0 -101
  296. package/dist/chunk-3EF5ZXLG.js +0 -101
  297. package/dist/chunk-7FBSP3VD.js +0 -78
  298. package/dist/chunk-AP5HBIV4.js +0 -112
  299. package/dist/chunk-F56AI52P.js +0 -49
  300. package/dist/chunk-HAQZSRJL.js +0 -4696
  301. package/dist/chunk-I2U5PTWN.js +0 -79
  302. package/dist/chunk-IBQGXFEX.js +0 -8729
  303. package/dist/chunk-PJORQUQP.js +0 -58
  304. package/dist/chunk-W6RPAAKG.js +0 -2144
  305. package/dist/chunk-WKJTU24D.js +0 -88
  306. package/dist/chunk-WWBSNLRC.js +0 -63
  307. package/src/authjs.ts +0 -34
@@ -1,4894 +1,140 @@
1
- import slugify from '@sindresorhus/slugify'
2
- import { addSeconds, isAfter } from 'date-fns'
3
- import {
4
- and,
5
- asc,
6
- count,
7
- desc,
8
- eq,
9
- gte,
10
- inArray,
11
- isNotNull,
12
- isNull,
13
- not,
14
- or,
15
- sql,
16
- } from 'drizzle-orm'
17
1
  import {
18
2
  mysqlTable as defaultMySqlTableFn,
19
3
  MySqlDatabase,
20
- MySqlTableFn,
4
+ type MySqlTableFn,
21
5
  } from 'drizzle-orm/mysql-core'
22
- import { customAlphabet } from 'nanoid'
23
- import { v4 } from 'uuid'
24
- import { z } from 'zod'
25
-
26
- import type {
27
- CourseBuilderAuthSession,
28
- CourseBuilderUser,
29
- } from '@coursebuilder/core/auth'
30
- import {
31
- parseAndExpandPurchaseBenefitTransport,
32
- PURCHASE_BENEFIT_METADATA_KEY,
33
- } from '@coursebuilder/core/pricing/purchase-benefit-transport'
34
- import {
35
- Coupon,
36
- couponSchema,
37
- Entitlement,
38
- entitlementSchema,
39
- MerchantCharge,
40
- merchantChargeSchema,
41
- MerchantCoupon,
42
- merchantCouponSchema,
43
- MerchantCustomer,
44
- MerchantEvents,
45
- merchantEventsSchema,
46
- merchantPriceSchema,
47
- MerchantProduct,
48
- merchantProductSchema,
49
- NewProduct,
50
- Price,
51
- priceSchema,
52
- Product,
53
- productSchema,
54
- Purchase,
55
- purchaseSchema,
56
- PurchaseUserTransfer,
57
- purchaseUserTransferSchema,
58
- PurchaseUserTransferState,
59
- ResourceProgress,
60
- resourceProgressSchema,
61
- UpgradableProduct,
62
- upgradableProductSchema,
63
- User,
64
- userSchema,
65
- } from '@coursebuilder/core/schemas'
66
- import {
67
- ContentResourceProductSchema,
68
- ContentResourceResourceSchema,
69
- ContentResourceSchema,
70
- type ContentResource,
71
- } from '@coursebuilder/core/schemas/content-resource-schema'
72
- import { merchantAccountSchema } from '@coursebuilder/core/schemas/merchant-account-schema'
73
- import { merchantCustomerSchema } from '@coursebuilder/core/schemas/merchant-customer-schema'
74
- import {
75
- MerchantSession,
76
- MerchantSessionSchema,
77
- } from '@coursebuilder/core/schemas/merchant-session'
78
- import { MerchantSubscriptionSchema } from '@coursebuilder/core/schemas/merchant-subscription'
79
- import { OrganizationMemberSchema } from '@coursebuilder/core/schemas/organization-member'
80
- import { OrganizationSchema } from '@coursebuilder/core/schemas/organization-schema'
81
- import { type ModuleProgress } from '@coursebuilder/core/schemas/resource-progress-schema'
82
- import { SubscriptionSchema } from '@coursebuilder/core/schemas/subscription'
83
- import { VideoResourceSchema } from '@coursebuilder/core/schemas/video-resource'
84
- import { PaymentsProviderConfig } from '@coursebuilder/core/types'
85
- import { logger as baseLogger } from '@coursebuilder/core/utils/logger'
86
- import { validateCoupon } from '@coursebuilder/core/utils/validate-coupon'
87
6
 
88
- import type { AuthJsCourseBuilderAdapter } from '../../authjs.js'
89
- import {
90
- getAccountsRelationsSchema,
91
- getAccountsSchema,
92
- } from './schemas/auth/accounts.js'
93
- import {
94
- getDeviceAccessTokenRelationsSchema,
95
- getDeviceAccessTokenSchema,
96
- } from './schemas/auth/device-access-token.js'
97
- import {
98
- getDeviceVerificationRelationsSchema,
99
- getDeviceVerificationSchema,
100
- } from './schemas/auth/device-verification.js'
101
- import {
102
- getPermissionsRelationsSchema,
103
- getPermissionsSchema,
104
- } from './schemas/auth/permissions.js'
105
- import {
106
- getPersonalAccessTokenRelationsSchema,
107
- getPersonalAccessTokenSchema,
108
- } from './schemas/auth/personal-access-token.js'
109
- import {
110
- getProfilesRelationsSchema,
111
- getProfilesSchema,
112
- } from './schemas/auth/profiles.js'
113
- import {
114
- getRolePermissionsRelationsSchema,
115
- getRolePermissionsSchema,
116
- } from './schemas/auth/role-permissions.js'
117
- import {
118
- getRolesRelationsSchema,
119
- getRolesSchema,
120
- } from './schemas/auth/roles.js'
121
- import {
122
- getSessionRelationsSchema,
123
- getSessionsSchema,
124
- } from './schemas/auth/sessions.js'
125
- import {
126
- getUserPermissionsRelationsSchema,
127
- getUserPermissionsSchema,
128
- } from './schemas/auth/user-permissions.js'
129
- import {
130
- getUserPrefsRelationsSchema,
131
- getUserPrefsSchema,
132
- } from './schemas/auth/user-prefs.js'
133
- import {
134
- getUserRolesRelationsSchema,
135
- getUserRolesSchema,
136
- } from './schemas/auth/user-roles.js'
137
- import {
138
- getUsersRelationsSchema,
139
- getUsersSchema,
140
- type UsersSchemaOverrides,
141
- } from './schemas/auth/users.js'
142
- import { getVerificationTokensSchema } from './schemas/auth/verification-tokens.js'
143
- import {
144
- getCouponRelationsSchema,
145
- getCouponSchema,
146
- } from './schemas/commerce/coupon.js'
147
- import { getMerchantAccountSchema } from './schemas/commerce/merchant-account.js'
148
- import {
149
- getMerchantChargeRelationsSchema,
150
- getMerchantChargeSchema,
151
- } from './schemas/commerce/merchant-charge.js'
152
- import { getMerchantCouponSchema } from './schemas/commerce/merchant-coupon.js'
153
- import { getMerchantCustomerSchema } from './schemas/commerce/merchant-customer.js'
154
- import {
155
- getMerchantEventsRelationsSchema,
156
- getMerchantEventsSchema,
157
- } from './schemas/commerce/merchant-events.js'
158
- import { getMerchantPriceSchema } from './schemas/commerce/merchant-price.js'
159
- import { getMerchantProductSchema } from './schemas/commerce/merchant-product.js'
160
- import { getMerchantSessionSchema } from './schemas/commerce/merchant-session.js'
161
- import {
162
- getMerchantSubscriptionRelationsSchema,
163
- getMerchantSubscriptionSchema,
164
- } from './schemas/commerce/merchant-subscription.js'
165
- import { getPriceSchema } from './schemas/commerce/price.js'
166
- import {
167
- getProductRelationsSchema,
168
- getProductSchema,
169
- } from './schemas/commerce/product.js'
170
- import {
171
- getPurchaseUserTransferRelationsSchema,
172
- getPurchaseUserTransferSchema,
173
- } from './schemas/commerce/purchase-user-transfer.js'
174
- import {
175
- getPurchaseRelationsSchema,
176
- getPurchaseSchema,
177
- } from './schemas/commerce/purchase.js'
178
- import {
179
- getSubscriptionRelationsSchema,
180
- getSubscriptionSchema,
181
- } from './schemas/commerce/subscription.js'
182
- import {
183
- getUpgradableProductsRelationsSchema,
184
- getUpgradableProductsSchema,
185
- } from './schemas/commerce/upgradable-products.js'
186
- import {
187
- getCommentRelationsSchema,
188
- getCommentsSchema,
189
- } from './schemas/communication/comment.js'
190
- import { getCommunicationChannelSchema } from './schemas/communication/communication-channel.js'
191
- import { getCommunicationPreferenceTypesSchema } from './schemas/communication/communication-preference-types.js'
192
- import {
193
- getCommunicationPreferencesRelationsSchema,
194
- getCommunicationPreferencesSchema,
195
- } from './schemas/communication/communication-preferences.js'
196
- import {
197
- getQuestionResponseRelationsSchema,
198
- getQuestionResponseSchema,
199
- } from './schemas/communication/question-response.js'
200
- import {
201
- getContentContributionRelationsSchema,
202
- getContentContributionsSchema,
203
- } from './schemas/content/content-contributions.js'
204
- import {
205
- getContentResourceProductRelationsSchema,
206
- getContentResourceProductSchema,
207
- } from './schemas/content/content-resource-product.js'
208
- import {
209
- getContentResourceResourceRelationsSchema,
210
- getContentResourceResourceSchema,
211
- } from './schemas/content/content-resource-resource.js'
212
- import {
213
- getContentResourceTagRelationsSchema,
214
- getContentResourceTagSchema,
215
- } from './schemas/content/content-resource-tag.js'
216
- import {
217
- getContentResourceVersionRelationsSchema,
218
- getContentResourceVersionSchema,
219
- } from './schemas/content/content-resource-version.js'
220
- import {
221
- getContentResourceRelationsSchema,
222
- getContentResourceSchema,
223
- } from './schemas/content/content-resource.js'
224
- import {
225
- getContributionTypesRelationsSchema,
226
- getContributionTypesSchema,
227
- } from './schemas/content/contribution-types.js'
228
- import { getLessonProgressSchema } from './schemas/content/lesson-progress.js'
229
- import { getResourceProgressSchema } from './schemas/content/resource-progress.js'
230
- import {
231
- getTagTagRelationsSchema,
232
- getTagTagSchema,
233
- } from './schemas/content/tag-tag.js'
234
- import { getTagRelationsSchema, getTagSchema } from './schemas/content/tag.js'
235
- import { getEntitlementTypesSchema } from './schemas/entitlements/entitlement-type.js'
236
- import {
237
- getEntitlementRelationsSchema,
238
- getEntitlementsSchema,
239
- } from './schemas/entitlements/entitlement.js'
240
- import {
241
- getOrganizationMembershipRolesRelationsSchema,
242
- getOrganizationMembershipRolesSchema,
243
- } from './schemas/org/organization-membership-roles.js'
244
- import {
245
- getOrganizationMembershipsRelationsSchema,
246
- getOrganizationMembershipsSchema,
247
- } from './schemas/org/organization-memberships.js'
248
- import {
249
- getOrganizationsRelationsSchema,
250
- getOrganizationsSchema,
251
- } from './schemas/org/organizations.js'
7
+ import type { PaymentsProviderConfig } from '@coursebuilder/core/types'
8
+
9
+ import type { DrizzleAdapterComposition } from '../../types.js'
10
+ import { createMySqlContext } from './context.js'
11
+ import { createAuthStoragePort } from './ports/auth-storage.js'
12
+ import { createCatalogPort } from './ports/catalog.js'
13
+ import { createContentResourcePort } from './ports/content-resource.js'
14
+ import { createCouponPort } from './ports/coupon.js'
15
+ import { createEntitlementPort } from './ports/entitlement.js'
16
+ import { createIdentityPort } from './ports/identity.js'
17
+ import { createMerchantPort } from './ports/merchant.js'
18
+ import { createOrganizationPort } from './ports/organization.js'
19
+ import { createProgressPort } from './ports/progress.js'
20
+ import { createPurchasePort } from './ports/purchase.js'
21
+ import { createSubscriptionPort } from './ports/subscription.js'
22
+ import { createCohortWorkflow } from './workflows/create-cohort.js'
23
+ import { createEventSeriesWorkflow } from './workflows/create-event-series.js'
24
+ import { createEventWorkflow } from './workflows/create-event.js'
25
+ import { createMerchantChargeAndPurchaseWorkflow } from './workflows/create-merchant-charge-and-purchase.js'
26
+ import { createWorkshopWorkflow } from './workflows/create-workshop.js'
27
+ import { createRedeemFullPriceCouponWorkflow } from './workflows/redeem-full-price-coupon.js'
28
+ import { createTransferPurchaseToUserWorkflow } from './workflows/transfer-purchase-to-user.js'
252
29
 
253
30
  export * from './cms-resource-actions.js'
31
+ export * from './schema.js'
254
32
 
255
- export const guid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 5)
256
-
257
- export const normalizeExpirationDate = (date: Date | undefined) => {
258
- if (!date) return undefined
259
-
260
- // Create a new Date object for 23:59:59 UTC on the date part of the input date
261
- // The input date should be a JS Date representing 00:00:00 LA time for the chosen day.
262
- // Its UTC date parts (getUTCFullYear, etc.) will give us the correct calendar day.
263
- return new Date(
264
- Date.UTC(
265
- date.getUTCFullYear(),
266
- date.getUTCMonth(), // 0-indexed
267
- date.getUTCDate(),
268
- 23, // hours
269
- 59, // minutes
270
- 59, // seconds
271
- 0, // milliseconds
272
- ),
273
- )
274
- }
275
-
276
- function toError(error: unknown) {
277
- return error instanceof Error ? error : new Error(String(error))
278
- }
279
-
280
- const logger = baseLogger as unknown as {
281
- debug: (message: string, attrs?: Record<string, unknown>) => void
282
- error: (error: Error, attrs?: Record<string, unknown>) => void
283
- info: (message: string, attrs?: Record<string, unknown>) => void
284
- }
285
-
286
- type PurchaseBenefitExpansion = ReturnType<
287
- typeof parseAndExpandPurchaseBenefitTransport
288
- > | null
289
-
290
- function mergePurchaseBenefitFields(
291
- fields: Record<string, any>,
292
- expansion: PurchaseBenefitExpansion,
293
- ) {
294
- if (!expansion) return fields
295
-
296
- return {
297
- ...fields,
298
- ...(expansion.success
299
- ? { purchaseBenefits: expansion.benefits }
300
- : { purchaseBenefitsReview: expansion.review }),
301
- }
302
- }
303
-
304
- export type CourseBuilderSchemaOverrides<
305
- TUsers extends ReturnType<typeof getUsersSchema> = ReturnType<
306
- typeof getUsersSchema
307
- >,
308
- > = UsersSchemaOverrides<TUsers>
309
-
310
- export function getCourseBuilderSchema<
311
- TUsers extends ReturnType<typeof getUsersSchema> = ReturnType<
312
- typeof getUsersSchema
313
- >,
314
- >(
315
- mysqlTable: MySqlTableFn,
316
- overrides: CourseBuilderSchemaOverrides<TUsers> = {},
317
- ) {
318
- const resolvedGetUsersSchema = (overrides.getUsersSchema ??
319
- getUsersSchema) as (mysqlTable: MySqlTableFn) => TUsers
320
- const userRelationsOverrides: UsersSchemaOverrides<TUsers> = {
321
- getUsersSchema: resolvedGetUsersSchema,
322
- }
323
- return {
324
- accounts: getAccountsSchema(mysqlTable),
325
- accountsRelations: getAccountsRelationsSchema(
326
- mysqlTable,
327
- userRelationsOverrides,
328
- ),
329
- permissions: getPermissionsSchema(mysqlTable),
330
- permissionsRelations: getPermissionsRelationsSchema(mysqlTable),
331
- rolePermissions: getRolePermissionsSchema(mysqlTable),
332
- rolePermissionsRelations: getRolePermissionsRelationsSchema(mysqlTable),
333
- roles: getRolesSchema(mysqlTable),
334
- rolesRelations: getRolesRelationsSchema(mysqlTable),
335
- sessions: getSessionsSchema(mysqlTable),
336
- sessionsRelations: getSessionRelationsSchema(
337
- mysqlTable,
338
- userRelationsOverrides,
339
- ),
340
- userPermissions: getUserPermissionsSchema(mysqlTable),
341
- userPermissionsRelations: getUserPermissionsRelationsSchema(
342
- mysqlTable,
343
- userRelationsOverrides,
344
- ),
345
- userRoles: getUserRolesSchema(mysqlTable),
346
- userRolesRelations: getUserRolesRelationsSchema(
347
- mysqlTable,
348
- userRelationsOverrides,
349
- ),
350
- users: resolvedGetUsersSchema(mysqlTable),
351
- usersRelations: getUsersRelationsSchema(mysqlTable, userRelationsOverrides),
352
- verificationTokens: getVerificationTokensSchema(mysqlTable),
353
- coupon: getCouponSchema(mysqlTable),
354
- couponRelations: getCouponRelationsSchema(mysqlTable),
355
- lessonProgress: getLessonProgressSchema(mysqlTable),
356
- merchantAccount: getMerchantAccountSchema(mysqlTable),
357
- merchantCharge: getMerchantChargeSchema(mysqlTable),
358
- merchantChargeRelations: getMerchantChargeRelationsSchema(mysqlTable),
359
- merchantEvents: getMerchantEventsSchema(mysqlTable),
360
- merchantEventsRelations: getMerchantEventsRelationsSchema(mysqlTable),
361
- merchantCoupon: getMerchantCouponSchema(mysqlTable),
362
- merchantCustomer: getMerchantCustomerSchema(mysqlTable),
363
- merchantPrice: getMerchantPriceSchema(mysqlTable),
364
- merchantProduct: getMerchantProductSchema(mysqlTable),
365
- merchantSession: getMerchantSessionSchema(mysqlTable),
366
- prices: getPriceSchema(mysqlTable),
367
- products: getProductSchema(mysqlTable),
368
- purchases: getPurchaseSchema(mysqlTable),
369
- purchaseRelations: getPurchaseRelationsSchema(
370
- mysqlTable,
371
- userRelationsOverrides,
372
- ),
373
- purchaseUserTransfer: getPurchaseUserTransferSchema(mysqlTable),
374
- purchaseUserTransferRelations: getPurchaseUserTransferRelationsSchema(
375
- mysqlTable,
376
- userRelationsOverrides,
377
- ),
378
- communicationChannel: getCommunicationChannelSchema(mysqlTable),
379
- communicationPreferenceTypes:
380
- getCommunicationPreferenceTypesSchema(mysqlTable),
381
- communicationPreferences: getCommunicationPreferencesSchema(mysqlTable),
382
- communicationPreferencesRelations:
383
- getCommunicationPreferencesRelationsSchema(
384
- mysqlTable,
385
- userRelationsOverrides,
386
- ),
387
- contentContributions: getContentContributionsSchema(mysqlTable),
388
- contentContributionRelations: getContentContributionRelationsSchema(
389
- mysqlTable,
390
- userRelationsOverrides,
391
- ),
392
- contentResource: getContentResourceSchema(mysqlTable),
393
- contentResourceVersion: getContentResourceVersionSchema(mysqlTable),
394
- contentResourceVersionRelations: getContentResourceVersionRelationsSchema(
395
- mysqlTable,
396
- userRelationsOverrides,
397
- ),
398
- contentResourceRelations: getContentResourceRelationsSchema(
399
- mysqlTable,
400
- userRelationsOverrides,
401
- ),
402
- contentResourceResource: getContentResourceResourceSchema(mysqlTable),
403
- contentResourceResourceRelations:
404
- getContentResourceResourceRelationsSchema(mysqlTable),
405
- contentResourceTag: getContentResourceTagSchema(mysqlTable),
406
- contentResourceTagRelations:
407
- getContentResourceTagRelationsSchema(mysqlTable),
408
- contributionTypes: getContributionTypesSchema(mysqlTable),
409
- contributionTypesRelations: getContributionTypesRelationsSchema(mysqlTable),
410
- resourceProgress: getResourceProgressSchema(mysqlTable),
411
- questionResponse: getQuestionResponseSchema(mysqlTable),
412
- questionResponseRelations: getQuestionResponseRelationsSchema(
413
- mysqlTable,
414
- userRelationsOverrides,
415
- ),
416
- upgradableProducts: getUpgradableProductsSchema(mysqlTable),
417
- upgradableProductsRelations:
418
- getUpgradableProductsRelationsSchema(mysqlTable),
419
- contentResourceProduct: getContentResourceProductSchema(mysqlTable),
420
- contentResourceProductRelations:
421
- getContentResourceProductRelationsSchema(mysqlTable),
422
- productRelations: getProductRelationsSchema(mysqlTable),
423
- comments: getCommentsSchema(mysqlTable),
424
- commentsRelations: getCommentRelationsSchema(
425
- mysqlTable,
426
- userRelationsOverrides,
427
- ),
428
- deviceVerifications: getDeviceVerificationSchema(mysqlTable),
429
- deviceVerificationRelations: getDeviceVerificationRelationsSchema(
430
- mysqlTable,
431
- userRelationsOverrides,
432
- ),
433
- deviceAccessToken: getDeviceAccessTokenSchema(mysqlTable),
434
- deviceAccessTokenRelations: getDeviceAccessTokenRelationsSchema(
435
- mysqlTable,
436
- userRelationsOverrides,
437
- ),
438
- personalAccessToken: getPersonalAccessTokenSchema(mysqlTable),
439
- personalAccessTokenRelations: getPersonalAccessTokenRelationsSchema(
440
- mysqlTable,
441
- userRelationsOverrides,
442
- ),
443
- tag: getTagSchema(mysqlTable),
444
- tagRelations: getTagRelationsSchema(mysqlTable),
445
- tagTag: getTagTagSchema(mysqlTable),
446
- tagTagRelations: getTagTagRelationsSchema(mysqlTable),
447
- userPrefs: getUserPrefsSchema(mysqlTable),
448
- userPrefsRelations: getUserPrefsRelationsSchema(
449
- mysqlTable,
450
- userRelationsOverrides,
451
- ),
452
- organization: getOrganizationsSchema(mysqlTable),
453
- organizationRelations: getOrganizationsRelationsSchema(mysqlTable),
454
- organizationMemberships: getOrganizationMembershipsSchema(mysqlTable),
455
- organizationMembershipRelations: getOrganizationMembershipsRelationsSchema(
456
- mysqlTable,
457
- userRelationsOverrides,
458
- ),
459
- organizationMembershipRoles:
460
- getOrganizationMembershipRolesSchema(mysqlTable),
461
- organizationMembershipRolesRelations:
462
- getOrganizationMembershipRolesRelationsSchema(mysqlTable),
463
- merchantSubscription: getMerchantSubscriptionSchema(mysqlTable),
464
- merchantSubscriptionRelations:
465
- getMerchantSubscriptionRelationsSchema(mysqlTable),
466
- subscription: getSubscriptionSchema(mysqlTable),
467
- subscriptionRelations: getSubscriptionRelationsSchema(mysqlTable),
468
- profiles: getProfilesSchema(mysqlTable),
469
- profilesRelations: getProfilesRelationsSchema(
470
- mysqlTable,
471
- userRelationsOverrides,
472
- ),
473
- entitlementTypes: getEntitlementTypesSchema(mysqlTable),
474
- entitlements: getEntitlementsSchema(mysqlTable),
475
- entitlementsRelations: getEntitlementRelationsSchema(
476
- mysqlTable,
477
- userRelationsOverrides,
478
- ),
479
- } as const
480
- }
481
-
482
- export function createTables(mySqlTable: MySqlTableFn) {
483
- return getCourseBuilderSchema(mySqlTable)
484
- }
485
-
486
- export type DefaultSchema = ReturnType<typeof createTables>
33
+ export {
34
+ guid,
35
+ normalizeExpirationDate,
36
+ } from './workflows/content-resource-workflow-utils.js'
487
37
 
488
38
  /**
489
- * Provisions the personal organization every Course Builder user owns:
490
- * creates the organization, adds the user as a member, and grants the owner
491
- * role. Exposed on the adapter as the UserProvisioningPort contract from
492
- * @coursebuilder/core (ADR-0012 section 3).
39
+ * Composes the legacy combined MySQL adapter from capability ports and private
40
+ * cross-port workflows.
41
+ *
42
+ * @param client - The Drizzle MySQL client retained on the compatibility adapter.
43
+ * @param tableFn - Optional table factory used to construct one shared schema.
44
+ * @param paymentProvider - Optional payment-provider configuration.
45
+ * @returns The compatibility adapter implementing all capability and Auth.js methods.
493
46
  *
494
- * MIGRATION BRIDGE (ADR-0012 section 6): createUser still invokes this from
495
- * inside the Auth.js storage hook, and 7 apps carry duplicate logic in
496
- * src/lib/personal-organization-service.ts. Phase 3 must move the trigger to
497
- * the app auth boundary (NextAuth createUser event / signIn callback) and
498
- * delete the app copies. Removal criteria: no call sites inside createUser
499
- * and zero personal-organization-service.ts files under apps/.
500
- * Do not add new callers of the inline-hook path.
47
+ * @example
48
+ * ```ts
49
+ * const adapter = mySqlDrizzleAdapter(client)
50
+ * ```
501
51
  */
502
- async function provisionPersonalOrganization(
503
- adapter: AuthJsCourseBuilderAdapter<typeof MySqlDatabase>,
504
- params: { userId: string; email: string },
505
- ): Promise<void> {
506
- const { userId, email } = params
507
-
508
- // creating a personal organization for the user
509
- // every user gets an organization of their very own
510
- const personalOrganization = await adapter.createOrganization({
511
- name: `Personal (${email})`,
512
- })
513
-
514
- if (!personalOrganization) {
515
- throw new Error('Failed to create personal organization')
516
- }
517
-
518
- const membership = await adapter.addMemberToOrganization({
519
- organizationId: personalOrganization.id,
520
- userId: userId,
521
- invitedById: userId,
522
- })
523
-
524
- if (!membership) {
525
- throw new Error('Failed to add user to personal organization')
526
- }
527
-
528
- await adapter.addRoleForMember({
529
- organizationId: personalOrganization.id,
530
- memberId: membership.id,
531
- role: 'owner',
532
- })
533
- }
534
-
535
52
  export function mySqlDrizzleAdapter(
536
53
  client: InstanceType<typeof MySqlDatabase>,
537
- tableFn = defaultMySqlTableFn,
54
+ tableFn: MySqlTableFn = defaultMySqlTableFn,
538
55
  paymentProvider?: PaymentsProviderConfig,
539
- ): AuthJsCourseBuilderAdapter<typeof MySqlDatabase> {
540
- const {
541
- users,
542
- accounts,
543
- sessions,
544
- verificationTokens,
545
- contentResource,
546
- contentResourceResource,
547
- contentResourceProduct,
548
- contentResourceTag,
549
- purchases: purchaseTable,
550
- purchaseUserTransfer,
551
- coupon,
552
- merchantCoupon,
553
- merchantCharge,
554
- merchantAccount,
555
- merchantEvents,
556
- merchantPrice,
557
- merchantCustomer,
558
- merchantSession,
559
- merchantProduct,
560
- prices,
561
- products,
562
- upgradableProducts,
563
- resourceProgress,
564
- comments,
565
- organization: organizationTable,
566
- organizationMemberships: organizationMembershipTable,
567
- organizationMembershipRoles: organizationMembershipRoleTable,
568
- roles: roleTable,
569
- merchantSubscription: merchantSubscriptionTable,
570
- subscription: subscriptionTable,
571
- entitlements: entitlementTable,
572
- entitlementTypes,
573
- } = createTables(tableFn)
574
-
575
- const normalizeLearnerEmail = (email: string) =>
576
- email.trim().toLowerCase().replace(/\s+/g, '+')
577
-
578
- const adapter: AuthJsCourseBuilderAdapter<typeof MySqlDatabase> = {
579
- client,
580
- async provisionPersonalOrganization(params) {
581
- await provisionPersonalOrganization(adapter, params)
582
- },
583
- async redeemFullPriceCoupon(options) {
584
- const {
585
- email: baseEmail,
586
- couponId,
587
- productIds,
588
- currentUserId,
589
- redeemingProductId,
590
- } = options
591
- const email = normalizeLearnerEmail(String(baseEmail))
592
-
593
- const coupon = await adapter.getCouponWithBulkPurchases(couponId)
594
-
595
- const productId =
596
- (coupon && (coupon.restrictedToProductId as string)) ||
597
- redeemingProductId
598
-
599
- if (!productId) throw new Error(`unable-to-find-any-product-id`)
600
-
601
- const couponValidation = validateCoupon(coupon, productIds)
602
-
603
- if (coupon && couponValidation.isRedeemable) {
604
- // if the Coupon is the Bulk Coupon of a Bulk Purchase,
605
- // then a bulk coupon is being redeemed
606
- const bulkCouponRedemption = Boolean(coupon.maxUses > 1)
607
-
608
- const { user } = await adapter.findOrCreateUser(email)
609
-
610
- if (!user) throw new Error(`unable-to-create-user-${email}`)
611
-
612
- const currentUser = currentUserId
613
- ? await adapter.getUserById(currentUserId)
614
- : null
615
-
616
- const redeemingForCurrentUser = currentUser?.id === user.id
617
-
618
- if (bulkCouponRedemption) {
619
- const existingValidRedemption =
620
- await client.query.purchases.findFirst({
621
- where: and(
622
- eq(purchaseTable.redeemedBulkCouponId, coupon.id),
623
- eq(purchaseTable.userId, user.id),
624
- eq(purchaseTable.status, 'Valid'),
625
- ),
626
- })
627
-
628
- if (existingValidRedemption) {
629
- logger.info('adapter.coupon.redeem.duplicate_team_seat', {
630
- couponId: coupon.id,
631
- currentUserId,
632
- email,
633
- productId,
634
- purchaseId: existingValidRedemption.id,
635
- userId: user.id,
636
- })
637
- return {
638
- error: {
639
- message: 'team-seat-redemption-already-claimed',
640
- },
641
- redeemingForCurrentUser,
642
- purchase: null,
643
- }
644
- }
645
- }
646
-
647
- // To prevent double-purchasing, check if this user already has a
648
- // Purchase record for this product that is valid and wasn't a bulk
649
- // coupon purchase.
650
- const existingPurchases =
651
- await adapter.getExistingNonBulkValidPurchasesOfProduct({
652
- userId: user.id,
653
- productId,
654
- })
655
-
656
- if (existingPurchases.length > 0) {
657
- const errorMessage = `already-purchased-${email}`
658
- logger.info('adapter.coupon.redeem.already_purchased', {
659
- couponId: coupon.id,
660
- currentUserId,
661
- email,
662
- productId,
663
- userId: user.id,
664
- })
665
- return {
666
- error: {
667
- message: errorMessage,
668
- },
669
- redeemingForCurrentUser,
670
- purchase: null,
671
- }
672
- throw new Error(errorMessage)
673
- }
674
-
675
- const purchaseId = `purchase-${v4()}`
676
- const userMemberships = await adapter.getMembershipsForUser(user.id)
677
- const organizationId =
678
- coupon.organizationId ||
679
- userMemberships.find((m) => m.organization.name?.includes(user.email))
680
- ?.organizationId // safer way to make sure we are using personal organization
681
-
682
- await adapter.createPurchase({
683
- id: purchaseId,
684
- userId: user.id,
685
- couponId: bulkCouponRedemption ? null : coupon.id,
686
- redeemedBulkCouponId: bulkCouponRedemption ? coupon.id : null,
687
- productId,
688
- totalAmount: '0',
689
- organizationId,
690
- metadata: {
691
- couponUsedId: bulkCouponRedemption ? null : coupon.id,
692
- },
693
- })
694
-
695
- const newPurchase = await adapter.getPurchase(purchaseId)
696
-
697
- await adapter.incrementCouponUsedCount(coupon.id)
698
-
699
- await adapter.createPurchaseTransfer({
700
- sourceUserId: user.id,
701
- purchaseId: purchaseId,
702
- expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
703
- })
704
-
705
- return { purchase: newPurchase, redeemingForCurrentUser }
706
- }
707
-
708
- return null
709
- },
710
- createPurchaseTransfer: async (options) => {
711
- const id = `put_${v4()}`
712
- await client.insert(purchaseUserTransfer).values({
713
- id,
714
- purchaseId: options.purchaseId,
715
- sourceUserId: options.sourceUserId,
716
- expiresAt: options.expiresAt,
717
- })
718
- },
719
- incrementCouponUsedCount: async (couponId) => {
720
- await client
721
- .update(coupon)
722
- .set({ usedCount: sql`${coupon.usedCount} + 1` })
723
- .where(eq(coupon.id, couponId))
724
- },
725
- getExistingNonBulkValidPurchasesOfProduct: async ({
726
- userId,
727
- productId,
728
- }) => {
729
- const existingPurchases = await client.query.purchases.findMany({
730
- where: and(
731
- eq(purchaseTable.userId, userId),
732
- productId ? eq(purchaseTable.productId, productId) : undefined,
733
- eq(purchaseTable.status, 'Valid'),
734
- isNull(purchaseTable.bulkCouponId),
735
- ),
736
- })
737
-
738
- return z.array(purchaseSchema).parse(existingPurchases)
739
- },
740
- createMerchantCustomer: async (options) => {
741
- await client.insert(merchantCustomer).values({
742
- id: `mc_${v4()}`,
743
- identifier: options.identifier,
744
- merchantAccountId: options.merchantAccountId,
745
- userId: options.userId,
746
- status: 1,
747
- })
748
- return merchantCustomerSchema.parse(
749
- await client.query.merchantCustomer.findFirst({
750
- where: eq(merchantCustomer.identifier, options.identifier),
751
- }),
752
- )
753
- },
754
- createMerchantCoupon: async (options) => {
755
- await client.insert(merchantCoupon).values({
756
- id: `mcoupon_${v4()}`,
757
- identifier: options.identifier,
758
- merchantAccountId: options.merchantAccountId,
759
- type: options.type,
760
- amountDiscount: Math.floor(options.amountDiscount),
761
- status: 1,
762
- })
763
- return merchantCouponSchema.parse(
764
- await client.query.merchantCoupon.findFirst({
765
- where: eq(merchantCoupon.identifier, options.identifier),
766
- }),
767
- )
768
- },
769
- getMerchantAccount: async (options) => {
770
- return merchantAccountSchema.parse(
771
- await client.query.merchantAccount.findFirst({
772
- where: eq(merchantAccount.label, options.provider),
773
- }),
774
- )
775
- },
776
- getMerchantPriceForProductId: async (productId) => {
777
- const merchantPriceData = await client.query.merchantPrice.findFirst({
778
- where: and(
779
- eq(merchantPrice.merchantProductId, productId),
780
- eq(merchantPrice.status, 1),
781
- ),
782
- })
783
-
784
- const parsedMerchantPrice =
785
- merchantPriceSchema.safeParse(merchantPriceData)
786
-
787
- if (!parsedMerchantPrice.success) {
788
- logger.error(parsedMerchantPrice.error, {
789
- event: 'adapter.merchantPrice.parse_failed',
790
- productId,
791
- })
792
- return null
793
- }
794
-
795
- return parsedMerchantPrice.data
796
- },
797
- getMerchantProductForProductId: async (productId) => {
798
- const merchantProductData = await client.query.merchantProduct.findFirst({
799
- where: eq(merchantProduct.productId, productId),
800
- })
801
-
802
- if (!merchantProductData) return null
803
- return merchantProductSchema.parse(merchantProductData)
804
- },
805
- getMerchantCustomerForUserId: async (userId) => {
806
- const merchantCustomerData =
807
- await client.query.merchantCustomer.findFirst({
808
- where: eq(merchantCustomer.userId, userId),
809
- })
810
-
811
- if (!merchantCustomerData) return null
812
- return merchantCustomerSchema.parse(merchantCustomerData)
813
- },
814
- getUpgradableProducts: async (options) => {
815
- const { upgradableFromId, upgradableToId } = options
816
- return z.array(upgradableProductSchema).parse(
817
- await client.query.upgradableProducts.findMany({
818
- where: and(
819
- eq(upgradableProducts.upgradableFromId, upgradableFromId),
820
- eq(upgradableProducts.upgradableToId, upgradableToId),
821
- ),
822
- }),
823
- )
824
- },
825
- async availableUpgradesForProduct(
826
- purchases: any,
827
- productId: string,
828
- ): Promise<any[]> {
829
- const previousPurchaseProductIds = purchases.map(
830
- ({ productId }: Purchase) => productId,
831
- )
832
-
833
- if (previousPurchaseProductIds.length > 0) {
834
- return client.query.upgradableProducts.findMany({
835
- where: and(
836
- eq(upgradableProducts.upgradableToId, productId),
837
- inArray(
838
- upgradableProducts.upgradableFromId,
839
- previousPurchaseProductIds,
840
- ),
841
- ),
842
- })
843
- }
844
- return []
845
- },
846
- clearLessonProgressForUser(options: {
847
- userId: string
848
- lessons: { id: string; slug: string }[]
849
- }): Promise<void> {
850
- throw new Error('clearLessonProgressForUser Method not implemented.')
851
- },
852
- async completeLessonProgressForUser(options: {
853
- userId: string
854
- lessonId?: string
855
- }): Promise<ResourceProgress | null> {
856
- if (!options.lessonId) {
857
- throw new Error('No lessonId provided')
858
- }
859
-
860
- let lessonProgress = await client.query.resourceProgress.findFirst({
861
- where: and(
862
- eq(resourceProgress.userId, options.userId),
863
- eq(resourceProgress.resourceId, options.lessonId),
864
- ),
865
- })
866
-
867
- const now = new Date()
868
-
869
- if (lessonProgress) {
870
- if (!lessonProgress.completedAt) {
871
- await client
872
- .update(resourceProgress)
873
- .set({
874
- completedAt: now,
875
- updatedAt: now,
876
- })
877
- .where(
878
- and(
879
- eq(resourceProgress.userId, options.userId),
880
- eq(resourceProgress.resourceId, options.lessonId),
881
- ),
882
- )
883
- }
884
- } else {
885
- await client.insert(resourceProgress).values({
886
- userId: options.userId,
887
- resourceId: options.lessonId,
888
- completedAt: now,
889
- updatedAt: now,
890
- })
891
- }
892
- lessonProgress = await client.query.resourceProgress.findFirst({
893
- where: and(
894
- eq(resourceProgress.userId, options.userId),
895
- eq(resourceProgress.resourceId, options.lessonId),
896
- ),
897
- })
898
- const parsedLessonProgress =
899
- resourceProgressSchema.safeParse(lessonProgress)
900
-
901
- if (!parsedLessonProgress.success) {
902
- logger.error(parsedLessonProgress.error, {
903
- event: 'adapter.lessonProgress.parse_failed',
904
- lessonId: options.lessonId,
905
- userId: options.userId,
906
- })
907
- return null
908
- }
909
-
910
- return parsedLessonProgress.data
911
- },
912
- async couponForIdOrCode(options: {
913
- code?: string
914
- couponId?: string
915
- }): Promise<(Coupon & { merchantCoupon: MerchantCoupon }) | null> {
916
- if (!options.couponId && !options.code) return null
917
- const couponForIdOrCode = await client.query.coupon.findFirst({
918
- where: or(
919
- and(
920
- or(
921
- options.code ? eq(coupon.code, options.code) : undefined,
922
- options.couponId ? eq(coupon.id, options.couponId) : undefined,
923
- ),
924
- gte(coupon.expires, new Date()),
925
- ),
926
- and(
927
- or(
928
- options.code ? eq(coupon.code, options.code) : undefined,
929
- options.couponId ? eq(coupon.id, options.couponId) : undefined,
930
- ),
931
- isNull(coupon.expires),
932
- ),
933
- ),
934
- with: {
935
- merchantCoupon: true,
936
- },
937
- })
938
-
939
- if (!couponForIdOrCode) return null
940
-
941
- const parsedCoupon = couponSchema
942
- .extend({
943
- merchantCoupon: merchantCouponSchema,
944
- })
945
- .safeParse(couponForIdOrCode)
946
-
947
- if (!parsedCoupon.success) {
948
- logger.error(parsedCoupon.error, {
949
- code: options.code,
950
- couponId: options.couponId,
951
- event: 'adapter.coupon.parse_failed',
952
- })
953
- return null
954
- }
955
-
956
- return parsedCoupon.data
957
- },
958
- async createMerchantSession(options): Promise<MerchantSession> {
959
- const id = `ms_${v4()}`
960
- await client.insert(merchantSession).values({
961
- id,
962
- identifier: options.identifier,
963
- merchantAccountId: options.merchantAccountId,
964
- ...(options.organizationId
965
- ? { organizationId: options.organizationId }
966
- : {}),
967
- })
968
-
969
- return MerchantSessionSchema.parse(
970
- await client.query.merchantSession.findFirst({
971
- where: eq(merchantSession.id, id),
972
- }),
973
- )
974
- },
975
- async createMerchantChargeAndPurchase(options): Promise<Purchase> {
976
- const purchaseId = await client.transaction(async (trx) => {
977
- try {
978
- const {
979
- userId,
980
- stripeChargeId,
981
- stripeCouponId,
982
- merchantAccountId,
983
- merchantProductId,
984
- merchantCustomerId,
985
- productId,
986
- stripeChargeAmount,
987
- quantity = 1,
988
- checkoutSessionId,
989
- appliedPPPStripeCouponId,
990
- upgradedFromPurchaseId,
991
- country,
992
- usedCouponId,
993
- organizationId,
994
- attributionSnapshot,
995
- } = options
996
-
997
- const existingMerchantCharge = merchantChargeSchema.nullable().parse(
998
- (await client.query.merchantCharge.findFirst({
999
- where: eq(merchantCharge.identifier, stripeChargeId),
1000
- })) || null,
1001
- )
1002
-
1003
- const existingPurchaseForCharge = existingMerchantCharge
1004
- ? await client.query.purchases.findFirst({
1005
- where: eq(
1006
- purchaseTable.merchantChargeId,
1007
- existingMerchantCharge.id,
1008
- ),
1009
- with: {
1010
- user: true,
1011
- product: true,
1012
- bulkCoupon: true,
1013
- },
1014
- })
1015
- : null
1016
-
1017
- if (existingPurchaseForCharge) {
1018
- return existingPurchaseForCharge.id
1019
- }
1020
-
1021
- const merchantChargeId = `mc_${v4()}`
1022
- const purchaseId = `purch_${v4()}`
1023
-
1024
- const newMerchantCharge = await client.insert(merchantCharge).values({
1025
- id: merchantChargeId,
1026
- userId,
1027
- identifier: stripeChargeId,
1028
- merchantAccountId,
1029
- merchantProductId,
1030
- merchantCustomerId,
1031
- })
1032
-
1033
- const existingPurchase = purchaseSchema.nullable().parse(
1034
- (await client.query.purchases.findFirst({
1035
- where: and(
1036
- eq(purchaseTable.productId, productId),
1037
- eq(purchaseTable.userId, userId),
1038
- inArray(purchaseTable.status, ['Valid', 'Restricted']),
1039
- ),
1040
- })) || null,
1041
- )
1042
-
1043
- const existingBulkCoupon = couponSchema.nullable().parse(
1044
- await client
1045
- .select()
1046
- .from(coupon)
1047
- .leftJoin(
1048
- purchaseTable,
1049
- and(
1050
- eq(coupon.id, purchaseTable.bulkCouponId),
1051
- eq(purchaseTable.userId, userId),
1052
- ),
1053
- )
1054
- .where(
1055
- and(
1056
- eq(coupon.restrictedToProductId, productId),
1057
- eq(purchaseTable.userId, userId),
1058
- eq(purchaseTable.status, 'Valid'),
1059
- ),
1060
- )
1061
- .then((res) => {
1062
- return res[0]?.Coupon ?? null
1063
- }),
1064
- )
1065
-
1066
- const isBulkPurchase =
1067
- quantity > 1 || Boolean(existingBulkCoupon) || Boolean(options.bulk)
1068
-
1069
- let bulkCouponId: string | null = null
1070
- let couponToUpdate = null
1071
- const purchaseBenefitsTransport =
1072
- options.purchaseBenefitsTransport?.trim() || undefined
1073
- const purchaseBenefitExpansion = purchaseBenefitsTransport
1074
- ? parseAndExpandPurchaseBenefitTransport({
1075
- transport: purchaseBenefitsTransport,
1076
- appliesTo: isBulkPurchase ? 'redeemed_team_seat' : 'buyer',
1077
- })
1078
- : null
1079
-
1080
- if (purchaseBenefitExpansion && !purchaseBenefitExpansion.success) {
1081
- logger.info('purchase_benefits.transport.needs_review', {
1082
- checkoutSessionId,
1083
- event: 'purchase_benefits.transport.needs_review',
1084
- metadataKey: PURCHASE_BENEFIT_METADATA_KEY,
1085
- productId,
1086
- stripeChargeId,
1087
- userId,
1088
- errors: purchaseBenefitExpansion.review.errors,
1089
- })
1090
- }
1091
-
1092
- if (isBulkPurchase) {
1093
- bulkCouponId =
1094
- existingBulkCoupon !== null ? existingBulkCoupon.id : v4()
1095
-
1096
- if (existingBulkCoupon !== null) {
1097
- const existingCouponFields = existingBulkCoupon.fields ?? {}
1098
- couponToUpdate = await client
1099
- .update(coupon)
1100
- .set({
1101
- maxUses: (existingBulkCoupon?.maxUses || 0) + quantity,
1102
- fields: mergePurchaseBenefitFields(
1103
- existingCouponFields,
1104
- purchaseBenefitExpansion,
1105
- ),
1106
- ...(organizationId ? { organizationId } : {}),
1107
- })
1108
- .where(eq(coupon.id, bulkCouponId as string))
1109
- } else {
1110
- // Try to find merchant coupon by stripeCouponId first
1111
- let merchantCouponToUse = stripeCouponId
1112
- ? merchantCouponSchema.nullable().parse(
1113
- (await client.query.merchantCoupon.findFirst({
1114
- where: eq(merchantCoupon.identifier, stripeCouponId),
1115
- })) || null,
1116
- )
1117
- : null
1118
-
1119
- // Fallback: If not found, try to find via usedCouponId from checkout session metadata
1120
- if (!merchantCouponToUse && usedCouponId) {
1121
- // First get the coupon record to find its merchantCouponId
1122
- const usedCoupon = couponSchema.nullable().parse(
1123
- (await client.query.coupon.findFirst({
1124
- where: eq(coupon.id, usedCouponId),
1125
- })) || null,
1126
- )
1127
-
1128
- // Then get the merchant coupon
1129
- if (usedCoupon?.merchantCouponId) {
1130
- merchantCouponToUse = merchantCouponSchema.nullable().parse(
1131
- (await client.query.merchantCoupon.findFirst({
1132
- where: eq(merchantCoupon.id, usedCoupon.merchantCouponId),
1133
- })) || null,
1134
- )
1135
- }
1136
- }
1137
-
1138
- const bulkCouponValues = {
1139
- id: bulkCouponId as string,
1140
- percentageDiscount: '1.0',
1141
- restrictedToProductId: productId,
1142
- maxUses: quantity,
1143
- status: 1,
1144
- fields: mergePurchaseBenefitFields(
1145
- {},
1146
- purchaseBenefitExpansion,
1147
- ),
1148
- ...(organizationId ? { organizationId } : {}),
1149
- ...(merchantCouponToUse
1150
- ? {
1151
- merchantCouponId: merchantCouponToUse.id,
1152
- }
1153
- : {}),
1154
- }
1155
-
1156
- couponToUpdate = await client
1157
- .insert(coupon)
1158
- .values(bulkCouponValues)
1159
- }
1160
- }
1161
-
1162
- // create a new merchant session
1163
- const merchantSessionId = `ms_${v4()}`
1164
-
1165
- await client.insert(merchantSession).values({
1166
- id: merchantSessionId,
1167
- identifier: checkoutSessionId,
1168
- merchantAccountId,
1169
- })
1170
-
1171
- const merchantCouponUsed = stripeCouponId
1172
- ? await client.query.merchantCoupon.findFirst({
1173
- where: eq(merchantCoupon.identifier, stripeCouponId),
1174
- })
1175
- : null
1176
-
1177
- const pppMerchantCoupon = appliedPPPStripeCouponId
1178
- ? await client.query.merchantCoupon.findFirst({
1179
- where: and(
1180
- eq(merchantCoupon.identifier, appliedPPPStripeCouponId),
1181
- eq(merchantCoupon.type, 'ppp'),
1182
- ),
1183
- })
1184
- : null
1185
-
1186
- const newPurchaseStatus =
1187
- merchantCouponUsed?.type === 'ppp' || pppMerchantCoupon
1188
- ? 'Restricted'
1189
- : 'Valid'
1190
-
1191
- await client.insert(purchaseTable).values({
1192
- id: purchaseId,
1193
- status: newPurchaseStatus,
1194
- userId,
1195
- productId,
1196
- merchantChargeId,
1197
- totalAmount: (stripeChargeAmount / 100).toFixed(),
1198
- bulkCouponId,
1199
- merchantSessionId,
1200
- country,
1201
- fields: {
1202
- ...(!isBulkPurchase
1203
- ? mergePurchaseBenefitFields({}, purchaseBenefitExpansion)
1204
- : {}),
1205
- ...(attributionSnapshot
1206
- ? { attribution: attributionSnapshot }
1207
- : {}),
1208
- },
1209
- upgradedFromId: upgradedFromPurchaseId || null,
1210
- couponId: usedCouponId || null,
1211
- ...(organizationId ? { organizationId } : {}),
1212
- })
1213
-
1214
- const oneWeekInMilliseconds = 1000 * 60 * 60 * 24 * 7
1215
-
1216
- await client.insert(purchaseUserTransfer).values({
1217
- id: `put_${v4()}`,
1218
- purchaseId: purchaseId as string,
1219
- expiresAt: existingPurchase
1220
- ? new Date()
1221
- : new Date(Date.now() + oneWeekInMilliseconds),
1222
- sourceUserId: userId,
1223
- ...(organizationId ? { organizationId } : {}),
1224
- })
1225
-
1226
- // const result = await Promise.all([
1227
- // newMerchantCharge,
1228
- // newPurchase,
1229
- // newPurchaseUserTransfer,
1230
- // newMerchantSession,
1231
- // ...(couponToUpdate ? [couponToUpdate] : []),
1232
- // ])
1233
- //
1234
- // console.log('result', { result })
1235
-
1236
- return purchaseId
1237
- } catch (error) {
1238
- logger.error(toError(error), {
1239
- checkoutSessionId: options.checkoutSessionId,
1240
- event: 'adapter.purchase.create.failed',
1241
- organizationId: options.organizationId,
1242
- productId: options.productId,
1243
- stripeChargeId: options.stripeChargeId,
1244
- userId: options.userId,
1245
- })
1246
- trx.rollback()
1247
- throw error
1248
- }
1249
- })
1250
-
1251
- const parsedPurchase = purchaseSchema.safeParse(
1252
- await client.query.purchases.findFirst({
1253
- where: eq(purchaseTable.id, purchaseId as string),
1254
- }),
1255
- )
1256
-
1257
- if (!parsedPurchase.success) {
1258
- logger.error(parsedPurchase.error, {
1259
- event: 'adapter.purchase.parse_failed',
1260
- productId: options.productId,
1261
- purchaseId: purchaseId as string,
1262
- userId: options.userId,
1263
- })
1264
- throw new Error('Error parsing purchase')
1265
- }
1266
-
1267
- return parsedPurchase.data
1268
- },
1269
- async findOrCreateMerchantCustomer(options: {
1270
- user: User
1271
- identifier: string
1272
- merchantAccountId: string
1273
- }): Promise<MerchantCustomer | null> {
1274
- const merchantCustomer = merchantCustomerSchema
1275
- .nullable()
1276
- .optional()
1277
- .parse(
1278
- await client.query.merchantCustomer.findFirst({
1279
- where: (merchantCustomer, { eq }) =>
1280
- eq(merchantCustomer.identifier, options.identifier),
1281
- }),
1282
- )
1283
-
1284
- if (merchantCustomer) {
1285
- return merchantCustomer
1286
- }
1287
-
1288
- return await adapter.createMerchantCustomer({
1289
- identifier: options.identifier,
1290
- merchantAccountId: options.merchantAccountId,
1291
- userId: options.user.id,
1292
- })
1293
- },
1294
- async createMerchantEvent(options: {
1295
- merchantAccountId: string
1296
- identifier: string
1297
- payload: Record<string, any>
1298
- }): Promise<MerchantEvents> {
1299
- const eventId = `me_${v4()}`
1300
-
1301
- await client.insert(merchantEvents).values({
1302
- id: eventId,
1303
- merchantAccountId: options.merchantAccountId,
1304
- identifier: options.identifier,
1305
- payload: options.payload,
1306
- })
1307
-
1308
- const createdEvent = await client.query.merchantEvents.findFirst({
1309
- where: eq(merchantEvents.id, eventId),
1310
- })
1311
-
1312
- return merchantEventsSchema.parse(createdEvent)
1313
- },
1314
- async getMerchantEventByIdentifier(
1315
- identifier: string,
1316
- ): Promise<MerchantEvents | null> {
1317
- const event = await client.query.merchantEvents.findFirst({
1318
- where: eq(merchantEvents.identifier, identifier),
1319
- })
1320
-
1321
- return event ? merchantEventsSchema.parse(event) : null
1322
- },
1323
- async getMerchantEventsByAccount(
1324
- merchantAccountId: string,
1325
- ): Promise<MerchantEvents[]> {
1326
- const events = await client.query.merchantEvents.findMany({
1327
- where: eq(merchantEvents.merchantAccountId, merchantAccountId),
1328
- orderBy: [desc(merchantEvents.createdAt)],
1329
- })
1330
-
1331
- return events.map((event) => merchantEventsSchema.parse(event))
1332
- },
1333
- async findOrCreateUser(
1334
- email: string,
1335
- name?: string | null,
1336
- ): Promise<{
1337
- user: User
1338
- isNewUser: boolean
1339
- }> {
1340
- const user = await adapter.getUserByEmail?.(email)
1341
-
1342
- if (!user) {
1343
- const newUser = await adapter.createUser?.({
1344
- id: `u_${v4()}`,
1345
- email,
1346
- name,
1347
- emailVerified: null,
1348
- })
1349
- if (!newUser) {
1350
- throw new Error('Could not create user')
1351
- }
1352
- logger.info('adapter.user.created', {
1353
- email,
1354
- userId: newUser.id,
1355
- })
1356
- return {
1357
- user: newUser as User,
1358
- isNewUser: true,
1359
- }
1360
- }
1361
-
1362
- return {
1363
- user: user as User,
1364
- isNewUser: false,
1365
- }
1366
- },
1367
- async getCoupon(couponIdOrCode: string): Promise<Coupon | null> {
1368
- const loadedCoupon =
1369
- (await client.query.coupon.findFirst({
1370
- where: or(
1371
- eq(coupon.id, couponIdOrCode),
1372
- eq(coupon.code, couponIdOrCode),
1373
- ),
1374
- })) || null
1375
-
1376
- logger.debug('getCoupon', {
1377
- couponIdOrCode,
1378
- found: !!loadedCoupon,
1379
- })
1380
-
1381
- return couponSchema.nullable().parse(loadedCoupon)
1382
- },
1383
- async getPurchasesForBulkCouponId(
1384
- bulkCouponId: string,
1385
- ): Promise<(Purchase & { user: User })[]> {
1386
- return z.array(purchaseSchema.extend({ user: userSchema })).parse(
1387
- await client.query.purchases.findMany({
1388
- where: eq(purchaseTable.bulkCouponId, bulkCouponId),
1389
- with: {
1390
- user: true,
1391
- },
1392
- }),
1393
- )
1394
- },
1395
- async getCouponWithBulkPurchases(couponId: string): Promise<
1396
- | (Coupon & {
1397
- bulkPurchases?: Purchase[] | null
1398
- redeemedBulkCouponPurchases: { bulkCouponId?: string | null }[]
1399
- })
1400
- | null
1401
- > {
1402
- logger.debug('getCouponWithBulkPurchases', { couponId })
1403
- let couponData
1404
- let bulkCouponPurchases: unknown[] = []
1405
- try {
1406
- couponData =
1407
- (await client.query.coupon.findFirst({
1408
- where: eq(coupon.id, couponId),
1409
- with: {
1410
- bulkPurchases: true,
1411
- redeemedBulkCouponPurchases: true,
1412
- },
1413
- })) || null
1414
- } catch (e) {
1415
- logger.error(e as Error, {
1416
- method: 'getCouponWithBulkPurchases',
1417
- couponId,
1418
- step: 'load-coupon',
1419
- })
1420
- }
1421
-
1422
- try {
1423
- bulkCouponPurchases = await client.query.purchases.findMany({
1424
- where: eq(purchaseTable.redeemedBulkCouponId, couponId),
1425
- with: {
1426
- user: true,
1427
- },
1428
- })
1429
- logger.debug('getCouponWithBulkPurchases.redeemed', {
1430
- couponId,
1431
- count: bulkCouponPurchases?.length ?? 0,
1432
- })
1433
- } catch (e) {
1434
- logger.error(e as Error, {
1435
- method: 'getCouponWithBulkPurchases',
1436
- couponId,
1437
- step: 'load-redeemed-purchases',
1438
- })
1439
- }
1440
-
1441
- if (!couponData) {
1442
- logger.debug('getCouponWithBulkPurchases', {
1443
- couponId,
1444
- error: 'no coupon found',
1445
- })
1446
- return null
1447
- }
1448
-
1449
- const couponWithBulkPurchases = {
1450
- ...couponData,
1451
- redeemedBulkCouponPurchases: bulkCouponPurchases || [],
1452
- }
1453
-
1454
- const parsedCoupon = couponSchema
1455
- .merge(
1456
- z.object({
1457
- redeemedBulkCouponPurchases: z.array(purchaseSchema),
1458
- }),
1459
- )
1460
- .nullable()
1461
- .safeParse(couponWithBulkPurchases)
1462
-
1463
- if (!parsedCoupon.success) {
1464
- logger.error(parsedCoupon.error, {
1465
- couponId: couponData?.id ?? couponId,
1466
- event: 'adapter.coupon.parse_failed',
1467
- redeemedBulkCouponPurchasesCount: bulkCouponPurchases?.length ?? 0,
1468
- })
1469
- return null
1470
- }
1471
-
1472
- return parsedCoupon.data
1473
- },
1474
- async getDefaultCoupon(productIds?: string[]): Promise<{
1475
- defaultMerchantCoupon: MerchantCoupon
1476
- defaultCoupon: Coupon
1477
- } | null> {
1478
- const activeSaleCoupon = await client.query.coupon.findFirst({
1479
- where: and(
1480
- eq(coupon.status, 1),
1481
- eq(coupon.default, true),
1482
- gte(coupon.expires, new Date()),
1483
- or(
1484
- productIds
1485
- ? inArray(coupon.restrictedToProductId, productIds)
1486
- : undefined,
1487
- isNull(coupon.restrictedToProductId),
1488
- ),
1489
- ),
1490
- orderBy: desc(coupon.percentageDiscount),
1491
- with: {
1492
- merchantCoupon: true,
1493
- product: true,
1494
- },
1495
- })
1496
- if (activeSaleCoupon) {
1497
- const { restrictedToProductId } = activeSaleCoupon
1498
- const validForProdcutId = restrictedToProductId
1499
- ? productIds?.includes(restrictedToProductId as string)
1500
- : true
1501
-
1502
- const { merchantCoupon: defaultMerchantCoupon, ...defaultCoupon } =
1503
- activeSaleCoupon
1504
-
1505
- if (validForProdcutId) {
1506
- return {
1507
- defaultMerchantCoupon: merchantCouponSchema.parse(
1508
- defaultMerchantCoupon,
1509
- ),
1510
- defaultCoupon: couponSchema.parse(defaultCoupon),
1511
- }
1512
- }
1513
- }
1514
- return null
1515
- },
1516
- getLessonProgressCountsByDate(): Promise<
1517
- {
1518
- count: number
1519
- completedAt: string
1520
- }[]
1521
- > {
1522
- throw new Error('getLessonProgressCountsByDate Method not implemented.')
1523
- },
1524
- async getLessonProgressForUser(
1525
- userId: string,
1526
- ): Promise<ResourceProgress[]> {
1527
- const userProgress = await client.query.resourceProgress.findMany({
1528
- where: eq(resourceProgress.userId, userId),
1529
- })
1530
- const parsed = z.array(resourceProgressSchema).safeParse(userProgress)
1531
- if (!parsed.success) {
1532
- logger.error(parsed.error, {
1533
- event: 'adapter.userProgress.parse_failed',
1534
- userId,
1535
- })
1536
- return []
1537
- }
1538
- return parsed.data
1539
- },
1540
- async getModuleProgressForUser(
1541
- userIdOrEmail: string,
1542
- moduleIdOrSlug: string,
1543
- ): Promise<ModuleProgress | null> {
1544
- // First, get the user ID
1545
- const user = await client.query.users.findFirst({
1546
- where: or(eq(users.id, userIdOrEmail), eq(users.email, userIdOrEmail)),
1547
- columns: {
1548
- id: true,
1549
- },
1550
- })
1551
-
1552
- if (!user) {
1553
- return null
1554
- }
1555
-
1556
- const ResultRowSchema = z.object({
1557
- resource_id: z.string(),
1558
- resource_type: z.enum(['lesson', 'exercise', 'post']),
1559
- resource_slug: z.string().nullable(),
1560
- completed_at: z
1561
- .string()
1562
- .nullable()
1563
- .transform((val) => (val ? new Date(val) : null)),
1564
- })
1565
-
1566
- // Execute the optimized query - create a flattened, properly ordered list
1567
- const results: any = await client.execute(sql`
1568
- WITH RECURSIVE workshop AS (
1569
- SELECT id, fields->>'$.slug' AS slug
1570
- FROM ${contentResource}
1571
- WHERE id = ${moduleIdOrSlug} OR fields->>'$.slug' = ${moduleIdOrSlug}
1572
- ),
1573
- -- Get all workshop resources with their positions
1574
- workshop_structure AS (
1575
- SELECT
1576
- w.id AS workshop_id,
1577
- crr.resourceId,
1578
- crr.position AS position,
1579
- cr.type AS resource_type,
1580
- cr.fields->>'$.slug' AS resource_slug,
1581
- cr.id AS resource_content_id
1582
- FROM workshop w
1583
- JOIN ${contentResourceResource} crr ON w.id = crr.resourceOfId
1584
- JOIN ${contentResource} cr ON crr.resourceId = cr.id
1585
- ORDER BY crr.position
1586
- ),
1587
- -- Recursively expand sections and maintain global ordering
1588
- expanded_resources AS (
1589
- -- Base case: direct lessons/posts from workshop
1590
- SELECT
1591
- workshop_id,
1592
- position * 1000 AS global_order, -- Multiply by 1000 to leave room for section items
1593
- resource_content_id AS resource_id,
1594
- resource_type,
1595
- resource_slug
1596
- FROM workshop_structure
1597
- WHERE resource_type IN ('lesson', 'post')
1598
- UNION ALL
1599
- -- Recursive case: lessons within sections
1600
- SELECT
1601
- ws.workshop_id,
1602
- ws.position * 1000 + crr.position AS global_order, -- Section position * 1000 + lesson position
1603
- cr.id AS resource_id,
1604
- cr.type AS resource_type,
1605
- cr.fields->>'$.slug' AS resource_slug
1606
- FROM workshop_structure ws
1607
- JOIN ${contentResourceResource} crr ON ws.resource_content_id = crr.resourceOfId
1608
- JOIN ${contentResource} cr ON crr.resourceId = cr.id
1609
- WHERE ws.resource_type = 'section'
1610
- AND cr.type IN ('lesson', 'post')
1611
- )
1612
- SELECT
1613
- er.resource_id,
1614
- er.resource_type,
1615
- er.resource_slug,
1616
- rp.completedAt AS completed_at,
1617
- er.global_order
1618
- FROM expanded_resources er
1619
- LEFT JOIN ${resourceProgress} rp ON rp.resourceId = er.resource_id
1620
- AND rp.userId = ${user.id}
1621
- ORDER BY er.global_order ASC
1622
- `)
1623
- // Process the results
1624
- const completedLessons: ResourceProgress[] = []
1625
- let nextResource: Partial<ContentResource> | null = null
1626
- let completedLessonsCount = 0
1627
- let totalLessonsCount = results.rows.length
1628
-
1629
- const parsedRows = z.array(ResultRowSchema).safeParse(results.rows)
1630
-
1631
- if (!parsedRows.success) {
1632
- logger.error(parsedRows.error, {
1633
- event: 'adapter.moduleProgress.rows.parse_failed',
1634
- moduleIdOrSlug,
1635
- userId: user.id,
1636
- })
1637
- return {
1638
- completedLessons: [],
1639
- nextResource: null,
1640
- percentCompleted: 0,
1641
- completedLessonsCount: 0,
1642
- totalLessonsCount,
1643
- }
1644
- }
1645
-
1646
- for (const row of parsedRows.data) {
1647
- if (row.completed_at) {
1648
- completedLessonsCount++
1649
- completedLessons.push({
1650
- userId: user.id as string,
1651
- resourceId: row.resource_id,
1652
- completedAt: new Date(row.completed_at),
1653
- // Add other fields as needed
1654
- })
1655
- } else if (!nextResource) {
1656
- nextResource = {
1657
- id: row.resource_id,
1658
- type: row.resource_type,
1659
- fields: {
1660
- slug: row.resource_slug,
1661
- },
1662
- }
1663
- }
1664
- }
1665
-
1666
- const percentCompleted =
1667
- totalLessonsCount > 0
1668
- ? Math.ceil((completedLessonsCount / totalLessonsCount) * 100)
1669
- : 0
1670
-
1671
- return {
1672
- completedLessons,
1673
- nextResource,
1674
- percentCompleted,
1675
- completedLessonsCount,
1676
- totalLessonsCount,
1677
- }
1678
- },
1679
- getLessonProgresses(): Promise<ResourceProgress[]> {
1680
- throw new Error('getLessonProgresses Method not implemented.')
1681
- },
1682
- async getMerchantCharge(
1683
- merchantChargeId: string,
1684
- ): Promise<MerchantCharge | null> {
1685
- const mCharge = await client.query.merchantCharge.findFirst({
1686
- where: eq(merchantCharge.id, merchantChargeId),
1687
- })
1688
- const parsed = merchantChargeSchema.safeParse(mCharge)
1689
- if (!parsed.success) {
1690
- logger.error(parsed.error, {
1691
- event: 'adapter.merchantCharge.parse_failed',
1692
- merchantChargeId,
1693
- })
1694
- return null
1695
- }
1696
- return parsed.data
1697
- },
1698
- async getMerchantCouponsForTypeAndPercent(params: {
1699
- type: string
1700
- percentageDiscount: number
1701
- }): Promise<MerchantCoupon[]> {
1702
- return z.array(merchantCouponSchema).parse(
1703
- await client.query.merchantCoupon.findMany({
1704
- where: and(
1705
- eq(merchantCoupon.type, params.type),
1706
- eq(
1707
- merchantCoupon.percentageDiscount,
1708
- params.percentageDiscount.toString(),
1709
- ),
1710
- ),
1711
- }),
1712
- )
1713
- },
1714
- async getMerchantCouponForTypeAndPercent(params: {
1715
- type: string
1716
- percentageDiscount: number
1717
- }): Promise<MerchantCoupon | null> {
1718
- const foundMerchantCoupon = await client.query.merchantCoupon.findFirst({
1719
- where: and(
1720
- eq(merchantCoupon.type, params.type),
1721
- eq(
1722
- merchantCoupon.percentageDiscount,
1723
- params.percentageDiscount.toString(),
1724
- ),
1725
- ),
1726
- })
1727
-
1728
- const parsed = merchantCouponSchema
1729
- .nullable()
1730
- .safeParse(foundMerchantCoupon)
1731
- if (parsed.success) {
1732
- return parsed.data
1733
- }
1734
-
1735
- return null
1736
- },
1737
- async getMerchantCouponForTypeAndAmount(params: {
1738
- type: string
1739
- amountDiscount: number
1740
- }): Promise<MerchantCoupon | null> {
1741
- const foundMerchantCoupon = await client.query.merchantCoupon.findFirst({
1742
- where: and(
1743
- eq(merchantCoupon.type, params.type),
1744
- eq(merchantCoupon.amountDiscount, params.amountDiscount),
1745
- ),
1746
- })
1747
-
1748
- const parsed = merchantCouponSchema
1749
- .nullable()
1750
- .safeParse(foundMerchantCoupon)
1751
- if (parsed.success) {
1752
- return parsed.data
1753
- }
1754
-
1755
- return null
1756
- },
1757
- async getMerchantCoupon(
1758
- merchantCouponId: string,
1759
- ): Promise<MerchantCoupon | null> {
1760
- const foundMerchantCoupon = await client.query.merchantCoupon.findFirst({
1761
- where: eq(merchantCoupon.id, merchantCouponId),
1762
- })
1763
-
1764
- const parsed = merchantCouponSchema
1765
- .nullable()
1766
- .safeParse(foundMerchantCoupon)
1767
- if (parsed.success) {
1768
- return parsed.data
1769
- }
1770
-
1771
- return null
56
+ ): DrizzleAdapterComposition<InstanceType<typeof MySqlDatabase>> {
57
+ const context = createMySqlContext(client, tableFn)
58
+ const organizationPort = createOrganizationPort(context)
59
+ const catalogPort = createCatalogPort(context, paymentProvider)
60
+ const contentResourcePort = createContentResourcePort(context)
61
+ const couponPort = createCouponPort(context)
62
+ const entitlementPort = createEntitlementPort(context)
63
+ const identityPort = createIdentityPort(context)
64
+ const merchantPort = createMerchantPort(context)
65
+ const subscriptionPort = createSubscriptionPort(context)
66
+ const authStoragePort = createAuthStoragePort(context)
67
+ const progressPort = createProgressPort(context)
68
+ const purchasePort = createPurchasePort(context)
69
+ const merchantChargeAndPurchaseWorkflow =
70
+ createMerchantChargeAndPurchaseWorkflow(context)
71
+ const transferPurchaseToUserWorkflow = createTransferPurchaseToUserWorkflow(
72
+ context,
73
+ paymentProvider,
74
+ {
75
+ getPurchaseUserTransferById: purchasePort.getPurchaseUserTransferById,
1772
76
  },
1773
- async getMerchantProduct(
1774
- stripeProductId: string,
1775
- ): Promise<MerchantProduct | null> {
1776
- return merchantProductSchema.nullable().parse(
1777
- await client.query.merchantProduct.findFirst({
1778
- where: eq(merchantProduct.identifier, stripeProductId),
1779
- }),
1780
- )
1781
- },
1782
- getPrice(productId: string): Promise<Price | null> {
1783
- throw new Error('getPrice not implemented.')
1784
- },
1785
- async getPriceForProduct(productId: string): Promise<Price | null> {
1786
- return priceSchema.nullable().parse(
1787
- await client.query.prices.findFirst({
1788
- where: eq(prices.productId, productId),
1789
- }),
1790
- )
1791
- },
1792
- async archiveProduct(productId) {
1793
- if (!paymentProvider) throw new Error('Payment provider not found')
1794
- const product = await adapter.getProduct(productId)
1795
-
1796
- if (!product) {
1797
- throw new Error(`Product not found for id (${productId})`)
1798
- }
1799
-
1800
- if (!product.price) {
1801
- throw new Error(`Product has no price`)
1802
- }
1803
-
1804
- await client
1805
- .update(products)
1806
- .set({ status: 0, name: `${product.name} (Archived)` })
1807
- .where(eq(products.id, productId))
1808
-
1809
- await client
1810
- .update(prices)
1811
- .set({ status: 0, nickname: `${product.name} (Archived)` })
1812
- .where(eq(prices.productId, productId))
1813
-
1814
- await client
1815
- .update(merchantProduct)
1816
- .set({ status: 0 })
1817
- .where(eq(merchantProduct.productId, productId))
1818
-
1819
- await client
1820
- .update(merchantPrice)
1821
- .set({ status: 0 })
1822
- .where(eq(merchantPrice.priceId, product.price.id))
1823
-
1824
- const currentMerchantProduct = merchantProductSchema.nullish().parse(
1825
- await client.query.merchantProduct.findFirst({
1826
- where: eq(merchantProduct.productId, productId),
1827
- }),
1828
- )
1829
-
1830
- if (!currentMerchantProduct || !currentMerchantProduct.identifier) {
1831
- throw new Error(`Merchant product not found for id (${productId})`)
1832
- }
1833
-
1834
- await paymentProvider.updateProduct(currentMerchantProduct.identifier, {
1835
- active: false,
1836
- })
1837
-
1838
- const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1839
- await client.query.merchantPrice.findFirst({
1840
- where: and(
1841
- eq(merchantPrice.priceId, product.price.id),
1842
- eq(merchantPrice.status, 1),
1843
- ),
1844
- }),
1845
- )
1846
-
1847
- if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1848
- throw new Error(`Merchant price not found for id (${productId})`)
1849
- }
1850
-
1851
- await paymentProvider.updatePrice(currentMerchantPrice.identifier, {
1852
- active: false,
1853
- })
1854
-
1855
- return adapter.getProduct(productId)
77
+ )
78
+ const redeemFullPriceCouponWorkflow = createRedeemFullPriceCouponWorkflow(
79
+ context,
80
+ {
81
+ getCouponWithBulkPurchases: couponPort.getCouponWithBulkPurchases,
82
+ incrementCouponUsedCount: couponPort.incrementCouponUsedCount,
83
+ findOrCreateUser: identityPort.findOrCreateUser,
84
+ getUserById: identityPort.getUserById,
85
+ getMembershipsForUser: organizationPort.getMembershipsForUser,
86
+ createPurchase: purchasePort.createPurchase,
87
+ createPurchaseTransfer: purchasePort.createPurchaseTransfer,
88
+ getExistingNonBulkValidPurchasesOfProduct:
89
+ purchasePort.getExistingNonBulkValidPurchasesOfProduct,
90
+ getPurchase: purchasePort.getPurchase,
1856
91
  },
1857
- async updateProduct(input: Product) {
1858
- if (!paymentProvider) throw new Error('Payment provider not found')
1859
- const currentProduct = await adapter.getProduct(input.id)
1860
- if (!currentProduct) {
1861
- throw new Error(`Product not found`)
1862
- }
1863
- if (!currentProduct.price) {
1864
- throw new Error(`Product has no price`)
1865
- }
1866
-
1867
- const merchantProduct = merchantProductSchema.nullish().parse(
1868
- await client.query.merchantProduct.findFirst({
1869
- where: (merchantProduct, { eq }) =>
1870
- eq(merchantProduct.productId, input.id),
1871
- }),
1872
- )
1873
-
1874
- if (!merchantProduct || !merchantProduct.identifier) {
1875
- throw new Error(`Merchant product not found`)
1876
- }
1877
-
1878
- // TODO: handle upgrades
1879
-
1880
- const stripeProduct = await paymentProvider.getProduct(
1881
- merchantProduct.identifier,
1882
- )
1883
-
1884
- const priceChanged =
1885
- currentProduct.price.unitAmount.toString() !==
1886
- input.price?.unitAmount.toString()
1887
-
1888
- // Get current merchant price and Stripe price to check if it needs updating
1889
- const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1890
- await client.query.merchantPrice.findFirst({
1891
- where: (merchantPrice, { eq, and }) =>
1892
- and(
1893
- eq(merchantPrice.merchantProductId, merchantProduct.id),
1894
- eq(merchantPrice.status, 1),
1895
- ),
1896
- }),
1897
- )
1898
-
1899
- if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1900
- throw new Error(`Merchant price not found`)
1901
- }
1902
-
1903
- const currentStripePrice = await paymentProvider.getPrice(
1904
- currentMerchantPrice.identifier,
1905
- )
1906
-
1907
- // Check if type changed to/from membership (requires Stripe price recreation)
1908
- const membershipTypeChanged =
1909
- currentProduct.type !== input.type &&
1910
- (input.type === 'membership' || currentProduct.type === 'membership')
1911
-
1912
- // Check if billing interval changed for membership products
1913
- const billingIntervalChanged =
1914
- input.type === 'membership' &&
1915
- currentProduct.type === 'membership' &&
1916
- currentProduct.fields?.billingInterval !== input.fields?.billingInterval
1917
-
1918
- // Check if Stripe price recurring status doesn't match expected state for input type:
1919
- // - membership products require recurring=true
1920
- // - non-membership products require recurring=false (null/undefined)
1921
- const isMembership = input.type === 'membership'
1922
- const stripePriceMismatch =
1923
- isMembership !== !!currentStripePrice.recurring
1924
-
1925
- // Need to recreate price if: price changed, type changed to/from membership,
1926
- // billing interval changed, or Stripe price doesn't match expected recurring status
1927
- const needsPriceRecreation =
1928
- priceChanged ||
1929
- membershipTypeChanged ||
1930
- billingIntervalChanged ||
1931
- stripePriceMismatch
1932
-
1933
- if (needsPriceRecreation) {
1934
- // Get billing interval from input or default to 'year'
1935
- const billingInterval = input.fields?.billingInterval || 'year'
1936
-
1937
- const priceParams = {
1938
- product: stripeProduct.id,
1939
- unit_amount: Math.floor(Number(input.price?.unitAmount || 0) * 100),
1940
- currency: 'usd',
1941
- metadata: {
1942
- slug: input.fields.slug,
1943
- },
1944
- active: true,
1945
- // Membership products get recurring billing
1946
- ...(input.type === 'membership' && {
1947
- recurring: { interval: billingInterval },
1948
- }),
1949
- }
1950
-
1951
- logger.debug('adapter.product.price_recreation.started', {
1952
- billingInterval,
1953
- currency: priceParams.currency,
1954
- currentMerchantPriceId: currentMerchantPrice.id,
1955
- isMembership: input.type === 'membership',
1956
- merchantProductId: merchantProduct.id,
1957
- productId: input.id,
1958
- productSlug: input.fields.slug,
1959
- stripeProductId: stripeProduct.id,
1960
- unitAmount: priceParams.unit_amount,
1961
- })
1962
-
1963
- const newStripePrice = await paymentProvider.createPrice(priceParams)
1964
-
1965
- await paymentProvider.updateProduct(stripeProduct.id, {
1966
- default_price: newStripePrice.id,
1967
- })
1968
-
1969
- const newMerchantPriceId = `mprice_${v4()}`
1970
- await client.insert(merchantPrice).values({
1971
- id: newMerchantPriceId,
1972
- merchantProductId: merchantProduct.id,
1973
- merchantAccountId: merchantProduct.merchantAccountId,
1974
- priceId: currentProduct.price.id,
1975
- status: 1,
1976
- identifier: newStripePrice.id,
1977
- })
1978
-
1979
- if (currentMerchantPrice) {
1980
- await client
1981
- .update(merchantPrice)
1982
- .set({
1983
- status: 0,
1984
- })
1985
- .where(eq(merchantPrice.id, currentMerchantPrice.id))
1986
- }
1987
-
1988
- await client
1989
- .update(prices)
1990
- .set({
1991
- unitAmount: Math.floor(
1992
- Number(input.price?.unitAmount || 0),
1993
- ).toString(),
1994
- nickname: input.name,
1995
- })
1996
- .where(eq(prices.id, currentProduct.price.id))
1997
-
1998
- if (currentStripePrice) {
1999
- await paymentProvider.updatePrice(currentStripePrice.id, {
2000
- active: false,
2001
- })
2002
- }
2003
- }
2004
-
2005
- await paymentProvider.updateProduct(stripeProduct.id, {
2006
- name: input.name,
2007
- active: true,
2008
- images: input.fields.image?.url ? [input.fields.image.url] : undefined,
2009
- description: input.fields.description || '',
2010
- metadata: {
2011
- slug: input.fields.slug,
2012
- },
2013
- })
2014
-
2015
- const { image, ...fieldsNoImage } = input.fields
2016
-
2017
- await client
2018
- .update(products)
2019
- .set({
2020
- name: input.name,
2021
- quantityAvailable: input.quantityAvailable,
2022
- status: 1,
2023
- fields: {
2024
- ...fieldsNoImage,
2025
- ...(image?.url && { image }),
2026
- },
2027
- type: input.type,
2028
- })
2029
- .where(eq(products.id, currentProduct.id))
2030
-
2031
- return adapter.getProduct(currentProduct.id)
92
+ )
93
+ const createEventWorkflowOperations = createEventWorkflow(context, {
94
+ getEvent: contentResourcePort.getEvent,
95
+ addResourceToProduct: catalogPort.addResourceToProduct,
96
+ createProduct: catalogPort.createProduct,
97
+ createCoupon: couponPort.createCoupon,
98
+ })
99
+ const createEventSeriesWorkflowOperations = createEventSeriesWorkflow(
100
+ context,
101
+ {
102
+ addResourceToProduct: catalogPort.addResourceToProduct,
103
+ createProduct: catalogPort.createProduct,
104
+ createCoupon: couponPort.createCoupon,
2032
105
  },
2033
- async createProduct(input: NewProduct) {
2034
- if (!paymentProvider) throw new Error('Payment provider not found')
2035
- const merchantAccount = merchantAccountSchema.nullish().parse(
2036
- await client.query.merchantAccount.findFirst({
2037
- where: (merchantAccount, { eq }) =>
2038
- eq(merchantAccount.label, 'stripe'),
2039
- }),
2040
- )
2041
-
2042
- if (!merchantAccount) {
2043
- throw new Error('Merchant account not found')
2044
- }
106
+ )
107
+ const createCohortWorkflowOperations = createCohortWorkflow(context, {
108
+ createProduct: catalogPort.createProduct,
109
+ createCoupon: couponPort.createCoupon,
110
+ })
111
+ const createWorkshopWorkflowOperations = createWorkshopWorkflow(context, {
112
+ createProduct: catalogPort.createProduct,
113
+ createCoupon: couponPort.createCoupon,
114
+ })
2045
115
 
2046
- const hash = guid()
2047
- const newProductId = slugify(`product-${hash}`)
2048
-
2049
- const newProduct = {
2050
- id: newProductId,
2051
- name: input.name,
2052
- status: 1,
2053
- type: input.type || 'self-paced',
2054
- quantityAvailable: input.quantityAvailable,
2055
- fields: {
2056
- state: input.state || 'draft',
2057
- visibility: input.visibility || 'unlisted',
2058
- slug: slugify(`${input.name}-${hash}`),
2059
- ...(input.openEnrollment && { openEnrollment: input.openEnrollment }),
2060
- ...(input.closeEnrollment && {
2061
- closeEnrollment: input.closeEnrollment,
2062
- }),
2063
- ...(typeof input.availableAfterDays === 'number' && {
2064
- availableAfterDays: input.availableAfterDays,
2065
- }),
2066
- ...(typeof input.accessDurationDays === 'number' && {
2067
- accessDurationDays: input.accessDurationDays,
2068
- }),
2069
- },
2070
- }
2071
-
2072
- await client.insert(products).values(newProduct)
2073
-
2074
- const priceHash = guid()
2075
- const newPriceId = `price-${priceHash}`
2076
-
2077
- await client.insert(prices).values({
2078
- id: newPriceId,
2079
- productId: newProductId,
2080
- unitAmount: input.price.toString(),
2081
- status: 1,
2082
- })
2083
-
2084
- const product = await adapter.getProduct(newProductId)
2085
-
2086
- const stripeProduct = await paymentProvider.createProduct({
2087
- name: input.name,
2088
- metadata: {
2089
- slug: product?.fields?.slug || null,
2090
- },
2091
- })
2092
-
2093
- const stripePrice = await paymentProvider.createPrice({
2094
- product: stripeProduct.id,
2095
- unit_amount: Math.floor(Number(input.price) * 100),
2096
- currency: 'usd',
2097
- nickname: input.name,
2098
- metadata: {
2099
- slug: product?.fields?.slug || null,
2100
- },
2101
- // Membership products get recurring billing
2102
- ...(input.type === 'membership' && {
2103
- recurring: { interval: input.billingInterval || 'year' },
2104
- }),
2105
- })
2106
-
2107
- const newMerchantProductId = `mproduct_${v4()}`
2108
-
2109
- await client.insert(merchantProduct).values({
2110
- id: newMerchantProductId,
2111
- merchantAccountId: merchantAccount.id,
2112
- productId: newProductId,
2113
- identifier: stripeProduct.id,
2114
- status: 1,
2115
- })
2116
-
2117
- const newMerchantPriceId = `mprice_${v4()}`
2118
- await client.insert(merchantPrice).values({
2119
- id: newMerchantPriceId,
2120
- merchantAccountId: merchantAccount.id,
2121
- merchantProductId: newMerchantProductId,
2122
- priceId: newPriceId,
2123
- identifier: stripePrice.id,
2124
- status: 1,
2125
- })
2126
-
2127
- // TODO: handle upgrades
2128
-
2129
- return product
2130
- },
2131
- async getProduct(
2132
- productSlugOrId?: string,
2133
- withResources: boolean = true,
2134
- ): Promise<Product | null> {
2135
- if (!productSlugOrId) {
2136
- return null
2137
- }
2138
-
2139
- try {
2140
- const productData = await client.query.products.findFirst({
2141
- where: and(
2142
- or(
2143
- eq(
2144
- sql`JSON_EXTRACT (${products.fields}, "$.slug")`,
2145
- `${productSlugOrId}`,
2146
- ),
2147
- eq(products.id, productSlugOrId),
2148
- ),
2149
- ),
2150
- with: {
2151
- price: true,
2152
- ...(withResources && {
2153
- resources: {
2154
- with: {
2155
- resource: {
2156
- with: {
2157
- resources: true,
2158
- },
2159
- },
2160
- },
2161
- },
2162
- }),
2163
- },
2164
- })
2165
- const parsedProduct = productSchema.safeParse(productData)
2166
- if (!parsedProduct.success) {
2167
- logger.error(parsedProduct.error, {
2168
- event: 'adapter.product.parse_failed',
2169
- productId: productData?.id,
2170
- productSlugOrId,
2171
- })
2172
- return null
2173
- }
2174
- return parsedProduct.data
2175
- } catch (e) {
2176
- logger.error(toError(e), {
2177
- event: 'adapter.product.lookup.failed',
2178
- productSlugOrId,
2179
- withResources,
2180
- })
2181
- return null
2182
- }
2183
- },
2184
- async getProductResources(
2185
- productId: string,
2186
- ): Promise<ContentResource[] | null> {
2187
- const contentResourceProductsForProduct = z
2188
- .array(ContentResourceProductSchema)
2189
- .nullable()
2190
- .parse(
2191
- await client.query.contentResourceProduct.findMany({
2192
- where: eq(contentResourceProduct.productId, productId),
2193
- }),
2194
- )
2195
-
2196
- if (!contentResourceProductsForProduct) {
2197
- return null
2198
- } else {
2199
- const contentResources = z.array(ContentResourceSchema).parse(
2200
- await client.query.contentResource.findMany({
2201
- where: inArray(
2202
- contentResource.id,
2203
- contentResourceProductsForProduct.map((crp) => crp.resourceId),
2204
- ),
2205
- }),
2206
- )
2207
- return contentResources
2208
- }
2209
- },
2210
- async getPurchaseCountForProduct(productId: string): Promise<number> {
2211
- return await client.query.purchases
2212
- .findMany({
2213
- where: and(
2214
- eq(purchaseTable.productId, productId),
2215
- inArray(purchaseTable.status, ['Valid', 'Restricted']),
2216
- ),
2217
- })
2218
- .then((res) => res.length)
2219
- },
2220
- async getPurchase(purchaseId: string): Promise<Purchase | null> {
2221
- const purchase = await client.query.purchases.findFirst({
2222
- where: eq(purchaseTable.id, purchaseId),
2223
- })
2224
-
2225
- return purchase ? purchaseSchema.parse(purchase) : null
2226
- },
2227
- async createPurchase(options): Promise<Purchase> {
2228
- const newPurchaseId = options.id || `purchase-${v4()}`
2229
- await client.insert(purchaseTable).values({
2230
- ...options,
2231
- id: newPurchaseId,
2232
- })
2233
-
2234
- const newPurchase = await adapter.getPurchase(newPurchaseId)
2235
-
2236
- return purchaseSchema.parse(newPurchase)
2237
- },
2238
-
2239
- async getPurchaseForStripeCharge(
2240
- stripeChargeId: string,
2241
- ): Promise<Purchase | null> {
2242
- logger.debug('getPurchaseForStripeCharge', { stripeChargeId })
2243
-
2244
- const chargeForPurchase = merchantChargeSchema.nullable().parse(
2245
- (await client.query.merchantCharge.findFirst({
2246
- where: eq(merchantCharge.identifier, stripeChargeId),
2247
- })) || null,
2248
- )
2249
-
2250
- if (!chargeForPurchase) {
2251
- // Expected during initial checkout processing — the merchant
2252
- // charge hasn't been created yet. Also expected when the
2253
- // payment-status-checker passes a PaymentIntent ID (pi_xxx)
2254
- // instead of a Charge ID (ch_xxx).
2255
- logger.debug('getPurchaseForStripeCharge: no matching charge', {
2256
- stripeChargeId,
2257
- idPrefix: stripeChargeId?.substring(0, 3),
2258
- })
2259
- return null
2260
- }
2261
-
2262
- const purchase = purchaseSchema.safeParse(
2263
- await client.query.purchases.findFirst({
2264
- where: eq(purchaseTable.merchantChargeId, chargeForPurchase.id),
2265
- with: {
2266
- user: true,
2267
- product: true,
2268
- bulkCoupon: true,
2269
- },
2270
- }),
2271
- )
2272
-
2273
- if (!purchase.success) {
2274
- return null
2275
- }
2276
-
2277
- return purchase.data
2278
- },
2279
- async getPurchaseByCheckoutSessionId(
2280
- checkoutSessionId: string,
2281
- ): Promise<Purchase | null> {
2282
- logger.debug('getPurchaseByCheckoutSessionId', { checkoutSessionId })
2283
-
2284
- // Find the MerchantSession by its identifier (the Stripe checkout session ID)
2285
- const session = await client.query.merchantSession.findFirst({
2286
- where: eq(merchantSession.identifier, checkoutSessionId),
2287
- })
2288
-
2289
- if (!session) {
2290
- logger.debug('getPurchaseByCheckoutSessionId: no matching session', {
2291
- checkoutSessionId,
2292
- })
2293
- return null
2294
- }
2295
-
2296
- const purchase = purchaseSchema.safeParse(
2297
- await client.query.purchases.findFirst({
2298
- where: eq(purchaseTable.merchantSessionId, String(session.id)),
2299
- with: {
2300
- user: true,
2301
- product: true,
2302
- bulkCoupon: true,
2303
- },
2304
- }),
2305
- )
2306
-
2307
- if (!purchase.success) {
2308
- return null
2309
- }
2310
-
2311
- return purchase.data
2312
- },
2313
- async getPurchaseUserTransferById(options: { id: string }): Promise<
2314
- | (PurchaseUserTransfer & {
2315
- sourceUser: User
2316
- targetUser: User | null
2317
- purchase: Purchase
2318
- })
2319
- | null
2320
- > {
2321
- const purchaseTransferData =
2322
- await client.query.purchaseUserTransfer.findFirst({
2323
- where: eq(purchaseUserTransfer.id, options.id),
2324
- with: {
2325
- sourceUser: true,
2326
- targetUser: true,
2327
- purchase: true,
2328
- },
2329
- })
2330
-
2331
- return purchaseUserTransferSchema
2332
- .merge(
2333
- z.object({
2334
- sourceUser: userSchema,
2335
- targetUser: userSchema.nullable(),
2336
- purchase: purchaseSchema,
2337
- organizationId: z.string().nullable(),
2338
- }),
2339
- )
2340
- .nullable()
2341
- .parse(purchaseTransferData)
2342
- },
2343
- async getPurchaseWithUser(purchaseId: string): Promise<
2344
- | (Purchase & {
2345
- user: User
2346
- })
2347
- | null
2348
- > {
2349
- const purchaseData =
2350
- (await client.query.purchases.findFirst({
2351
- where: eq(purchaseTable.id, purchaseId),
2352
- with: {
2353
- user: true,
2354
- },
2355
- })) || null
2356
-
2357
- const parsedPurchase = purchaseSchema
2358
- .merge(z.object({ user: userSchema }))
2359
- .nullable()
2360
- .safeParse(purchaseData)
2361
-
2362
- if (!parsedPurchase.success) {
2363
- logger.error(parsedPurchase.error, {
2364
- event: 'adapter.purchase.parse_failed',
2365
- purchaseId,
2366
- })
2367
- return null
2368
- }
2369
- return parsedPurchase.data
2370
- },
2371
- async getPurchasesForUser(userId?: string): Promise<Purchase[]> {
2372
- if (!userId) {
2373
- return []
2374
- }
2375
-
2376
- const visiblePurchaseStates = ['Valid', 'Refunded', 'Restricted']
2377
-
2378
- const userPurchases = await client.query.purchases.findMany({
2379
- where: and(
2380
- eq(purchaseTable.userId, userId),
2381
- inArray(purchaseTable.status, visiblePurchaseStates),
2382
- ),
2383
- with: {
2384
- user: true,
2385
- product: true,
2386
- bulkCoupon: true,
2387
- },
2388
- orderBy: asc(purchaseTable.createdAt),
2389
- })
2390
-
2391
- const parsedPurchases = z.array(purchaseSchema).safeParse(userPurchases)
2392
-
2393
- if (!parsedPurchases.success) {
2394
- logger.error(parsedPurchases.error, {
2395
- event: 'adapter.purchase.list.parse_failed',
2396
- purchaseCount: userPurchases.length,
2397
- userId,
2398
- })
2399
- return []
2400
- }
2401
-
2402
- return parsedPurchases.data
2403
- },
2404
- async getEntitlementsForUser(params: {
2405
- userId: string
2406
- sourceType?: string
2407
- entitlementType?: string
2408
- }): Promise<Entitlement[]> {
2409
- const { userId, sourceType, entitlementType } = params
2410
-
2411
- if (!userId) {
2412
- return []
2413
- }
2414
-
2415
- const conditions = [eq(entitlementTable.userId, userId)]
2416
-
2417
- if (sourceType) {
2418
- conditions.push(eq(entitlementTable.sourceType, sourceType))
2419
- }
2420
-
2421
- if (entitlementType) {
2422
- conditions.push(eq(entitlementTable.entitlementType, entitlementType))
2423
- }
2424
-
2425
- // Only return active entitlements (not deleted, not expired)
2426
- conditions.push(isNull(entitlementTable.deletedAt))
2427
- const expiresCondition = or(
2428
- isNull(entitlementTable.expiresAt),
2429
- gte(entitlementTable.expiresAt, sql`CURRENT_TIMESTAMP`),
2430
- )
2431
- if (expiresCondition) {
2432
- conditions.push(expiresCondition)
2433
- }
2434
-
2435
- const userEntitlements = await client.query.entitlements.findMany({
2436
- where: conditions.length > 0 ? and(...conditions) : undefined,
2437
- orderBy: asc(entitlementTable.createdAt),
2438
- })
2439
-
2440
- const parsedEntitlements = z
2441
- .array(entitlementSchema)
2442
- .safeParse(userEntitlements)
2443
-
2444
- if (!parsedEntitlements.success) {
2445
- logger.error(parsedEntitlements.error, {
2446
- entitlementCount: userEntitlements.length,
2447
- entitlementType,
2448
- event: 'adapter.entitlement.list.parse_failed',
2449
- sourceType,
2450
- userId,
2451
- })
2452
- return []
2453
- }
2454
-
2455
- return parsedEntitlements.data
2456
- },
2457
- async getEntitlementTypeByName(
2458
- name: string,
2459
- ): Promise<{ id: string; name: string } | null> {
2460
- const entitlementType = await client.query.entitlementTypes.findFirst({
2461
- where: eq(entitlementTypes.name, name),
2462
- })
2463
-
2464
- if (!entitlementType) {
2465
- return null
2466
- }
2467
-
2468
- return {
2469
- id: String(entitlementType.id),
2470
- name: String(entitlementType.name),
2471
- }
2472
- },
2473
- async getPurchaseDetails(
2474
- purchaseId: string,
2475
- userId: string,
2476
- ): Promise<{
2477
- purchase?: Purchase
2478
- existingPurchase?: Purchase | null
2479
- availableUpgrades: UpgradableProduct[]
2480
- }> {
2481
- const visiblePurchaseStates = ['Valid', 'Refunded', 'Restricted']
2482
-
2483
- const userPurchases = await client.query.purchases.findMany({
2484
- where: and(
2485
- eq(purchaseTable.userId, userId),
2486
- inArray(purchaseTable.status, visiblePurchaseStates),
2487
- ),
2488
- with: {
2489
- user: true,
2490
- product: true,
2491
- bulkCoupon: true,
2492
- },
2493
- orderBy: asc(purchaseTable.createdAt),
2494
- })
2495
-
2496
- const parsedPurchases = z.array(purchaseSchema).safeParse(userPurchases)
2497
-
2498
- const allPurchases = parsedPurchases.success ? parsedPurchases.data : []
2499
-
2500
- const thePurchase = await client.query.purchases.findFirst({
2501
- where: and(
2502
- eq(purchaseTable.id, purchaseId),
2503
- eq(purchaseTable.userId, userId),
2504
- ),
2505
- with: {
2506
- user: true,
2507
- product: true,
2508
- bulkCoupon: true,
2509
- },
2510
- })
2511
-
2512
- const parsedPurchase = purchaseSchema.safeParse(thePurchase)
2513
-
2514
- if (!parsedPurchase.success) {
2515
- logger.error(parsedPurchase.error, {
2516
- event: 'adapter.purchase.details.parse_failed',
2517
- purchaseId,
2518
- userId,
2519
- })
2520
- return {
2521
- availableUpgrades: [],
2522
- }
2523
- }
2524
-
2525
- const purchaseCanUpgrade = ['Valid', 'Restricted'].includes(
2526
- parsedPurchase.data.state || '',
2527
- )
2528
-
2529
- let availableUpgrades: any[] = []
2530
-
2531
- if (purchaseCanUpgrade) {
2532
- availableUpgrades = await client.query.upgradableProducts.findMany({
2533
- where: and(
2534
- eq(
2535
- upgradableProducts.upgradableFromId,
2536
- parsedPurchase.data.product?.id as string,
2537
- ),
2538
- not(
2539
- inArray(
2540
- upgradableProducts.upgradableToId,
2541
- allPurchases.map((p) => p.product?.id as string),
2542
- ),
2543
- ),
2544
- ),
2545
- with: {
2546
- upgradableTo: true,
2547
- upgradableFrom: true,
2548
- },
2549
- })
2550
- }
2551
-
2552
- const existingPurchase = purchaseSchema
2553
- .optional()
2554
- .nullable()
2555
- .parse(
2556
- (await client.query.purchases.findFirst({
2557
- where: and(
2558
- eq(purchaseTable.userId, userId),
2559
- eq(purchaseTable.productId, parsedPurchase.data.productId),
2560
- inArray(purchaseTable.status, ['Valid', 'Restricted']),
2561
- isNull(purchaseTable.bulkCouponId),
2562
- not(eq(purchaseTable.id, parsedPurchase.data.id)),
2563
- ),
2564
- with: {
2565
- user: true,
2566
- product: true,
2567
- bulkCoupon: true,
2568
- },
2569
- })) || null,
2570
- )
2571
-
2572
- return {
2573
- availableUpgrades: z
2574
- .array(upgradableProductSchema)
2575
- .parse(availableUpgrades),
2576
- existingPurchase,
2577
- purchase: parsedPurchase.data,
2578
- }
2579
- },
2580
- async getUserWithPurchasersByEmail(email: string): Promise<any | null> {
2581
- const user = await client.query.users.findFirst({
2582
- where: eq(users.email, email.trim().toLowerCase()),
2583
- with: {
2584
- // merchantCustomer: true,
2585
- roles: {
2586
- with: {
2587
- role: true,
2588
- },
2589
- },
2590
- purchases: {
2591
- with: {
2592
- bulkCoupon: true,
2593
- merchantCharge: true,
2594
- product: {
2595
- columns: {
2596
- id: true,
2597
- name: true,
2598
- key: true,
2599
- },
2600
- },
2601
- },
2602
- },
2603
- },
2604
- })
2605
-
2606
- return user
2607
- },
2608
- async getUserById(userId: string): Promise<User | null> {
2609
- const user = await client.query.users
2610
- .findFirst({
2611
- where: eq(users.id, userId),
2612
- with: {
2613
- roles: {
2614
- with: {
2615
- role: true,
2616
- },
2617
- },
2618
- },
2619
- })
2620
- .then(async (res) => {
2621
- if (res) {
2622
- return {
2623
- ...res,
2624
- roles: res.roles.map((r) => r.role),
2625
- }
2626
- }
2627
- })
2628
-
2629
- return userSchema.nullable().parse(user ?? null)
2630
- },
2631
- async pricesOfPurchasesTowardOneBundle({
2632
- userId,
2633
- bundleId,
2634
- }: {
2635
- userId: string | undefined
2636
- bundleId: string
2637
- }): Promise<Price[]> {
2638
- if (userId === undefined) return []
2639
-
2640
- const canUpgradeProducts = await client.query.upgradableProducts.findMany(
2641
- {
2642
- where: eq(upgradableProducts.upgradableToId, bundleId),
2643
- },
2644
- )
2645
-
2646
- const upgradableFrom = z.array(z.string()).parse(
2647
- canUpgradeProducts.map((product) => {
2648
- return product.upgradableFromId
2649
- }),
2650
- )
2651
-
2652
- const purchases = await client.query.purchases.findMany({
2653
- where: and(
2654
- eq(purchaseTable.userId, userId),
2655
- inArray(purchaseTable.productId, upgradableFrom),
2656
- ),
2657
- })
2658
-
2659
- const productsPurchased = z.array(z.string()).parse(
2660
- purchases.map((purchase) => {
2661
- return purchase.productId
2662
- }),
2663
- )
2664
-
2665
- if (productsPurchased.length === 0) return []
2666
-
2667
- const foundPrices = await client.query.prices.findMany({
2668
- where: inArray(prices.productId, productsPurchased),
2669
- })
2670
-
2671
- return z.array(priceSchema).parse(foundPrices)
2672
- },
2673
- async toggleLessonProgressForUser(options: {
2674
- userId: string
2675
- lessonId?: string
2676
- }): Promise<ResourceProgress | null> {
2677
- if (!options.lessonId) {
2678
- throw new Error('No lessonId provided')
2679
- }
2680
-
2681
- let lessonProgress = await client.query.resourceProgress.findFirst({
2682
- where: and(
2683
- eq(resourceProgress.userId, options.userId),
2684
- eq(resourceProgress.resourceId, options.lessonId),
2685
- ),
2686
- })
2687
-
2688
- const now = new Date()
2689
-
2690
- if (lessonProgress) {
2691
- await client
2692
- .update(resourceProgress)
2693
- .set({
2694
- completedAt: lessonProgress.completedAt ? null : now,
2695
- updatedAt: now,
2696
- })
2697
- .where(
2698
- and(
2699
- eq(resourceProgress.userId, options.userId),
2700
- eq(resourceProgress.resourceId, options.lessonId),
2701
- ),
2702
- )
2703
- } else {
2704
- await client.insert(resourceProgress).values({
2705
- userId: options.userId,
2706
- resourceId: options.lessonId,
2707
- completedAt: now,
2708
- updatedAt: now,
2709
- })
2710
- }
2711
-
2712
- lessonProgress = await client.query.resourceProgress.findFirst({
2713
- where: and(
2714
- eq(resourceProgress.userId, options.userId),
2715
- eq(resourceProgress.resourceId, options.lessonId),
2716
- ),
2717
- })
2718
-
2719
- const parsedLessonProgress =
2720
- resourceProgressSchema.safeParse(lessonProgress)
2721
-
2722
- if (!parsedLessonProgress.success) {
2723
- logger.error(parsedLessonProgress.error, {
2724
- event: 'adapter.lessonProgress.parse_failed',
2725
- lessonId: options.lessonId,
2726
- userId: options.userId,
2727
- })
2728
- return null
2729
- }
2730
-
2731
- return parsedLessonProgress.data
2732
- },
2733
- transferPurchaseToUser: async (options: {
2734
- purchaseId: string
2735
- targetUserId: string
2736
- sourceUserId: string
2737
- }) => {
2738
- const { purchaseId, targetUserId, sourceUserId } = options
2739
- const transferId = `put_${v4()}`
2740
- let purchase = await adapter.getPurchase(purchaseId)
2741
- if (!purchase) throw new Error('No purchase found')
2742
-
2743
- if (purchase.userId !== sourceUserId)
2744
- throw new Error('Invalid source user')
2745
-
2746
- await client.transaction(async (trx) => {
2747
- if (!purchase) throw new Error('No purchase found')
2748
- await trx
2749
- .update(purchaseTable)
2750
- .set({ userId: targetUserId })
2751
- .where(eq(purchaseTable.id, purchase.id))
2752
-
2753
- await trx.insert(purchaseUserTransfer).values({
2754
- id: transferId,
2755
- purchaseId: purchase.id,
2756
- sourceUserId: sourceUserId,
2757
- targetUserId: targetUserId,
2758
- transferState: 'COMPLETED',
2759
- completedAt: new Date(),
2760
- })
2761
-
2762
- purchase = await adapter.getPurchase(purchaseId)
2763
-
2764
- if (!purchase) throw new Error('No purchase found')
2765
-
2766
- if (paymentProvider && purchase.merchantChargeId) {
2767
- await trx
2768
- .update(merchantCharge)
2769
- .set({
2770
- userId: targetUserId,
2771
- })
2772
- .where(eq(merchantCharge.id, purchase.merchantChargeId))
2773
-
2774
- const updatedMerchantCharge = await adapter.getMerchantCharge(
2775
- purchase.merchantChargeId,
2776
- )
2777
-
2778
- if (!updatedMerchantCharge)
2779
- throw new Error('No merchant charge found')
2780
-
2781
- await trx
2782
- .update(merchantCustomer)
2783
- .set({ userId: targetUserId })
2784
- .where(
2785
- eq(merchantCustomer.id, updatedMerchantCharge.merchantCustomerId),
2786
- )
2787
-
2788
- const updatedMerchantCustomer = merchantCustomerSchema.parse(
2789
- await client.query.merchantCustomer.findFirst({
2790
- where: eq(
2791
- merchantCustomer.id,
2792
- updatedMerchantCharge.merchantCustomerId,
2793
- ),
2794
- }),
2795
- )
2796
-
2797
- const targetUser = userSchema.parse(
2798
- await client.query.users.findFirst({
2799
- where: eq(users.id, targetUserId),
2800
- }),
2801
- )
2802
-
2803
- const sourceUser = userSchema.parse(
2804
- await client.query.users.findFirst({
2805
- where: eq(users.id, sourceUserId),
2806
- }),
2807
- )
2808
-
2809
- await paymentProvider.updateCustomer(
2810
- updatedMerchantCustomer.identifier,
2811
- {
2812
- email: targetUser.email,
2813
- name: targetUser.name || sourceUser.name || '',
2814
- metadata: {
2815
- transferId,
2816
- transferredFrom: sourceUser.email,
2817
- transferredOn: new Date().toISOString(),
2818
- },
2819
- },
2820
- )
2821
- }
2822
- })
2823
- return await adapter.getPurchaseUserTransferById({
2824
- id: transferId,
2825
- })
2826
- },
2827
- transferPurchasesToNewUser(options: {
2828
- merchantCustomerId: string
2829
- userId: string
2830
- }): Promise<unknown> {
2831
- throw new Error('Method not implemented.')
2832
- },
2833
- async updatePurchaseStatusForCharge(
2834
- chargeId: string,
2835
- status: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',
2836
- ): Promise<Purchase | undefined> {
2837
- try {
2838
- logger.debug('adapter.purchase.status_update.started', {
2839
- chargeId,
2840
- status,
2841
- })
2842
- const merchantChargeForPurchase =
2843
- await client.query.merchantCharge.findFirst({
2844
- where: or(
2845
- eq(merchantCharge.identifier, chargeId),
2846
- eq(merchantCharge.id, chargeId),
2847
- ),
2848
- })
2849
-
2850
- logger.debug('adapter.purchase.status_update.charge_resolved', {
2851
- chargeId,
2852
- merchantChargeId: merchantChargeForPurchase?.id ?? null,
2853
- merchantChargeIdentifier:
2854
- merchantChargeForPurchase?.identifier ?? null,
2855
- })
2856
-
2857
- const parsedMerchantChargeForPurchase = merchantChargeSchema.parse(
2858
- merchantChargeForPurchase,
2859
- )
2860
-
2861
- if (!parsedMerchantChargeForPurchase)
2862
- throw new Error(`no-charge-found-for-purchase ${chargeId}`)
2863
-
2864
- const purchase = await client.query.purchases.findFirst({
2865
- where: eq(
2866
- purchaseTable.merchantChargeId,
2867
- parsedMerchantChargeForPurchase.id,
2868
- ),
2869
- })
2870
-
2871
- const parsedPurchase = purchaseSchema.nullable().parse(purchase)
2872
-
2873
- if (parsedPurchase) {
2874
- await client
2875
- .update(purchaseTable)
2876
- .set({ status: status })
2877
- .where(eq(purchaseTable.id, parsedPurchase.id))
2878
-
2879
- const newPurchase = await client.query.purchases.findFirst({
2880
- where: eq(purchaseTable.id, parsedPurchase.id),
2881
- })
2882
-
2883
- return purchaseSchema.optional().parse(newPurchase)
2884
- } else {
2885
- throw new Error(`no-purchase-found-for-charge ${chargeId}`)
2886
- }
2887
- } catch (e) {
2888
- logger.error(toError(e), {
2889
- chargeId,
2890
- event: 'adapter.purchase.status_update.failed',
2891
- status,
2892
- })
2893
- throw e
2894
- }
2895
- },
2896
- async updatePurchaseUserTransferTransferState(options: {
2897
- id: string
2898
- transferState: PurchaseUserTransferState
2899
- }): Promise<PurchaseUserTransfer | null> {
2900
- await client
2901
- .update(purchaseUserTransfer)
2902
- .set({
2903
- transferState: options.transferState,
2904
- })
2905
- .where(eq(purchaseUserTransfer.id, options.id))
2906
-
2907
- const purchaseUserTransferData =
2908
- (await client.query.purchaseUserTransfer.findFirst({
2909
- where: eq(purchaseUserTransfer.id, options.id),
2910
- })) || null
2911
-
2912
- return purchaseUserTransferSchema
2913
- .nullable()
2914
- .parse(purchaseUserTransferData)
2915
- },
2916
- addResourceToResource: async function (options) {
2917
- const { parentResourceId, childResourceId } = options
2918
-
2919
- const parentResourceData = await client.query.contentResource.findFirst({
2920
- where: or(
2921
- eq(
2922
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
2923
- parentResourceId,
2924
- ),
2925
- eq(contentResource.id, parentResourceId),
2926
- ),
2927
- with: {
2928
- resources: true,
2929
- },
2930
- })
2931
-
2932
- const parsedParentResource =
2933
- ContentResourceSchema.safeParse(parentResourceData)
2934
- if (!parsedParentResource.success) {
2935
- return null
2936
- }
2937
-
2938
- const parentResource = parsedParentResource.data
2939
-
2940
- await client.insert(contentResourceResource).values({
2941
- resourceOfId: parentResource.id,
2942
- resourceId: childResourceId,
2943
- position: parentResource.resources?.length || 0,
2944
- })
2945
-
2946
- const resourceJoin = await client.query.contentResourceResource.findFirst(
2947
- {
2948
- where: and(
2949
- eq(contentResourceResource.resourceOfId, parentResource.id),
2950
- eq(contentResourceResource.resourceId, childResourceId),
2951
- ),
2952
- with: {
2953
- resource: true,
2954
- },
2955
- },
2956
- )
2957
-
2958
- const parsedResourceJoin =
2959
- ContentResourceResourceSchema.safeParse(resourceJoin)
2960
- if (!parsedResourceJoin.success) {
2961
- return null
2962
- }
2963
-
2964
- return parsedResourceJoin.data
2965
- },
2966
- async removeResourceFromResource(options) {
2967
- const { childResourceId, parentResourceId } = options
2968
-
2969
- const parentResourceData = await client.query.contentResource.findFirst({
2970
- where: or(
2971
- eq(
2972
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
2973
- parentResourceId,
2974
- ),
2975
- eq(contentResource.id, parentResourceId),
2976
- ),
2977
- })
2978
-
2979
- const parsedParentResource =
2980
- ContentResourceSchema.safeParse(parentResourceData)
2981
- if (!parsedParentResource.success) {
2982
- return null
2983
- }
2984
-
2985
- const parentResource = parsedParentResource.data
2986
-
2987
- const resourceJoin = await client.query.contentResourceResource.findFirst(
2988
- {
2989
- where: and(
2990
- eq(contentResourceResource.resourceOfId, parentResource.id),
2991
- eq(contentResourceResource.resourceId, childResourceId),
2992
- ),
2993
- with: {
2994
- resource: true,
2995
- },
2996
- },
2997
- )
2998
-
2999
- const parsedResourceJoin =
3000
- ContentResourceResourceSchema.safeParse(resourceJoin)
3001
- if (!parsedResourceJoin.success) {
3002
- return null
3003
- }
3004
-
3005
- await client
3006
- .delete(contentResourceResource)
3007
- .where(
3008
- and(
3009
- eq(contentResourceResource.resourceOfId, parentResource.id),
3010
- eq(contentResourceResource.resourceId, childResourceId),
3011
- ),
3012
- )
3013
-
3014
- return parsedResourceJoin.data.resource
3015
- },
3016
- async updateContentResourceFields(options) {
3017
- if (!options.id) {
3018
- throw new Error('No content resource id.')
3019
- }
3020
-
3021
- const currentResource = await client
3022
- .select()
3023
- .from(contentResource)
3024
- .where(eq(contentResource.id, options.id))
3025
- .then((res) => res[0])
3026
-
3027
- await client
3028
- .update(contentResource)
3029
- .set({ fields: { ...currentResource.fields, ...options.fields } })
3030
- .where(eq(contentResource.id, options.id))
3031
-
3032
- const resource = await client.query.contentResource.findFirst({
3033
- where: eq(contentResource.id, options.id),
3034
- with: {
3035
- resources: {
3036
- with: {
3037
- resource: {
3038
- with: {
3039
- resources: {
3040
- with: {
3041
- resource: true,
3042
- },
3043
- orderBy: asc(contentResourceResource.position),
3044
- },
3045
- },
3046
- },
3047
- },
3048
- orderBy: asc(contentResourceResource.position),
3049
- },
3050
- },
3051
- })
3052
-
3053
- const parsedResource = ContentResourceSchema.safeParse(resource)
3054
-
3055
- if (!parsedResource.success) {
3056
- logger.error(parsedResource.error, {
3057
- event: 'adapter.contentResource.parse_failed',
3058
- resourceId: options.id,
3059
- })
3060
- return null
3061
- }
3062
-
3063
- return parsedResource.data
3064
- },
3065
- async getVideoResource(id) {
3066
- if (!id) {
3067
- return null
3068
- }
3069
-
3070
- const query = sql`
3071
- SELECT
3072
- id as id,
3073
- CAST(updatedAt AS DATETIME) as updatedAt,
3074
- CAST(createdAt AS DATETIME) as createdAt,
3075
- JSON_EXTRACT (${contentResource.fields}, "$.state") AS state,
3076
- JSON_EXTRACT (${contentResource.fields}, "$.duration") AS duration,
3077
- JSON_EXTRACT (${contentResource.fields}, "$.muxPlaybackId") AS muxPlaybackId,
3078
- JSON_EXTRACT (${contentResource.fields}, "$.muxAssetId") AS muxAssetId,
3079
- JSON_EXTRACT (${contentResource.fields}, "$.transcript") AS transcript,
3080
- JSON_EXTRACT (${contentResource.fields}, "$.chapters") AS chapters
3081
- FROM
3082
- ${contentResource}
3083
- WHERE
3084
- type = 'videoResource'
3085
- AND (id = ${id});
3086
-
3087
- `
3088
- return client
3089
- .execute(query)
3090
- .then((result: any) => {
3091
- const parsedResource = VideoResourceSchema.safeParse(result.rows[0])
3092
- return parsedResource.success ? parsedResource.data : null
3093
- })
3094
- .catch((error) => {
3095
- logger.error(toError(error), {
3096
- event: 'adapter.videoResource.lookup.failed',
3097
- resourceId: id,
3098
- })
3099
- throw error
3100
- })
3101
- },
3102
- async getParentResourceOfVideoResource(
3103
- videoResourceId: string,
3104
- ): Promise<ContentResource | null> {
3105
- if (!videoResourceId) {
3106
- return null
3107
- }
3108
-
3109
- // Use a SQL query to find the parent post or lesson of this video resource
3110
- const results = await client
3111
- .select()
3112
- .from(contentResource)
3113
- .innerJoin(
3114
- contentResourceResource,
3115
- eq(contentResourceResource.resourceOfId, contentResource.id),
3116
- )
3117
- .where(
3118
- and(
3119
- eq(contentResourceResource.resourceId, videoResourceId),
3120
- or(
3121
- eq(contentResource.type, 'post'),
3122
- eq(contentResource.type, 'lesson'),
3123
- ),
3124
- ),
3125
- )
3126
- .limit(1)
3127
-
3128
- if (!results || results.length === 0) {
3129
- return null
3130
- }
3131
-
3132
- const resourceData = results[0]?.ContentResource
3133
-
3134
- if (!resourceData) {
3135
- return null
3136
- }
3137
-
3138
- // Fetch the full resource with relationships
3139
- const fullResource = await client.query.contentResource.findFirst({
3140
- where: eq(contentResource.id, resourceData.id),
3141
- with: {
3142
- resources: true,
3143
- },
3144
- })
3145
-
3146
- if (!fullResource) {
3147
- return null
3148
- }
3149
-
3150
- return ContentResourceSchema.parse(fullResource)
3151
- },
3152
- async createContentResource(data) {
3153
- const id = data.id || crypto.randomUUID()
3154
-
3155
- await client.insert(contentResource).values({ ...data, id })
3156
-
3157
- const resource = await client.query.contentResource.findFirst({
3158
- where: eq(contentResource.id, id),
3159
- with: {
3160
- resources: {
3161
- with: {
3162
- resource: {
3163
- with: {
3164
- resources: {
3165
- with: {
3166
- resource: true,
3167
- },
3168
- orderBy: asc(contentResourceResource.position),
3169
- },
3170
- },
3171
- },
3172
- },
3173
- orderBy: asc(contentResourceResource.position),
3174
- },
3175
- },
3176
- })
3177
-
3178
- const parsedResource = ContentResourceSchema.safeParse(resource)
3179
-
3180
- if (!parsedResource.success) {
3181
- logger.error(parsedResource.error, {
3182
- event: 'adapter.contentResource.parse_failed',
3183
- resourceId: id,
3184
- resourceType: data.type,
3185
- })
3186
- throw new Error('Error parsing resource')
3187
- }
3188
-
3189
- return parsedResource.data
3190
- },
3191
- async getContentResource(data) {
3192
- const resource = await client.query.contentResource.findFirst({
3193
- where: or(
3194
- eq(contentResource.id, data),
3195
- eq(
3196
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
3197
- `${data}`,
3198
- ),
3199
- ),
3200
- with: {
3201
- resources: {
3202
- with: {
3203
- resource: {
3204
- with: {
3205
- resources: {
3206
- with: {
3207
- resource: {
3208
- with: {
3209
- resources: {
3210
- with: {
3211
- resource: true,
3212
- },
3213
- },
3214
- },
3215
- },
3216
- },
3217
- orderBy: asc(contentResourceResource.position),
3218
- },
3219
- },
3220
- },
3221
- },
3222
- orderBy: asc(contentResourceResource.position),
3223
- },
3224
- },
3225
- })
3226
-
3227
- const parsedResource = ContentResourceSchema.safeParse(resource)
3228
-
3229
- if (!parsedResource.success) {
3230
- logger.error(parsedResource.error, {
3231
- event: 'adapter.contentResource.parse_failed',
3232
- resourceId: resource?.id,
3233
- resourceIdOrSlug: data,
3234
- })
3235
- return null
3236
- }
3237
-
3238
- return parsedResource.data
3239
- },
3240
- async getEvent(
3241
- eventIdOrSlug: string,
3242
- options?: {
3243
- withResources?: boolean
3244
- withTags?: boolean
3245
- withProducts?: boolean
3246
- withPricing?: boolean
3247
- },
3248
- ): Promise<ContentResource | null> {
3249
- const {
3250
- withResources = false,
3251
- withTags = false,
3252
- withProducts = false,
3253
- withPricing = false,
3254
- } = options || {}
3255
-
3256
- const eventQuery = await client.query.contentResource.findFirst({
3257
- where: and(
3258
- or(
3259
- eq(contentResource.type, 'event'),
3260
- eq(contentResource.type, 'event-series'),
3261
- ),
3262
- or(
3263
- eq(contentResource.id, eventIdOrSlug),
3264
- eq(
3265
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
3266
- eventIdOrSlug,
3267
- ),
3268
- ),
3269
- ),
3270
- with: {
3271
- ...(withResources && {
3272
- resources: {
3273
- with: { resource: true },
3274
- orderBy: asc(contentResourceResource.position),
3275
- },
3276
- }),
3277
- ...(withTags && {
3278
- tags: {
3279
- with: { tag: true },
3280
- orderBy: asc(contentResourceTag.position),
3281
- },
3282
- }),
3283
- ...(withProducts && {
3284
- resourceProducts: {
3285
- with: {
3286
- product: {
3287
- ...(withPricing && {
3288
- with: { price: true },
3289
- }),
3290
- },
3291
- },
3292
- },
3293
- }),
3294
- },
3295
- })
3296
-
3297
- const parsedEvent = ContentResourceSchema.safeParse(eventQuery)
3298
- if (!parsedEvent.success) {
3299
- logger.debug('Error parsing event', {
3300
- eventQuery,
3301
- error: parsedEvent.error,
3302
- })
3303
- return null
3304
- }
3305
-
3306
- return parsedEvent.data
3307
- },
3308
- async addResourceToProduct(options: {
3309
- resource: ContentResource
3310
- productId: string
3311
- userId: string
3312
- }) {
3313
- const { resource, productId, userId } = options
3314
- const product = await adapter.getProduct(productId)
3315
-
3316
- if (!product) {
3317
- throw new Error(`Product not found for id (${productId})`)
3318
- }
3319
-
3320
- await client.insert(contentResourceProduct).values({
3321
- resourceId: resource.id,
3322
- productId,
3323
- position: product.resources?.length || 0,
3324
- metadata: {
3325
- addedBy: userId,
3326
- },
3327
- })
3328
-
3329
- const resourceProduct =
3330
- await client.query.contentResourceProduct.findFirst({
3331
- where: eq(contentResourceProduct.resourceId, resource.id),
3332
- with: {
3333
- resource: true,
3334
- product: true,
3335
- },
3336
- })
3337
-
3338
- return (
3339
- ContentResourceProductSchema.safeParse(resourceProduct).data || null
3340
- )
3341
- },
3342
- async createEvent(
3343
- input: {
3344
- type: 'event'
3345
- fields: {
3346
- title: string
3347
- startsAt?: Date | null | undefined
3348
- endsAt?: Date | null | undefined
3349
- description?: string | null | undefined
3350
- price?: number | null | undefined
3351
- quantity?: number | null | undefined
3352
- state?: string | null | undefined
3353
- visibility?: string | null | undefined
3354
- slug?: string | null | undefined
3355
- tagIds?:
3356
- | { id: string; fields: Record<string, any> }[]
3357
- | null
3358
- | undefined
3359
- }
3360
- coupon?: {
3361
- enabled: boolean
3362
- percentageDiscount: string
3363
- expires: Date
3364
- }
3365
- },
3366
- userId: string,
3367
- ) {
3368
- const hash = guid()
3369
- const newEventId = slugify(`${input.type}~${hash}`)
3370
-
3371
- const newEvent = {
3372
- id: newEventId,
3373
- type: 'event',
3374
- fields: {
3375
- title: input.fields.title,
3376
- state: 'draft',
3377
- visibility: 'public',
3378
- slug: slugify(`${input.fields.title}~${hash}`),
3379
- description: input.fields.description,
3380
- startsAt: input.fields.startsAt,
3381
- endsAt: input.fields.endsAt,
3382
- },
3383
- createdById: userId,
3384
- }
3385
-
3386
- await client.insert(contentResource).values(newEvent)
3387
- const event = await adapter.getEvent(newEventId, {
3388
- withResources: true,
3389
- withTags: true,
3390
- withProducts: true,
3391
- withPricing: true,
3392
- })
3393
-
3394
- const parsedEvent = ContentResourceSchema.safeParse(event)
3395
-
3396
- if (!parsedEvent.success) {
3397
- logger.error(new Error('Error parsing event resource'))
3398
- throw new Error('Error parsing event resource')
3399
- }
3400
-
3401
- // if price is provided, create a product and associate it with the event
3402
- if (input.fields.price && input.fields.price > 0) {
3403
- try {
3404
- const product = await adapter.createProduct({
3405
- name: input.fields.title,
3406
- price: input.fields.price,
3407
- quantityAvailable: input.fields.quantity ?? -1,
3408
- type: 'live',
3409
- state: 'published',
3410
- visibility: 'public',
3411
- })
3412
- if (product) {
3413
- const resourceProduct = await adapter.addResourceToProduct({
3414
- resource: parsedEvent.data,
3415
- productId: product.id,
3416
- userId,
3417
- })
3418
- if (!resourceProduct) {
3419
- logger.error(new Error('Failed to add resource to product'))
3420
- logger.debug('event.addResourceToProduct.failed', {
3421
- eventId: newEventId,
3422
- productId: product.id,
3423
- userId,
3424
- })
3425
- } else {
3426
- logger.debug('event.addResourceToProduct.success', {
3427
- eventId: newEventId,
3428
- productId: product.id,
3429
- userId,
3430
- })
3431
- }
3432
- // Create coupon if enabled
3433
- if (
3434
- input.coupon?.enabled &&
3435
- input.coupon.percentageDiscount &&
3436
- input.coupon.expires
3437
- ) {
3438
- try {
3439
- const finalExpires = normalizeExpirationDate(
3440
- input.coupon.expires,
3441
- )
3442
- const couponInput = {
3443
- quantity: '1',
3444
- maxUses: -1,
3445
- expires: finalExpires || null,
3446
- restrictedToProductId: product.id,
3447
- percentageDiscount: input.coupon.percentageDiscount,
3448
- status: 1,
3449
- default: true,
3450
- fields: {
3451
- bypassSoldOut: false,
3452
- },
3453
- }
3454
- await adapter.createCoupon(couponInput)
3455
- logger.debug('event.create.coupon.success', {
3456
- eventId: newEventId,
3457
- productId: product.id,
3458
- userId,
3459
- percentageDiscount: input.coupon.percentageDiscount,
3460
- })
3461
- } catch (couponError) {
3462
- logger.error(
3463
- new Error(
3464
- `Failed to create coupon for event: ${couponError}`,
3465
- ),
3466
- )
3467
- logger.debug('event.create.coupon.failed', {
3468
- eventId: newEventId,
3469
- productId: product.id,
3470
- userId,
3471
- })
3472
- // Don't throw here - event creation should succeed even if coupon fails
3473
- }
3474
- }
3475
- } else {
3476
- logger.error(new Error('Failed to create product'))
3477
- logger.debug('event.create.product.failed', {
3478
- eventId: newEventId,
3479
- userId,
3480
- price: input.fields.price,
3481
- })
3482
- }
3483
- } catch (error) {
3484
- logger.error(
3485
- new Error(`Error creating and associating product: ${error}`),
3486
- )
3487
- logger.debug('event.create.product.failed', {
3488
- eventId: newEventId,
3489
- userId,
3490
- price: input.fields.price,
3491
- })
3492
- }
3493
- }
3494
-
3495
- // if we provide tagIds, we need to associate them with the event
3496
- if (input.fields.tagIds) {
3497
- try {
3498
- await client.insert(contentResourceTag).values(
3499
- input.fields.tagIds.map((tag) => ({
3500
- contentResourceId: newEventId,
3501
- tagId: tag.id,
3502
- createdAt: new Date(),
3503
- updatedAt: new Date(),
3504
- position: 0,
3505
- })),
3506
- )
3507
- logger.debug('event.create.tags.success', {
3508
- eventId: newEventId,
3509
- userId,
3510
- tagIds: input.fields.tagIds,
3511
- })
3512
- } catch (error) {
3513
- logger.error(new Error(`Error associating tags with event: ${error}`))
3514
- logger.debug('event.create.tags.failed', {
3515
- eventId: newEventId,
3516
- userId,
3517
- tagIds: input.fields.tagIds,
3518
- })
3519
- }
3520
- }
3521
-
3522
- return parsedEvent.data
3523
- },
3524
- async createEventSeries(
3525
- input: {
3526
- eventSeries: {
3527
- type: 'event-series'
3528
- fields: {
3529
- title: string
3530
- description?: string | undefined
3531
- tagIds?:
3532
- | { id: string; fields: { label: string; name: string } }[]
3533
- | null
3534
- | undefined
3535
- }
3536
- sharedFields: {
3537
- price: number | null | undefined
3538
- quantity: number | null | undefined
3539
- }
3540
- }
3541
- childEvents: Array<{
3542
- type: 'event'
3543
- fields: {
3544
- title: string
3545
- startsAt: Date | null | undefined
3546
- endsAt: Date | null | undefined
3547
- description?: string | undefined
3548
- tagIds?:
3549
- | { id: string; fields: { label: string; name: string } }[]
3550
- | null
3551
- | undefined
3552
- }
3553
- }>
3554
- coupon?:
3555
- | {
3556
- enabled: boolean
3557
- expires?: Date | undefined
3558
- percentageDiscount?: string | undefined
3559
- }
3560
- | undefined
3561
- },
3562
- userId: string,
3563
- ) {
3564
- const { eventSeries: eventSeriesInput, childEvents: childEventsInput } =
3565
- input
3566
-
3567
- if (childEventsInput.length === 0) {
3568
- throw new Error('At least one event is required')
3569
- }
3570
-
3571
- // Execute all database operations within a transaction
3572
- const result = await client.transaction(async (tx) => {
3573
- // Step 1: Create the event series resource
3574
- const eventSeriesHash = guid()
3575
- const eventSeriesResourceId = slugify(`event-series~${eventSeriesHash}`)
3576
-
3577
- const newEventSeries = {
3578
- id: eventSeriesResourceId,
3579
- type: 'event-series',
3580
- fields: {
3581
- ...eventSeriesInput.fields,
3582
- title: eventSeriesInput.fields.title,
3583
- state: 'draft',
3584
- visibility: 'public',
3585
- slug: slugify(
3586
- `${eventSeriesInput.fields.title}~${eventSeriesHash}`,
3587
- ),
3588
- },
3589
- createdById: userId,
3590
- }
3591
-
3592
- await tx.insert(contentResource).values(newEventSeries)
3593
-
3594
- // Fetch the created event series with relations
3595
- const eventSeriesResource = await tx.query.contentResource.findFirst({
3596
- where: eq(contentResource.id, eventSeriesResourceId),
3597
- with: {
3598
- resources: {
3599
- with: {
3600
- resource: {
3601
- with: {
3602
- resources: {
3603
- with: {
3604
- resource: true,
3605
- },
3606
- orderBy: asc(contentResourceResource.position),
3607
- },
3608
- },
3609
- },
3610
- },
3611
- orderBy: asc(contentResourceResource.position),
3612
- },
3613
- tags: {
3614
- with: {
3615
- tag: true,
3616
- },
3617
- orderBy: asc(contentResourceTag.position),
3618
- },
3619
- resourceProducts: {
3620
- with: {
3621
- product: {
3622
- with: {
3623
- price: true,
3624
- },
3625
- },
3626
- },
3627
- },
3628
- },
3629
- })
3630
-
3631
- const parsedEventSeries =
3632
- ContentResourceSchema.safeParse(eventSeriesResource)
3633
- if (!parsedEventSeries.success) {
3634
- throw new Error('Error parsing event series resource')
3635
- }
3636
-
3637
- // Step 2: Associate tags with event series
3638
- if (eventSeriesInput.fields.tagIds) {
3639
- await tx.insert(contentResourceTag).values(
3640
- eventSeriesInput.fields.tagIds.map((tag) => ({
3641
- contentResourceId: eventSeriesResourceId,
3642
- tagId: tag.id,
3643
- createdAt: new Date(),
3644
- updatedAt: new Date(),
3645
- position: 0,
3646
- })),
3647
- )
3648
- }
3649
-
3650
- // Step 3: Create child events and associate them
3651
- const createdChildEvents = []
3652
-
3653
- for (let i = 0; i < childEventsInput.length; i++) {
3654
- const childEventInput = childEventsInput[i]
3655
-
3656
- if (!childEventInput) {
3657
- throw new Error(`Child event input is required`)
3658
- }
3659
-
3660
- // Create child event
3661
- const childEventHash = guid()
3662
- const childEventResourceId = slugify(`event~${childEventHash}`)
3663
-
3664
- const newChildEvent = {
3665
- id: childEventResourceId,
3666
- type: 'event',
3667
- fields: {
3668
- title: childEventInput.fields.title,
3669
- startsAt: childEventInput.fields.startsAt,
3670
- endsAt: childEventInput.fields.endsAt,
3671
- description: childEventInput.fields.description,
3672
- state: 'draft',
3673
- visibility: 'public',
3674
- slug: slugify(
3675
- `${childEventInput.fields.title}~${childEventHash}`,
3676
- ),
3677
- // No price/quantity - they're on the event series
3678
- price: null,
3679
- quantity: null,
3680
- },
3681
- createdById: userId,
3682
- }
3683
-
3684
- await tx.insert(contentResource).values(newChildEvent)
3685
-
3686
- // Fetch the created child event with relations
3687
- const childEventResource = await tx.query.contentResource.findFirst({
3688
- where: eq(contentResource.id, childEventResourceId),
3689
- with: {
3690
- resources: {
3691
- with: {
3692
- resource: {
3693
- with: {
3694
- resources: {
3695
- with: {
3696
- resource: true,
3697
- },
3698
- orderBy: asc(contentResourceResource.position),
3699
- },
3700
- },
3701
- },
3702
- },
3703
- orderBy: asc(contentResourceResource.position),
3704
- },
3705
- tags: {
3706
- with: {
3707
- tag: true,
3708
- },
3709
- orderBy: asc(contentResourceTag.position),
3710
- },
3711
- resourceProducts: {
3712
- with: {
3713
- product: {
3714
- with: {
3715
- price: true,
3716
- },
3717
- },
3718
- },
3719
- },
3720
- },
3721
- })
3722
-
3723
- const parsedChildEvent =
3724
- ContentResourceSchema.safeParse(childEventResource)
3725
- if (!parsedChildEvent.success) {
3726
- throw new Error(`Error parsing child event resource ${i + 1}`)
3727
- }
3728
-
3729
- // Associate tags with child event
3730
- if (childEventInput.fields.tagIds) {
3731
- await tx.insert(contentResourceTag).values(
3732
- childEventInput.fields.tagIds.map((tag) => ({
3733
- contentResourceId: childEventResourceId,
3734
- tagId: tag.id,
3735
- createdAt: new Date(),
3736
- updatedAt: new Date(),
3737
- position: 0,
3738
- })),
3739
- )
3740
- }
3741
-
3742
- // Associate child event with event series
3743
- await tx.insert(contentResourceResource).values({
3744
- resourceOfId: eventSeriesResourceId,
3745
- resourceId: childEventResourceId,
3746
- position: i,
3747
- })
3748
-
3749
- createdChildEvents.push(parsedChildEvent.data)
3750
- }
3751
-
3752
- return {
3753
- eventSeries: parsedEventSeries.data,
3754
- childEvents: createdChildEvents,
3755
- }
3756
- })
3757
-
3758
- const { eventSeries, childEvents } = result
3759
-
3760
- // Step 4: Handle product creation outside transaction
3761
- if (
3762
- eventSeriesInput.sharedFields.price &&
3763
- eventSeriesInput.sharedFields.price > 0
3764
- ) {
3765
- try {
3766
- const product = await adapter.createProduct({
3767
- name: eventSeriesInput.fields.title,
3768
- price: eventSeriesInput.sharedFields.price,
3769
- quantityAvailable: eventSeriesInput.sharedFields.quantity ?? -1,
3770
- type: 'live',
3771
- state: 'published',
3772
- visibility: 'public',
3773
- })
3774
- if (product) {
3775
- await adapter.addResourceToProduct({
3776
- resource: eventSeries,
3777
- productId: product.id,
3778
- userId,
3779
- })
3780
-
3781
- // Create coupon if enabled
3782
- if (
3783
- input.coupon?.enabled &&
3784
- input.coupon.percentageDiscount &&
3785
- input.coupon.expires
3786
- ) {
3787
- try {
3788
- let finalExpires = input.coupon.expires
3789
- if (finalExpires instanceof Date) {
3790
- finalExpires = new Date(
3791
- Date.UTC(
3792
- finalExpires.getUTCFullYear(),
3793
- finalExpires.getUTCMonth(),
3794
- finalExpires.getUTCDate(),
3795
- 23,
3796
- 59,
3797
- 59,
3798
- 0,
3799
- ),
3800
- )
3801
- }
3802
- const couponInput = {
3803
- quantity: '1',
3804
- maxUses: 1,
3805
- expires: finalExpires,
3806
- restrictedToProductId: product.id,
3807
- percentageDiscount: input.coupon.percentageDiscount,
3808
- status: 1,
3809
- default: true,
3810
- fields: {},
3811
- }
3812
- await adapter.createCoupon(couponInput)
3813
- logger.debug('event.series.coupon.success', {
3814
- eventSeriesId: eventSeries.id,
3815
- productId: product.id,
3816
- userId,
3817
- percentageDiscount: input.coupon.percentageDiscount,
3818
- })
3819
- } catch (couponError) {
3820
- logger.error(
3821
- new Error(
3822
- `Error creating coupon for event series: ${couponError}`,
3823
- ),
3824
- )
3825
- logger.debug('event.series.coupon.failed', {
3826
- eventSeriesId: eventSeries.id,
3827
- productId: product.id,
3828
- userId,
3829
- })
3830
- // Don't throw here - event series creation should succeed even if coupon fails
3831
- }
3832
- }
3833
- } else {
3834
- logger.error(new Error('Failed to create product'))
3835
- logger.debug('event.series.product.failed', {
3836
- eventSeriesId: eventSeries.id,
3837
- userId,
3838
- price: eventSeriesInput.sharedFields.price,
3839
- })
3840
- }
3841
- } catch (error) {
3842
- logger.error(
3843
- new Error(`Error creating and associating product: ${error}`),
3844
- )
3845
- logger.debug('event.series.product.creation.failed', {
3846
- eventSeriesId: eventSeries.id,
3847
- userId,
3848
- })
3849
- // Note: We don't throw here since the core data is already committed
3850
- }
3851
- }
3852
-
3853
- return { eventSeries, childEvents }
3854
- },
3855
- async createCohort(
3856
- input: {
3857
- cohort: {
3858
- title: string
3859
- description?: string
3860
- tagIds?:
3861
- | { id: string; fields: { label: string; name: string } }[]
3862
- | null
3863
- }
3864
- dates: {
3865
- start: Date
3866
- end: Date
3867
- }
3868
- createProduct?: boolean
3869
- pricing: {
3870
- price?: number | null
3871
- }
3872
- coupon?: {
3873
- enabled: boolean
3874
- percentageDiscount?: string
3875
- expires?: Date
3876
- }
3877
- workshops: { id: string }[]
3878
- },
3879
- userId: string,
3880
- ) {
3881
- const hash = guid()
3882
- const cohortId = `cohort~${hash}`
3883
-
3884
- // Transaction only handles database operations
3885
- const cohort = await client.transaction(async (tx) => {
3886
- // Create cohort content resource
3887
- await tx.insert(contentResource).values({
3888
- id: cohortId,
3889
- type: 'cohort',
3890
- createdById: userId,
3891
- fields: {
3892
- title: input.cohort.title,
3893
- description: input.cohort.description,
3894
- state: 'draft',
3895
- visibility: 'unlisted',
3896
- slug: slugify(`${input.cohort.title}~${hash}`),
3897
- startsAt: input.dates.start,
3898
- endsAt: input.dates.end,
3899
- },
3900
- })
3901
-
3902
- // Fetch created cohort
3903
- const cohort = await tx.query.contentResource.findFirst({
3904
- where: eq(contentResource.id, cohortId),
3905
- with: {
3906
- resources: {
3907
- with: {
3908
- resource: true,
3909
- },
3910
- orderBy: asc(contentResourceResource.position),
3911
- },
3912
- },
3913
- })
3914
-
3915
- if (!cohort) {
3916
- throw new Error('Failed to create cohort')
3917
- }
3918
-
3919
- const parsedCohort = ContentResourceSchema.safeParse(cohort)
3920
- if (!parsedCohort.success) {
3921
- throw new Error('Invalid cohort data')
3922
- }
3923
-
3924
- // Associate tags with cohort
3925
- if (input.cohort.tagIds) {
3926
- await tx.insert(contentResourceTag).values(
3927
- input.cohort.tagIds.map((tag) => ({
3928
- contentResourceId: cohortId,
3929
- tagId: tag.id,
3930
- createdAt: new Date(),
3931
- updatedAt: new Date(),
3932
- position: 0,
3933
- })),
3934
- )
3935
- }
3936
-
3937
- // Link workshops to cohort
3938
- let position = 0
3939
- for (const workshop of input.workshops) {
3940
- await tx.insert(contentResourceResource).values({
3941
- resourceOfId: cohortId,
3942
- resourceId: workshop.id,
3943
- position,
3944
- })
3945
- position++
3946
- }
3947
-
3948
- return parsedCohort.data
3949
- })
3950
-
3951
- // Handle product and coupon creation outside transaction
3952
- let product: any = null
3953
- if (
3954
- input.createProduct &&
3955
- input.pricing.price &&
3956
- input.pricing.price > 0
3957
- ) {
3958
- try {
3959
- product = await adapter.createProduct({
3960
- name: input.cohort.title,
3961
- price: input.pricing.price,
3962
- quantityAvailable: -1,
3963
- type: 'cohort',
3964
- state: 'published',
3965
- visibility: 'public',
3966
- openEnrollment: new Date().toISOString(),
3967
- closeEnrollment: input.dates.start.toISOString(),
3968
- })
3969
-
3970
- if (product) {
3971
- // Link product to cohort resource
3972
- await client.insert(contentResourceProduct).values({
3973
- resourceId: cohortId,
3974
- productId: product.id,
3975
- position: 0,
3976
- metadata: {
3977
- addedBy: userId,
3978
- },
3979
- })
3980
-
3981
- // Create coupon if enabled
3982
- if (
3983
- input.coupon?.enabled &&
3984
- input.coupon.percentageDiscount &&
3985
- input.coupon.expires
3986
- ) {
3987
- try {
3988
- const finalExpires = normalizeExpirationDate(
3989
- input.coupon.expires,
3990
- )
3991
- await adapter.createCoupon({
3992
- percentageDiscount: input.coupon.percentageDiscount,
3993
- expires: finalExpires || null,
3994
- restrictedToProductId: product.id,
3995
- default: true,
3996
- maxUses: -1,
3997
- quantity: '-1',
3998
- status: 1,
3999
- fields: {},
4000
- })
4001
- logger.debug('cohort.create.coupon.success', {
4002
- cohortId,
4003
- productId: product.id,
4004
- userId,
4005
- percentageDiscount: input.coupon.percentageDiscount,
4006
- })
4007
- } catch (couponError) {
4008
- logger.error(
4009
- new Error(
4010
- `Failed to create coupon for cohort: ${couponError}`,
4011
- ),
4012
- )
4013
- logger.debug('cohort.create.coupon.failed', {
4014
- cohortId,
4015
- productId: product.id,
4016
- userId,
4017
- })
4018
- // Don't throw - cohort creation should succeed even if coupon fails
4019
- }
4020
- }
4021
- }
4022
- } catch (productError) {
4023
- logger.error(
4024
- new Error(`Failed to create product for cohort: ${productError}`),
4025
- )
4026
- logger.debug('cohort.create.product.failed', {
4027
- cohortId,
4028
- userId,
4029
- price: input.pricing.price,
4030
- })
4031
- // Don't throw - cohort creation should succeed even if product fails
4032
- }
4033
- }
4034
-
4035
- return {
4036
- cohort,
4037
- product,
4038
- }
4039
- },
4040
- async createWorkshop(
4041
- input: {
4042
- workshop: {
4043
- title: string
4044
- description?: string
4045
- tagIds?:
4046
- | { id: string; fields: { label: string; name: string } }[]
4047
- | null
4048
- }
4049
- createProduct?: boolean
4050
- pricing: {
4051
- price?: number | null
4052
- quantity?: number | null
4053
- }
4054
- coupon?: {
4055
- enabled: boolean
4056
- percentageDiscount?: string
4057
- expires?: Date
4058
- }
4059
- structure: Array<
4060
- | {
4061
- type: 'section'
4062
- title: string
4063
- lessons: { title: string; videoResourceId?: string }[]
4064
- }
4065
- | { type: 'lesson'; title: string; videoResourceId?: string }
4066
- >
4067
- },
4068
- userId: string,
4069
- ) {
4070
- const hash = guid()
4071
- const workshopId = `workshop~${hash}`
4072
-
4073
- // Transaction only handles database operations
4074
- const result = await client.transaction(async (tx) => {
4075
- // Create workshop content resource
4076
- await tx.insert(contentResource).values({
4077
- id: workshopId,
4078
- type: 'workshop',
4079
- createdById: userId,
4080
- fields: {
4081
- title: input.workshop.title,
4082
- description: input.workshop.description,
4083
- state: 'draft',
4084
- visibility: 'unlisted',
4085
- slug: slugify(`${input.workshop.title}~${hash}`),
4086
- },
4087
- })
4088
-
4089
- // Fetch created workshop
4090
- const workshop = await tx.query.contentResource.findFirst({
4091
- where: eq(contentResource.id, workshopId),
4092
- with: {
4093
- resources: {
4094
- with: {
4095
- resource: {
4096
- with: {
4097
- resources: {
4098
- with: {
4099
- resource: true,
4100
- },
4101
- orderBy: asc(contentResourceResource.position),
4102
- },
4103
- },
4104
- },
4105
- },
4106
- orderBy: asc(contentResourceResource.position),
4107
- },
4108
- },
4109
- })
4110
-
4111
- if (!workshop) {
4112
- throw new Error('Failed to create workshop')
4113
- }
4114
-
4115
- const parsedWorkshop = ContentResourceSchema.safeParse(workshop)
4116
- if (!parsedWorkshop.success) {
4117
- throw new Error('Invalid workshop data')
4118
- }
4119
-
4120
- // Associate tags with workshop
4121
- if (input.workshop.tagIds) {
4122
- await tx.insert(contentResourceTag).values(
4123
- input.workshop.tagIds.map((tag) => ({
4124
- contentResourceId: workshopId,
4125
- tagId: tag.id,
4126
- createdAt: new Date(),
4127
- updatedAt: new Date(),
4128
- position: 0,
4129
- })),
4130
- )
4131
- }
4132
-
4133
- // Create sections and lessons
4134
- const createdSections: ContentResource[] = []
4135
- const createdLessons: ContentResource[] = []
4136
- let position = 0
4137
-
4138
- for (const item of input.structure) {
4139
- if (item.type === 'section') {
4140
- // Create section
4141
- const sectionHash = guid()
4142
- const sectionId = `section~${sectionHash}`
4143
-
4144
- await tx.insert(contentResource).values({
4145
- id: sectionId,
4146
- type: 'section',
4147
- createdById: userId,
4148
- fields: {
4149
- title: item.title,
4150
- state: 'draft',
4151
- visibility: 'unlisted',
4152
- slug: slugify(`${item.title}~${sectionHash}`),
4153
- },
4154
- })
4155
-
4156
- // Link section to workshop
4157
- await tx.insert(contentResourceResource).values({
4158
- resourceOfId: workshopId,
4159
- resourceId: sectionId,
4160
- position,
4161
- })
4162
-
4163
- const section = await tx.query.contentResource.findFirst({
4164
- where: eq(contentResource.id, sectionId),
4165
- })
4166
-
4167
- if (section) {
4168
- createdSections.push(section as ContentResource)
4169
- }
4170
-
4171
- // Create lessons in section
4172
- let lessonPosition = 0
4173
- for (const lessonData of item.lessons) {
4174
- const lessonHash = guid()
4175
- const lessonId = `lesson_${lessonHash}`
4176
-
4177
- await tx.insert(contentResource).values({
4178
- id: lessonId,
4179
- type: 'lesson',
4180
- createdById: userId,
4181
- fields: {
4182
- title: lessonData.title,
4183
- state: 'draft',
4184
- visibility: 'unlisted',
4185
- slug: slugify(`${lessonData.title}~${lessonHash}`),
4186
- lessonType: 'lesson',
4187
- },
4188
- })
4189
-
4190
- // Link video if provided
4191
- if (lessonData.videoResourceId) {
4192
- await tx.insert(contentResourceResource).values({
4193
- resourceOfId: lessonId,
4194
- resourceId: lessonData.videoResourceId,
4195
- position: 0,
4196
- })
4197
- }
4198
-
4199
- // Link lesson to section
4200
- await tx.insert(contentResourceResource).values({
4201
- resourceOfId: sectionId,
4202
- resourceId: lessonId,
4203
- position: lessonPosition,
4204
- })
4205
-
4206
- const lesson = await tx.query.contentResource.findFirst({
4207
- where: eq(contentResource.id, lessonId),
4208
- })
4209
-
4210
- if (lesson) {
4211
- createdLessons.push(lesson as ContentResource)
4212
- }
4213
-
4214
- lessonPosition++
4215
- }
4216
-
4217
- position++
4218
- } else {
4219
- // Create top-level lesson
4220
- const lessonHash = guid()
4221
- const lessonId = `lesson_${lessonHash}`
4222
-
4223
- await tx.insert(contentResource).values({
4224
- id: lessonId,
4225
- type: 'lesson',
4226
- createdById: userId,
4227
- fields: {
4228
- title: item.title,
4229
- state: 'draft',
4230
- visibility: 'unlisted',
4231
- slug: slugify(`${item.title}~${lessonHash}`),
4232
- lessonType: 'lesson',
4233
- },
4234
- })
4235
-
4236
- // Link video if provided
4237
- if (item.videoResourceId) {
4238
- await tx.insert(contentResourceResource).values({
4239
- resourceOfId: lessonId,
4240
- resourceId: item.videoResourceId,
4241
- position: 0,
4242
- })
4243
- }
4244
-
4245
- // Link lesson to workshop
4246
- await tx.insert(contentResourceResource).values({
4247
- resourceOfId: workshopId,
4248
- resourceId: lessonId,
4249
- position,
4250
- })
4251
-
4252
- const lesson = await tx.query.contentResource.findFirst({
4253
- where: eq(contentResource.id, lessonId),
4254
- })
4255
-
4256
- if (lesson) {
4257
- createdLessons.push(lesson as ContentResource)
4258
- }
4259
-
4260
- position++
4261
- }
4262
- }
4263
-
4264
- return {
4265
- workshop: parsedWorkshop.data,
4266
- sections: createdSections,
4267
- lessons: createdLessons,
4268
- }
4269
- })
4270
-
4271
- // Handle product and coupon creation outside transaction
4272
- let product: any = null
4273
- if (
4274
- input.createProduct &&
4275
- input.pricing.price &&
4276
- input.pricing.price > 0
4277
- ) {
4278
- try {
4279
- product = await adapter.createProduct({
4280
- name: input.workshop.title,
4281
- price: input.pricing.price,
4282
- quantityAvailable: input.pricing.quantity ?? -1,
4283
- type: 'self-paced',
4284
- state: 'published',
4285
- visibility: 'public',
4286
- })
4287
-
4288
- if (product) {
4289
- // Link product to workshop resource
4290
- await client.insert(contentResourceProduct).values({
4291
- resourceId: workshopId,
4292
- productId: product.id,
4293
- position: 0,
4294
- metadata: {
4295
- addedBy: userId,
4296
- },
4297
- })
4298
-
4299
- // Create coupon if enabled
4300
- if (
4301
- input.coupon?.enabled &&
4302
- input.coupon.percentageDiscount &&
4303
- input.coupon.expires
4304
- ) {
4305
- try {
4306
- const finalExpires = normalizeExpirationDate(
4307
- input.coupon.expires,
4308
- )
4309
- await adapter.createCoupon({
4310
- percentageDiscount: input.coupon.percentageDiscount,
4311
- expires: finalExpires || null,
4312
- restrictedToProductId: product.id,
4313
- default: true,
4314
- maxUses: -1,
4315
- quantity: '-1',
4316
- status: 1,
4317
- fields: {},
4318
- })
4319
- logger.debug('workshop.create.coupon.success', {
4320
- workshopId,
4321
- productId: product.id,
4322
- userId,
4323
- percentageDiscount: input.coupon.percentageDiscount,
4324
- })
4325
- } catch (couponError) {
4326
- logger.error(
4327
- new Error(
4328
- `Failed to create coupon for workshop: ${couponError}`,
4329
- ),
4330
- )
4331
- logger.debug('workshop.create.coupon.failed', {
4332
- workshopId,
4333
- productId: product.id,
4334
- userId,
4335
- })
4336
- // Don't throw - workshop creation should succeed even if coupon fails
4337
- }
4338
- }
4339
- }
4340
- } catch (productError) {
4341
- logger.error(
4342
- new Error(`Failed to create product for workshop: ${productError}`),
4343
- )
4344
- logger.debug('workshop.create.product.failed', {
4345
- workshopId,
4346
- userId,
4347
- price: input.pricing.price,
4348
- })
4349
- // Don't throw - workshop creation should succeed even if product fails
4350
- }
4351
- }
4352
-
4353
- return {
4354
- ...result,
4355
- product,
4356
- }
4357
- },
4358
- async createCoupon(input: {
4359
- quantity: string
4360
- maxUses: number
4361
- expires: Date | null
4362
- restrictedToProductId: string | null
4363
- percentageDiscount: string
4364
- status: number
4365
- default: boolean
4366
- fields: Record<string, any>
4367
- }) {
4368
- function findClosestDiscount(percentOff: number) {
4369
- // we want a fraction so if it is whole number, we make it fractional
4370
- percentOff = percentOff <= 1 ? percentOff : percentOff / 100
4371
- return [1, 0.95, 0.9, 0.75, 0.6, 0.5, 0.4, 0.25, 0.1].reduce((a, b) => {
4372
- let aDiff = Math.abs(a - percentOff)
4373
- let bDiff = Math.abs(b - percentOff)
4374
-
4375
- if (aDiff === bDiff) {
4376
- // Choose largest vs smallest (> vs <)
4377
- return a > b ? a : b
4378
- } else {
4379
- return bDiff < aDiff ? b : a
4380
- }
4381
- })
4382
- }
4383
-
4384
- const percentageDiscount = findClosestDiscount(
4385
- Number(input.percentageDiscount) * 100,
4386
- )
4387
-
4388
- const merchantCouponResult =
4389
- percentageDiscount < 1
4390
- ? await client.query.merchantCoupon.findFirst({
4391
- where: and(
4392
- eq(
4393
- merchantCoupon.percentageDiscount,
4394
- percentageDiscount.toString(),
4395
- ),
4396
- eq(merchantCoupon.type, 'special'),
4397
- ),
4398
- })
4399
- : null
4400
-
4401
- if (!merchantCouponResult) {
4402
- return []
4403
- }
4404
-
4405
- const codesArray: string[] = []
4406
- const requestedQuantity = Number(input.quantity)
4407
- const couponQuantity = requestedQuantity < 0 ? 1 : requestedQuantity
4408
-
4409
- await client.transaction(async (trx) => {
4410
- // insert coupon for CouponInput quantity
4411
- for (let i = 0; i < couponQuantity; i++) {
4412
- const id = `coupon_${guid()}`
4413
- await trx.insert(coupon).values({
4414
- ...input,
4415
- merchantCouponId: merchantCouponResult.id as string,
4416
- id,
4417
- })
4418
- codesArray.push(id)
4419
- }
4420
- })
4421
-
4422
- return codesArray
4423
- },
4424
- async createUser(data) {
4425
- const id = crypto.randomUUID()
4426
- try {
4427
- await client.insert(users).values({ ...data, id })
4428
-
4429
- await provisionPersonalOrganization(adapter, {
4430
- userId: id,
4431
- email: data.email,
4432
- })
4433
-
4434
- const user = await adapter.getUser?.(id)
4435
-
4436
- if (!user) {
4437
- throw new Error('Failed to get user')
4438
- }
4439
-
4440
- return user
4441
- } catch (error) {
4442
- logger.error(toError(error), {
4443
- email: data.email,
4444
- event: 'adapter.user.create.failed',
4445
- userId: id,
4446
- })
4447
- throw error
4448
- }
4449
- },
4450
- async getUser(data) {
4451
- return (
4452
- (await client
4453
- .select()
4454
- .from(users)
4455
- .where(eq(users.id, data))
4456
- .then((res) => res[0])) ?? null
4457
- )
4458
- },
4459
- async getUserByEmail(data) {
4460
- return (
4461
- (await client
4462
- .select()
4463
- .from(users)
4464
- .where(eq(users.email, data))
4465
- .then((res) => res[0])) ?? null
4466
- )
4467
- },
4468
- async createSession(data) {
4469
- await client.insert(sessions).values(data)
4470
-
4471
- return await client
4472
- .select()
4473
- .from(sessions)
4474
- .where(eq(sessions.sessionToken, data.sessionToken))
4475
- .then((res) => res[0] as CourseBuilderAuthSession)
4476
- },
4477
- async getSessionAndUser(data) {
4478
- return (
4479
- (await client
4480
- .select({
4481
- session: sessions,
4482
- user: users,
4483
- })
4484
- .from(sessions)
4485
- .where(eq(sessions.sessionToken, data))
4486
- .innerJoin(users, eq(users.id, sessions.userId))
4487
- .then((res) => res[0])) ?? null
4488
- )
4489
- },
4490
- async updateUser(data) {
4491
- if (!data.id) {
4492
- throw new Error('No user id.')
4493
- }
4494
-
4495
- await client.update(users).set(data).where(eq(users.id, data.id))
4496
-
4497
- return await client
4498
- .select()
4499
- .from(users)
4500
- .where(eq(users.id, data.id))
4501
- .then((res) => res[0] as CourseBuilderUser)
4502
- },
4503
- async updateSession(data) {
4504
- await client
4505
- .update(sessions)
4506
- .set(data)
4507
- .where(eq(sessions.sessionToken, data.sessionToken))
4508
-
4509
- return await client
4510
- .select()
4511
- .from(sessions)
4512
- .where(eq(sessions.sessionToken, data.sessionToken))
4513
- .then((res) => res[0])
4514
- },
4515
- async linkAccount(rawAccount) {
4516
- await client.insert(accounts).values(rawAccount)
4517
- },
4518
- async getUserByAccount(account) {
4519
- const dbAccount =
4520
- (await client
4521
- .select()
4522
- .from(accounts)
4523
- .where(
4524
- and(
4525
- eq(accounts.providerAccountId, account.providerAccountId),
4526
- eq(accounts.provider, account.provider),
4527
- ),
4528
- )
4529
- .leftJoin(users, eq(accounts.userId, users.id))
4530
- .then((res) => res[0])) ?? null
4531
-
4532
- if (!dbAccount) {
4533
- return null
4534
- }
4535
-
4536
- return dbAccount.User
4537
- },
4538
- async deleteSession(sessionToken) {
4539
- const session =
4540
- (await client
4541
- .select()
4542
- .from(sessions)
4543
- .where(eq(sessions.sessionToken, sessionToken))
4544
- .then((res) => res[0])) ?? null
4545
-
4546
- await client
4547
- .delete(sessions)
4548
- .where(eq(sessions.sessionToken, sessionToken))
4549
-
4550
- return session
4551
- },
4552
- async createVerificationToken(token) {
4553
- await client.insert(verificationTokens).values(token)
4554
-
4555
- return await client
4556
- .select()
4557
- .from(verificationTokens)
4558
- .where(eq(verificationTokens.identifier, token.identifier))
4559
- .then((res) => res[0])
4560
- },
4561
- async useVerificationToken(token) {
4562
- try {
4563
- const deletedToken =
4564
- (await client
4565
- .select()
4566
- .from(verificationTokens)
4567
- .where(
4568
- and(
4569
- eq(verificationTokens.identifier, token.identifier),
4570
- eq(verificationTokens.token, token.token),
4571
- ),
4572
- )
4573
- .then((res) => res[0])) ?? null
4574
-
4575
- if (deletedToken?.createdAt) {
4576
- const TIMEOUT_IN_SECONDS = 90
4577
- const expireMultipleClicks = addSeconds(
4578
- deletedToken.createdAt,
4579
- TIMEOUT_IN_SECONDS,
4580
- )
4581
- const now = new Date()
4582
-
4583
- if (isAfter(expireMultipleClicks, now)) {
4584
- // @ts-ignore
4585
- const { id: _, ...verificationToken } = token
4586
- return deletedToken
4587
- } else {
4588
- await client
4589
- .delete(verificationTokens)
4590
- .where(
4591
- and(
4592
- eq(verificationTokens.identifier, token.identifier),
4593
- eq(verificationTokens.token, token.token),
4594
- ),
4595
- )
4596
- return deletedToken
4597
- }
4598
- }
4599
-
4600
- return deletedToken
4601
- } catch (err) {
4602
- throw new Error('No verification token found.')
4603
- }
4604
- },
4605
- async deleteUser(id) {
4606
- const user = await client
4607
- .select()
4608
- .from(users)
4609
- .where(eq(users.id, id))
4610
- .then((res) => res[0] ?? null)
4611
-
4612
- await client.delete(users).where(eq(users.id, id))
4613
- await client.delete(sessions).where(eq(sessions.userId, id))
4614
- await client.delete(accounts).where(eq(accounts.userId, id))
4615
-
4616
- return user
4617
- },
4618
- async unlinkAccount(account) {
4619
- await client
4620
- .delete(accounts)
4621
- .where(
4622
- and(
4623
- eq(accounts.providerAccountId, account.providerAccountId),
4624
- eq(accounts.provider, account.provider),
4625
- ),
4626
- )
4627
-
4628
- return undefined
4629
- },
4630
- createOrganization: async (options: { name: string }) => {
4631
- const organizationId = crypto.randomUUID()
4632
- await client.insert(organizationTable).values({
4633
- ...options,
4634
- id: organizationId,
4635
- })
4636
-
4637
- return adapter.getOrganization(organizationId)
4638
- },
4639
- getOrganization: async (organizationId: string) => {
4640
- return OrganizationSchema.parse(
4641
- await client.query.organization.findFirst({
4642
- where: eq(organizationTable.id, organizationId),
4643
- }),
4644
- )
4645
- },
4646
- addMemberToOrganization: async (options: {
4647
- organizationId: string
4648
- userId: string
4649
- invitedById: string
4650
- }) => {
4651
- const currentMembership =
4652
- await client.query.organizationMemberships.findFirst({
4653
- where: and(
4654
- eq(
4655
- organizationMembershipTable.organizationId,
4656
- options.organizationId,
4657
- ),
4658
- eq(organizationMembershipTable.userId, options.userId),
4659
- ),
4660
- with: {
4661
- organization: true,
4662
- user: true,
4663
- },
4664
- })
4665
-
4666
- if (currentMembership) {
4667
- return OrganizationMemberSchema.parse(currentMembership)
4668
- } else {
4669
- const id = crypto.randomUUID()
4670
- await client.insert(organizationMembershipTable).values({
4671
- ...options,
4672
- id,
4673
- })
4674
-
4675
- return OrganizationMemberSchema.parse(
4676
- await client.query.organizationMemberships.findFirst({
4677
- where: eq(organizationMembershipTable.id, id),
4678
- with: {
4679
- organization: true,
4680
- user: true,
4681
- },
4682
- }),
4683
- )
4684
- }
4685
- },
4686
- removeMemberFromOrganization: async (options: {
4687
- organizationId: string
4688
- userId: string
4689
- }) => {
4690
- await client
4691
- .delete(organizationMembershipTable)
4692
- .where(
4693
- and(
4694
- eq(
4695
- organizationMembershipTable.organizationId,
4696
- options.organizationId,
4697
- ),
4698
- eq(organizationMembershipTable.userId, options.userId),
4699
- ),
4700
- )
4701
- },
4702
- addRoleForMember: async (options: {
4703
- organizationId: string
4704
- memberId: string
4705
- role: string
4706
- }) => {
4707
- const existingRole = z
4708
- .object({
4709
- id: z.string(),
4710
- })
4711
- .nullish()
4712
- .parse(
4713
- await client.query.roles.findFirst({
4714
- where: and(
4715
- eq(roleTable.organizationId, options.organizationId),
4716
- eq(roleTable.name, options.role),
4717
- ),
4718
- }),
4719
- )
4720
-
4721
- const roleId = existingRole?.id || crypto.randomUUID()
4722
-
4723
- if (!existingRole) {
4724
- await client.insert(roleTable).values({
4725
- name: options.role,
4726
- organizationId: options.organizationId,
4727
- id: roleId,
4728
- })
4729
- }
4730
-
4731
- const currentOrgMembershipRole =
4732
- await client.query.organizationMembershipRoles.findFirst({
4733
- where: and(
4734
- eq(
4735
- organizationMembershipRoleTable.organizationMembershipId,
4736
- options.memberId,
4737
- ),
4738
- eq(organizationMembershipRoleTable.roleId, roleId),
4739
- ),
4740
- })
4741
-
4742
- if (!currentOrgMembershipRole) {
4743
- await client.insert(organizationMembershipRoleTable).values({
4744
- organizationId: options.organizationId,
4745
- organizationMembershipId: options.memberId,
4746
- roleId,
4747
- })
4748
- }
4749
- },
4750
- removeRoleForMember: async (options: {
4751
- organizationId: string
4752
- memberId: string
4753
- role: string
4754
- }) => {
4755
- const existingRole = z
4756
- .object({
4757
- id: z.string(),
4758
- })
4759
- .nullable()
4760
- .parse(
4761
- await client.query.roles.findFirst({
4762
- where: and(
4763
- eq(roleTable.organizationId, options.organizationId),
4764
- eq(roleTable.name, options.role),
4765
- ),
4766
- }),
4767
- )
4768
-
4769
- const roleId = existingRole?.id
4770
-
4771
- if (roleId) {
4772
- await client
4773
- .delete(organizationMembershipRoleTable)
4774
- .where(eq(organizationMembershipRoleTable.roleId, roleId))
4775
- }
4776
- },
4777
- getMembershipsForUser: async (userId: string) => {
4778
- return OrganizationMemberSchema.array().parse(
4779
- (await client.query.organizationMemberships.findMany({
4780
- where: eq(organizationMembershipTable.userId, userId),
4781
- with: {
4782
- organization: true,
4783
- user: true,
4784
- },
4785
- })) || [],
4786
- )
4787
- },
4788
- getOrganizationMembers: async (organizationId: string) => {
4789
- return OrganizationMemberSchema.array().parse(
4790
- (await client.query.organizationMemberships.findMany({
4791
- where: eq(organizationMembershipTable.organizationId, organizationId),
4792
- })) || [],
4793
- )
4794
- },
4795
- createSubscription: async (options: {
4796
- organizationId: string
4797
- merchantSubscriptionId: string
4798
- productId: string
4799
- }) => {
4800
- const id = `sub_${crypto.randomUUID()}`
4801
- await client.insert(subscriptionTable).values({
4802
- ...options,
4803
- id,
4804
- })
4805
-
4806
- return SubscriptionSchema.parse(
4807
- await client.query.subscription.findFirst({
4808
- where: eq(subscriptionTable.id, id),
4809
- with: {
4810
- product: true,
4811
- },
4812
- }),
4813
- )
4814
- },
4815
- getMerchantSubscription: async (merchantSubscriptionId: string) => {
4816
- return MerchantSubscriptionSchema.parse(
4817
- await client.query.merchantSubscription.findFirst({
4818
- where: eq(merchantSubscriptionTable.id, merchantSubscriptionId),
4819
- }),
4820
- )
4821
- },
4822
- createMerchantSubscription: async (options: {
4823
- merchantAccountId: string
4824
- merchantCustomerId: string
4825
- merchantProductId: string
4826
- identifier: string
4827
- }) => {
4828
- const id = crypto.randomUUID()
4829
- await client.insert(merchantSubscriptionTable).values({
4830
- ...options,
4831
- id,
4832
- })
4833
-
4834
- return MerchantSubscriptionSchema.parse(
4835
- await client.query.merchantSubscription.findFirst({
4836
- where: eq(merchantSubscriptionTable.id, id),
4837
- }),
4838
- )
4839
- },
4840
- updateMerchantSubscription: async (options: {
4841
- merchantSubscriptionId: string
4842
- status: string
4843
- }) => {
4844
- throw new Error('Not implemented')
4845
- },
4846
- deleteMerchantSubscription: async (merchantSubscriptionId: string) => {
4847
- throw new Error('Not implemented')
4848
- },
4849
- getSubscriptionForStripeId: async (stripeSubscriptionId: string) => {
4850
- const merchantSubscriptionParsed = MerchantSubscriptionSchema.safeParse(
4851
- await client.query.merchantSubscription.findFirst({
4852
- where: eq(merchantSubscriptionTable.identifier, stripeSubscriptionId),
4853
- }),
4854
- )
4855
-
4856
- if (!merchantSubscriptionParsed.success) {
4857
- throw new Error(
4858
- `No merchant subscription found for stripe id ${stripeSubscriptionId} ${merchantSubscriptionParsed.error}`,
4859
- )
4860
- }
4861
-
4862
- const subscriptionParsed = SubscriptionSchema.safeParse(
4863
- await client.query.subscription.findFirst({
4864
- where: eq(
4865
- subscriptionTable.merchantSubscriptionId,
4866
- merchantSubscriptionParsed.data.id,
4867
- ),
4868
- with: {
4869
- product: true,
4870
- },
4871
- }),
4872
- )
4873
-
4874
- if (!subscriptionParsed.success) {
4875
- throw new Error(
4876
- `No subscription found for merchant subscription ${merchantSubscriptionParsed.data.id} ${subscriptionParsed.error}`,
4877
- )
4878
- }
4879
-
4880
- return subscriptionParsed.data
4881
- },
4882
- updateSubscriptionStatus: async (
4883
- subscriptionId: string,
4884
- status: string,
4885
- ) => {
4886
- await client
4887
- .update(subscriptionTable)
4888
- .set({ status })
4889
- .where(eq(subscriptionTable.id, subscriptionId))
4890
- },
4891
- }
116
+ const adapter: DrizzleAdapterComposition<InstanceType<typeof MySqlDatabase>> =
117
+ {
118
+ client,
119
+ ...redeemFullPriceCouponWorkflow,
120
+ ...purchasePort,
121
+ ...merchantChargeAndPurchaseWorkflow,
122
+ ...transferPurchaseToUserWorkflow,
123
+ ...contentResourcePort,
124
+ ...createEventWorkflowOperations,
125
+ ...createEventSeriesWorkflowOperations,
126
+ ...createCohortWorkflowOperations,
127
+ ...createWorkshopWorkflowOperations,
128
+ ...identityPort,
129
+ ...authStoragePort,
130
+ ...organizationPort,
131
+ ...catalogPort,
132
+ ...couponPort,
133
+ ...entitlementPort,
134
+ ...merchantPort,
135
+ ...progressPort,
136
+ ...subscriptionPort,
137
+ }
4892
138
 
4893
139
  return adapter
4894
140
  }