@coursebuilder/adapter-drizzle 2.1.1 → 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 (296) 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-7FBSP3VD.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-F56AI52P.js → chunk-INZTF6QV.js} +2 -2
  19. package/dist/{chunk-AP5HBIV4.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-PJORQUQP.js → chunk-WUMDFFN3.js} +2 -2
  32. package/dist/{chunk-2AW5KYAK.js → chunk-X7DRYXB7.js} +2 -2
  33. package/dist/{chunk-PNPTKVJ4.js → chunk-XUBZKYH6.js} +24 -15
  34. package/dist/{chunk-WWBSNLRC.js → chunk-ZY4AFKUQ.js} +2 -2
  35. package/dist/index.d.ts +5 -11
  36. package/dist/index.js +41 -18
  37. package/dist/lib/mysql/cms-resource-actions.cjs +15912 -4127
  38. package/dist/lib/mysql/cms-resource-actions.cjs.map +1 -1
  39. package/dist/lib/mysql/cms-resource-actions.d.cts +8 -38
  40. package/dist/lib/mysql/cms-resource-actions.d.ts +8 -38
  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 +21875 -15261
  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 +46 -22
  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 +4 -4
  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 +5 -5
  129. package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
  130. package/dist/lib/mysql/schemas/auth/device-verification.js +5 -5
  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 +5 -5
  133. package/dist/lib/mysql/schemas/auth/profiles.cjs.map +1 -1
  134. package/dist/lib/mysql/schemas/auth/profiles.js +4 -4
  135. package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
  136. package/dist/lib/mysql/schemas/auth/role-permissions.js +5 -5
  137. package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
  138. package/dist/lib/mysql/schemas/auth/roles.js +4 -4
  139. package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
  140. package/dist/lib/mysql/schemas/auth/sessions.js +5 -5
  141. package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
  142. package/dist/lib/mysql/schemas/auth/user-permissions.js +4 -4
  143. package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
  144. package/dist/lib/mysql/schemas/auth/user-prefs.js +4 -4
  145. package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
  146. package/dist/lib/mysql/schemas/auth/user-roles.js +4 -4
  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 +4 -4
  150. package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
  151. package/dist/lib/mysql/schemas/commerce/coupon.js +4 -4
  152. package/dist/lib/mysql/schemas/commerce/merchant-charge.cjs.map +1 -1
  153. package/dist/lib/mysql/schemas/commerce/merchant-charge.js +4 -4
  154. package/dist/lib/mysql/schemas/commerce/merchant-subscription.cjs.map +1 -1
  155. package/dist/lib/mysql/schemas/commerce/merchant-subscription.js +4 -4
  156. package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
  157. package/dist/lib/mysql/schemas/commerce/price.js +4 -4
  158. package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
  159. package/dist/lib/mysql/schemas/commerce/product.js +4 -4
  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 +5 -5
  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 +4 -4
  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 +4 -4
  168. package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
  169. package/dist/lib/mysql/schemas/commerce/upgradable-products.js +5 -5
  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 +4 -4
  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 +4 -4
  176. package/dist/lib/mysql/schemas/communication/question-response.cjs.map +1 -1
  177. package/dist/lib/mysql/schemas/communication/question-response.js +5 -5
  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 +4 -4
  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 +4 -4
  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 +4 -4
  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 +4 -4
  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 +4 -4
  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 +4 -4
  192. package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -1
  193. package/dist/lib/mysql/schemas/content/tag-tag.js +4 -4
  194. package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -1
  195. package/dist/lib/mysql/schemas/content/tag.js +4 -4
  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 +5 -5
  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 +4 -4
  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 +4 -4
  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 +4 -4
  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/context.ts +20 -0
  267. package/src/lib/mysql/index.ts +118 -4873
  268. package/src/lib/mysql/ports/auth-storage.ts +139 -0
  269. package/src/lib/mysql/ports/catalog.ts +610 -0
  270. package/src/lib/mysql/ports/content-resource.ts +442 -0
  271. package/src/lib/mysql/ports/coupon.ts +387 -0
  272. package/src/lib/mysql/ports/entitlement.ts +96 -0
  273. package/src/lib/mysql/ports/identity.ts +160 -0
  274. package/src/lib/mysql/ports/merchant.ts +274 -0
  275. package/src/lib/mysql/ports/organization.ts +216 -0
  276. package/src/lib/mysql/ports/progress.ts +366 -0
  277. package/src/lib/mysql/ports/purchase.ts +517 -0
  278. package/src/lib/mysql/ports/subscription.ts +93 -0
  279. package/src/lib/mysql/schema.ts +374 -0
  280. package/src/lib/mysql/schemas/org/organization-memberships.ts +7 -0
  281. package/src/lib/mysql/schemas/org/organizations.ts +8 -0
  282. package/src/lib/mysql/workflows/content-resource-workflow-utils.ts +22 -0
  283. package/src/lib/mysql/workflows/create-cohort.ts +239 -0
  284. package/src/lib/mysql/workflows/create-event-series.ts +384 -0
  285. package/src/lib/mysql/workflows/create-event.ts +232 -0
  286. package/src/lib/mysql/workflows/create-merchant-charge-and-purchase.ts +365 -0
  287. package/src/lib/mysql/workflows/create-workshop.ts +372 -0
  288. package/src/lib/mysql/workflows/redeem-full-price-coupon.ts +179 -0
  289. package/src/lib/mysql/workflows/transfer-purchase-to-user.ts +142 -0
  290. package/src/types.ts +33 -0
  291. package/dist/authjs.d.ts +0 -27
  292. package/dist/authjs.js +0 -1
  293. package/dist/chunk-62VSIVME.js +0 -4714
  294. package/dist/chunk-RVXF3JZ7.js +0 -8732
  295. package/dist/chunk-WEJL5OFI.js +0 -8732
  296. package/src/authjs.ts +0 -34
@@ -1,4895 +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_UNQUOTE(JSON_EXTRACT (${contentResource.fields}, "$.resolution")) AS resolution,
3078
- JSON_EXTRACT (${contentResource.fields}, "$.muxPlaybackId") AS muxPlaybackId,
3079
- JSON_EXTRACT (${contentResource.fields}, "$.muxAssetId") AS muxAssetId,
3080
- JSON_EXTRACT (${contentResource.fields}, "$.transcript") AS transcript,
3081
- JSON_EXTRACT (${contentResource.fields}, "$.chapters") AS chapters
3082
- FROM
3083
- ${contentResource}
3084
- WHERE
3085
- type = 'videoResource'
3086
- AND (id = ${id});
3087
-
3088
- `
3089
- return client
3090
- .execute(query)
3091
- .then((result: any) => {
3092
- const parsedResource = VideoResourceSchema.safeParse(result.rows[0])
3093
- return parsedResource.success ? parsedResource.data : null
3094
- })
3095
- .catch((error) => {
3096
- logger.error(toError(error), {
3097
- event: 'adapter.videoResource.lookup.failed',
3098
- resourceId: id,
3099
- })
3100
- throw error
3101
- })
3102
- },
3103
- async getParentResourceOfVideoResource(
3104
- videoResourceId: string,
3105
- ): Promise<ContentResource | null> {
3106
- if (!videoResourceId) {
3107
- return null
3108
- }
3109
-
3110
- // Use a SQL query to find the parent post or lesson of this video resource
3111
- const results = await client
3112
- .select()
3113
- .from(contentResource)
3114
- .innerJoin(
3115
- contentResourceResource,
3116
- eq(contentResourceResource.resourceOfId, contentResource.id),
3117
- )
3118
- .where(
3119
- and(
3120
- eq(contentResourceResource.resourceId, videoResourceId),
3121
- or(
3122
- eq(contentResource.type, 'post'),
3123
- eq(contentResource.type, 'lesson'),
3124
- ),
3125
- ),
3126
- )
3127
- .limit(1)
3128
-
3129
- if (!results || results.length === 0) {
3130
- return null
3131
- }
3132
-
3133
- const resourceData = results[0]?.ContentResource
3134
-
3135
- if (!resourceData) {
3136
- return null
3137
- }
3138
-
3139
- // Fetch the full resource with relationships
3140
- const fullResource = await client.query.contentResource.findFirst({
3141
- where: eq(contentResource.id, resourceData.id),
3142
- with: {
3143
- resources: true,
3144
- },
3145
- })
3146
-
3147
- if (!fullResource) {
3148
- return null
3149
- }
3150
-
3151
- return ContentResourceSchema.parse(fullResource)
3152
- },
3153
- async createContentResource(data) {
3154
- const id = data.id || crypto.randomUUID()
3155
-
3156
- await client.insert(contentResource).values({ ...data, id })
3157
-
3158
- const resource = await client.query.contentResource.findFirst({
3159
- where: eq(contentResource.id, id),
3160
- with: {
3161
- resources: {
3162
- with: {
3163
- resource: {
3164
- with: {
3165
- resources: {
3166
- with: {
3167
- resource: true,
3168
- },
3169
- orderBy: asc(contentResourceResource.position),
3170
- },
3171
- },
3172
- },
3173
- },
3174
- orderBy: asc(contentResourceResource.position),
3175
- },
3176
- },
3177
- })
3178
-
3179
- const parsedResource = ContentResourceSchema.safeParse(resource)
3180
-
3181
- if (!parsedResource.success) {
3182
- logger.error(parsedResource.error, {
3183
- event: 'adapter.contentResource.parse_failed',
3184
- resourceId: id,
3185
- resourceType: data.type,
3186
- })
3187
- throw new Error('Error parsing resource')
3188
- }
3189
-
3190
- return parsedResource.data
3191
- },
3192
- async getContentResource(data) {
3193
- const resource = await client.query.contentResource.findFirst({
3194
- where: or(
3195
- eq(contentResource.id, data),
3196
- eq(
3197
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
3198
- `${data}`,
3199
- ),
3200
- ),
3201
- with: {
3202
- resources: {
3203
- with: {
3204
- resource: {
3205
- with: {
3206
- resources: {
3207
- with: {
3208
- resource: {
3209
- with: {
3210
- resources: {
3211
- with: {
3212
- resource: true,
3213
- },
3214
- },
3215
- },
3216
- },
3217
- },
3218
- orderBy: asc(contentResourceResource.position),
3219
- },
3220
- },
3221
- },
3222
- },
3223
- orderBy: asc(contentResourceResource.position),
3224
- },
3225
- },
3226
- })
3227
-
3228
- const parsedResource = ContentResourceSchema.safeParse(resource)
3229
-
3230
- if (!parsedResource.success) {
3231
- logger.error(parsedResource.error, {
3232
- event: 'adapter.contentResource.parse_failed',
3233
- resourceId: resource?.id,
3234
- resourceIdOrSlug: data,
3235
- })
3236
- return null
3237
- }
3238
-
3239
- return parsedResource.data
3240
- },
3241
- async getEvent(
3242
- eventIdOrSlug: string,
3243
- options?: {
3244
- withResources?: boolean
3245
- withTags?: boolean
3246
- withProducts?: boolean
3247
- withPricing?: boolean
3248
- },
3249
- ): Promise<ContentResource | null> {
3250
- const {
3251
- withResources = false,
3252
- withTags = false,
3253
- withProducts = false,
3254
- withPricing = false,
3255
- } = options || {}
3256
-
3257
- const eventQuery = await client.query.contentResource.findFirst({
3258
- where: and(
3259
- or(
3260
- eq(contentResource.type, 'event'),
3261
- eq(contentResource.type, 'event-series'),
3262
- ),
3263
- or(
3264
- eq(contentResource.id, eventIdOrSlug),
3265
- eq(
3266
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
3267
- eventIdOrSlug,
3268
- ),
3269
- ),
3270
- ),
3271
- with: {
3272
- ...(withResources && {
3273
- resources: {
3274
- with: { resource: true },
3275
- orderBy: asc(contentResourceResource.position),
3276
- },
3277
- }),
3278
- ...(withTags && {
3279
- tags: {
3280
- with: { tag: true },
3281
- orderBy: asc(contentResourceTag.position),
3282
- },
3283
- }),
3284
- ...(withProducts && {
3285
- resourceProducts: {
3286
- with: {
3287
- product: {
3288
- ...(withPricing && {
3289
- with: { price: true },
3290
- }),
3291
- },
3292
- },
3293
- },
3294
- }),
3295
- },
3296
- })
3297
-
3298
- const parsedEvent = ContentResourceSchema.safeParse(eventQuery)
3299
- if (!parsedEvent.success) {
3300
- logger.debug('Error parsing event', {
3301
- eventQuery,
3302
- error: parsedEvent.error,
3303
- })
3304
- return null
3305
- }
3306
-
3307
- return parsedEvent.data
3308
- },
3309
- async addResourceToProduct(options: {
3310
- resource: ContentResource
3311
- productId: string
3312
- userId: string
3313
- }) {
3314
- const { resource, productId, userId } = options
3315
- const product = await adapter.getProduct(productId)
3316
-
3317
- if (!product) {
3318
- throw new Error(`Product not found for id (${productId})`)
3319
- }
3320
-
3321
- await client.insert(contentResourceProduct).values({
3322
- resourceId: resource.id,
3323
- productId,
3324
- position: product.resources?.length || 0,
3325
- metadata: {
3326
- addedBy: userId,
3327
- },
3328
- })
3329
-
3330
- const resourceProduct =
3331
- await client.query.contentResourceProduct.findFirst({
3332
- where: eq(contentResourceProduct.resourceId, resource.id),
3333
- with: {
3334
- resource: true,
3335
- product: true,
3336
- },
3337
- })
3338
-
3339
- return (
3340
- ContentResourceProductSchema.safeParse(resourceProduct).data || null
3341
- )
3342
- },
3343
- async createEvent(
3344
- input: {
3345
- type: 'event'
3346
- fields: {
3347
- title: string
3348
- startsAt?: Date | null | undefined
3349
- endsAt?: Date | null | undefined
3350
- description?: string | null | undefined
3351
- price?: number | null | undefined
3352
- quantity?: number | null | undefined
3353
- state?: string | null | undefined
3354
- visibility?: string | null | undefined
3355
- slug?: string | null | undefined
3356
- tagIds?:
3357
- | { id: string; fields: Record<string, any> }[]
3358
- | null
3359
- | undefined
3360
- }
3361
- coupon?: {
3362
- enabled: boolean
3363
- percentageDiscount: string
3364
- expires: Date
3365
- }
3366
- },
3367
- userId: string,
3368
- ) {
3369
- const hash = guid()
3370
- const newEventId = slugify(`${input.type}~${hash}`)
3371
-
3372
- const newEvent = {
3373
- id: newEventId,
3374
- type: 'event',
3375
- fields: {
3376
- title: input.fields.title,
3377
- state: 'draft',
3378
- visibility: 'public',
3379
- slug: slugify(`${input.fields.title}~${hash}`),
3380
- description: input.fields.description,
3381
- startsAt: input.fields.startsAt,
3382
- endsAt: input.fields.endsAt,
3383
- },
3384
- createdById: userId,
3385
- }
3386
-
3387
- await client.insert(contentResource).values(newEvent)
3388
- const event = await adapter.getEvent(newEventId, {
3389
- withResources: true,
3390
- withTags: true,
3391
- withProducts: true,
3392
- withPricing: true,
3393
- })
3394
-
3395
- const parsedEvent = ContentResourceSchema.safeParse(event)
3396
-
3397
- if (!parsedEvent.success) {
3398
- logger.error(new Error('Error parsing event resource'))
3399
- throw new Error('Error parsing event resource')
3400
- }
3401
-
3402
- // if price is provided, create a product and associate it with the event
3403
- if (input.fields.price && input.fields.price > 0) {
3404
- try {
3405
- const product = await adapter.createProduct({
3406
- name: input.fields.title,
3407
- price: input.fields.price,
3408
- quantityAvailable: input.fields.quantity ?? -1,
3409
- type: 'live',
3410
- state: 'published',
3411
- visibility: 'public',
3412
- })
3413
- if (product) {
3414
- const resourceProduct = await adapter.addResourceToProduct({
3415
- resource: parsedEvent.data,
3416
- productId: product.id,
3417
- userId,
3418
- })
3419
- if (!resourceProduct) {
3420
- logger.error(new Error('Failed to add resource to product'))
3421
- logger.debug('event.addResourceToProduct.failed', {
3422
- eventId: newEventId,
3423
- productId: product.id,
3424
- userId,
3425
- })
3426
- } else {
3427
- logger.debug('event.addResourceToProduct.success', {
3428
- eventId: newEventId,
3429
- productId: product.id,
3430
- userId,
3431
- })
3432
- }
3433
- // Create coupon if enabled
3434
- if (
3435
- input.coupon?.enabled &&
3436
- input.coupon.percentageDiscount &&
3437
- input.coupon.expires
3438
- ) {
3439
- try {
3440
- const finalExpires = normalizeExpirationDate(
3441
- input.coupon.expires,
3442
- )
3443
- const couponInput = {
3444
- quantity: '1',
3445
- maxUses: -1,
3446
- expires: finalExpires || null,
3447
- restrictedToProductId: product.id,
3448
- percentageDiscount: input.coupon.percentageDiscount,
3449
- status: 1,
3450
- default: true,
3451
- fields: {
3452
- bypassSoldOut: false,
3453
- },
3454
- }
3455
- await adapter.createCoupon(couponInput)
3456
- logger.debug('event.create.coupon.success', {
3457
- eventId: newEventId,
3458
- productId: product.id,
3459
- userId,
3460
- percentageDiscount: input.coupon.percentageDiscount,
3461
- })
3462
- } catch (couponError) {
3463
- logger.error(
3464
- new Error(
3465
- `Failed to create coupon for event: ${couponError}`,
3466
- ),
3467
- )
3468
- logger.debug('event.create.coupon.failed', {
3469
- eventId: newEventId,
3470
- productId: product.id,
3471
- userId,
3472
- })
3473
- // Don't throw here - event creation should succeed even if coupon fails
3474
- }
3475
- }
3476
- } else {
3477
- logger.error(new Error('Failed to create product'))
3478
- logger.debug('event.create.product.failed', {
3479
- eventId: newEventId,
3480
- userId,
3481
- price: input.fields.price,
3482
- })
3483
- }
3484
- } catch (error) {
3485
- logger.error(
3486
- new Error(`Error creating and associating product: ${error}`),
3487
- )
3488
- logger.debug('event.create.product.failed', {
3489
- eventId: newEventId,
3490
- userId,
3491
- price: input.fields.price,
3492
- })
3493
- }
3494
- }
3495
-
3496
- // if we provide tagIds, we need to associate them with the event
3497
- if (input.fields.tagIds) {
3498
- try {
3499
- await client.insert(contentResourceTag).values(
3500
- input.fields.tagIds.map((tag) => ({
3501
- contentResourceId: newEventId,
3502
- tagId: tag.id,
3503
- createdAt: new Date(),
3504
- updatedAt: new Date(),
3505
- position: 0,
3506
- })),
3507
- )
3508
- logger.debug('event.create.tags.success', {
3509
- eventId: newEventId,
3510
- userId,
3511
- tagIds: input.fields.tagIds,
3512
- })
3513
- } catch (error) {
3514
- logger.error(new Error(`Error associating tags with event: ${error}`))
3515
- logger.debug('event.create.tags.failed', {
3516
- eventId: newEventId,
3517
- userId,
3518
- tagIds: input.fields.tagIds,
3519
- })
3520
- }
3521
- }
3522
-
3523
- return parsedEvent.data
3524
- },
3525
- async createEventSeries(
3526
- input: {
3527
- eventSeries: {
3528
- type: 'event-series'
3529
- fields: {
3530
- title: string
3531
- description?: string | undefined
3532
- tagIds?:
3533
- | { id: string; fields: { label: string; name: string } }[]
3534
- | null
3535
- | undefined
3536
- }
3537
- sharedFields: {
3538
- price: number | null | undefined
3539
- quantity: number | null | undefined
3540
- }
3541
- }
3542
- childEvents: Array<{
3543
- type: 'event'
3544
- fields: {
3545
- title: string
3546
- startsAt: Date | null | undefined
3547
- endsAt: Date | null | undefined
3548
- description?: string | undefined
3549
- tagIds?:
3550
- | { id: string; fields: { label: string; name: string } }[]
3551
- | null
3552
- | undefined
3553
- }
3554
- }>
3555
- coupon?:
3556
- | {
3557
- enabled: boolean
3558
- expires?: Date | undefined
3559
- percentageDiscount?: string | undefined
3560
- }
3561
- | undefined
3562
- },
3563
- userId: string,
3564
- ) {
3565
- const { eventSeries: eventSeriesInput, childEvents: childEventsInput } =
3566
- input
3567
-
3568
- if (childEventsInput.length === 0) {
3569
- throw new Error('At least one event is required')
3570
- }
3571
-
3572
- // Execute all database operations within a transaction
3573
- const result = await client.transaction(async (tx) => {
3574
- // Step 1: Create the event series resource
3575
- const eventSeriesHash = guid()
3576
- const eventSeriesResourceId = slugify(`event-series~${eventSeriesHash}`)
3577
-
3578
- const newEventSeries = {
3579
- id: eventSeriesResourceId,
3580
- type: 'event-series',
3581
- fields: {
3582
- ...eventSeriesInput.fields,
3583
- title: eventSeriesInput.fields.title,
3584
- state: 'draft',
3585
- visibility: 'public',
3586
- slug: slugify(
3587
- `${eventSeriesInput.fields.title}~${eventSeriesHash}`,
3588
- ),
3589
- },
3590
- createdById: userId,
3591
- }
3592
-
3593
- await tx.insert(contentResource).values(newEventSeries)
3594
-
3595
- // Fetch the created event series with relations
3596
- const eventSeriesResource = await tx.query.contentResource.findFirst({
3597
- where: eq(contentResource.id, eventSeriesResourceId),
3598
- with: {
3599
- resources: {
3600
- with: {
3601
- resource: {
3602
- with: {
3603
- resources: {
3604
- with: {
3605
- resource: true,
3606
- },
3607
- orderBy: asc(contentResourceResource.position),
3608
- },
3609
- },
3610
- },
3611
- },
3612
- orderBy: asc(contentResourceResource.position),
3613
- },
3614
- tags: {
3615
- with: {
3616
- tag: true,
3617
- },
3618
- orderBy: asc(contentResourceTag.position),
3619
- },
3620
- resourceProducts: {
3621
- with: {
3622
- product: {
3623
- with: {
3624
- price: true,
3625
- },
3626
- },
3627
- },
3628
- },
3629
- },
3630
- })
3631
-
3632
- const parsedEventSeries =
3633
- ContentResourceSchema.safeParse(eventSeriesResource)
3634
- if (!parsedEventSeries.success) {
3635
- throw new Error('Error parsing event series resource')
3636
- }
3637
-
3638
- // Step 2: Associate tags with event series
3639
- if (eventSeriesInput.fields.tagIds) {
3640
- await tx.insert(contentResourceTag).values(
3641
- eventSeriesInput.fields.tagIds.map((tag) => ({
3642
- contentResourceId: eventSeriesResourceId,
3643
- tagId: tag.id,
3644
- createdAt: new Date(),
3645
- updatedAt: new Date(),
3646
- position: 0,
3647
- })),
3648
- )
3649
- }
3650
-
3651
- // Step 3: Create child events and associate them
3652
- const createdChildEvents = []
3653
-
3654
- for (let i = 0; i < childEventsInput.length; i++) {
3655
- const childEventInput = childEventsInput[i]
3656
-
3657
- if (!childEventInput) {
3658
- throw new Error(`Child event input is required`)
3659
- }
3660
-
3661
- // Create child event
3662
- const childEventHash = guid()
3663
- const childEventResourceId = slugify(`event~${childEventHash}`)
3664
-
3665
- const newChildEvent = {
3666
- id: childEventResourceId,
3667
- type: 'event',
3668
- fields: {
3669
- title: childEventInput.fields.title,
3670
- startsAt: childEventInput.fields.startsAt,
3671
- endsAt: childEventInput.fields.endsAt,
3672
- description: childEventInput.fields.description,
3673
- state: 'draft',
3674
- visibility: 'public',
3675
- slug: slugify(
3676
- `${childEventInput.fields.title}~${childEventHash}`,
3677
- ),
3678
- // No price/quantity - they're on the event series
3679
- price: null,
3680
- quantity: null,
3681
- },
3682
- createdById: userId,
3683
- }
3684
-
3685
- await tx.insert(contentResource).values(newChildEvent)
3686
-
3687
- // Fetch the created child event with relations
3688
- const childEventResource = await tx.query.contentResource.findFirst({
3689
- where: eq(contentResource.id, childEventResourceId),
3690
- with: {
3691
- resources: {
3692
- with: {
3693
- resource: {
3694
- with: {
3695
- resources: {
3696
- with: {
3697
- resource: true,
3698
- },
3699
- orderBy: asc(contentResourceResource.position),
3700
- },
3701
- },
3702
- },
3703
- },
3704
- orderBy: asc(contentResourceResource.position),
3705
- },
3706
- tags: {
3707
- with: {
3708
- tag: true,
3709
- },
3710
- orderBy: asc(contentResourceTag.position),
3711
- },
3712
- resourceProducts: {
3713
- with: {
3714
- product: {
3715
- with: {
3716
- price: true,
3717
- },
3718
- },
3719
- },
3720
- },
3721
- },
3722
- })
3723
-
3724
- const parsedChildEvent =
3725
- ContentResourceSchema.safeParse(childEventResource)
3726
- if (!parsedChildEvent.success) {
3727
- throw new Error(`Error parsing child event resource ${i + 1}`)
3728
- }
3729
-
3730
- // Associate tags with child event
3731
- if (childEventInput.fields.tagIds) {
3732
- await tx.insert(contentResourceTag).values(
3733
- childEventInput.fields.tagIds.map((tag) => ({
3734
- contentResourceId: childEventResourceId,
3735
- tagId: tag.id,
3736
- createdAt: new Date(),
3737
- updatedAt: new Date(),
3738
- position: 0,
3739
- })),
3740
- )
3741
- }
3742
-
3743
- // Associate child event with event series
3744
- await tx.insert(contentResourceResource).values({
3745
- resourceOfId: eventSeriesResourceId,
3746
- resourceId: childEventResourceId,
3747
- position: i,
3748
- })
3749
-
3750
- createdChildEvents.push(parsedChildEvent.data)
3751
- }
3752
-
3753
- return {
3754
- eventSeries: parsedEventSeries.data,
3755
- childEvents: createdChildEvents,
3756
- }
3757
- })
3758
-
3759
- const { eventSeries, childEvents } = result
3760
-
3761
- // Step 4: Handle product creation outside transaction
3762
- if (
3763
- eventSeriesInput.sharedFields.price &&
3764
- eventSeriesInput.sharedFields.price > 0
3765
- ) {
3766
- try {
3767
- const product = await adapter.createProduct({
3768
- name: eventSeriesInput.fields.title,
3769
- price: eventSeriesInput.sharedFields.price,
3770
- quantityAvailable: eventSeriesInput.sharedFields.quantity ?? -1,
3771
- type: 'live',
3772
- state: 'published',
3773
- visibility: 'public',
3774
- })
3775
- if (product) {
3776
- await adapter.addResourceToProduct({
3777
- resource: eventSeries,
3778
- productId: product.id,
3779
- userId,
3780
- })
3781
-
3782
- // Create coupon if enabled
3783
- if (
3784
- input.coupon?.enabled &&
3785
- input.coupon.percentageDiscount &&
3786
- input.coupon.expires
3787
- ) {
3788
- try {
3789
- let finalExpires = input.coupon.expires
3790
- if (finalExpires instanceof Date) {
3791
- finalExpires = new Date(
3792
- Date.UTC(
3793
- finalExpires.getUTCFullYear(),
3794
- finalExpires.getUTCMonth(),
3795
- finalExpires.getUTCDate(),
3796
- 23,
3797
- 59,
3798
- 59,
3799
- 0,
3800
- ),
3801
- )
3802
- }
3803
- const couponInput = {
3804
- quantity: '1',
3805
- maxUses: 1,
3806
- expires: finalExpires,
3807
- restrictedToProductId: product.id,
3808
- percentageDiscount: input.coupon.percentageDiscount,
3809
- status: 1,
3810
- default: true,
3811
- fields: {},
3812
- }
3813
- await adapter.createCoupon(couponInput)
3814
- logger.debug('event.series.coupon.success', {
3815
- eventSeriesId: eventSeries.id,
3816
- productId: product.id,
3817
- userId,
3818
- percentageDiscount: input.coupon.percentageDiscount,
3819
- })
3820
- } catch (couponError) {
3821
- logger.error(
3822
- new Error(
3823
- `Error creating coupon for event series: ${couponError}`,
3824
- ),
3825
- )
3826
- logger.debug('event.series.coupon.failed', {
3827
- eventSeriesId: eventSeries.id,
3828
- productId: product.id,
3829
- userId,
3830
- })
3831
- // Don't throw here - event series creation should succeed even if coupon fails
3832
- }
3833
- }
3834
- } else {
3835
- logger.error(new Error('Failed to create product'))
3836
- logger.debug('event.series.product.failed', {
3837
- eventSeriesId: eventSeries.id,
3838
- userId,
3839
- price: eventSeriesInput.sharedFields.price,
3840
- })
3841
- }
3842
- } catch (error) {
3843
- logger.error(
3844
- new Error(`Error creating and associating product: ${error}`),
3845
- )
3846
- logger.debug('event.series.product.creation.failed', {
3847
- eventSeriesId: eventSeries.id,
3848
- userId,
3849
- })
3850
- // Note: We don't throw here since the core data is already committed
3851
- }
3852
- }
3853
-
3854
- return { eventSeries, childEvents }
3855
- },
3856
- async createCohort(
3857
- input: {
3858
- cohort: {
3859
- title: string
3860
- description?: string
3861
- tagIds?:
3862
- | { id: string; fields: { label: string; name: string } }[]
3863
- | null
3864
- }
3865
- dates: {
3866
- start: Date
3867
- end: Date
3868
- }
3869
- createProduct?: boolean
3870
- pricing: {
3871
- price?: number | null
3872
- }
3873
- coupon?: {
3874
- enabled: boolean
3875
- percentageDiscount?: string
3876
- expires?: Date
3877
- }
3878
- workshops: { id: string }[]
3879
- },
3880
- userId: string,
3881
- ) {
3882
- const hash = guid()
3883
- const cohortId = `cohort~${hash}`
3884
-
3885
- // Transaction only handles database operations
3886
- const cohort = await client.transaction(async (tx) => {
3887
- // Create cohort content resource
3888
- await tx.insert(contentResource).values({
3889
- id: cohortId,
3890
- type: 'cohort',
3891
- createdById: userId,
3892
- fields: {
3893
- title: input.cohort.title,
3894
- description: input.cohort.description,
3895
- state: 'draft',
3896
- visibility: 'unlisted',
3897
- slug: slugify(`${input.cohort.title}~${hash}`),
3898
- startsAt: input.dates.start,
3899
- endsAt: input.dates.end,
3900
- },
3901
- })
3902
-
3903
- // Fetch created cohort
3904
- const cohort = await tx.query.contentResource.findFirst({
3905
- where: eq(contentResource.id, cohortId),
3906
- with: {
3907
- resources: {
3908
- with: {
3909
- resource: true,
3910
- },
3911
- orderBy: asc(contentResourceResource.position),
3912
- },
3913
- },
3914
- })
3915
-
3916
- if (!cohort) {
3917
- throw new Error('Failed to create cohort')
3918
- }
3919
-
3920
- const parsedCohort = ContentResourceSchema.safeParse(cohort)
3921
- if (!parsedCohort.success) {
3922
- throw new Error('Invalid cohort data')
3923
- }
3924
-
3925
- // Associate tags with cohort
3926
- if (input.cohort.tagIds) {
3927
- await tx.insert(contentResourceTag).values(
3928
- input.cohort.tagIds.map((tag) => ({
3929
- contentResourceId: cohortId,
3930
- tagId: tag.id,
3931
- createdAt: new Date(),
3932
- updatedAt: new Date(),
3933
- position: 0,
3934
- })),
3935
- )
3936
- }
3937
-
3938
- // Link workshops to cohort
3939
- let position = 0
3940
- for (const workshop of input.workshops) {
3941
- await tx.insert(contentResourceResource).values({
3942
- resourceOfId: cohortId,
3943
- resourceId: workshop.id,
3944
- position,
3945
- })
3946
- position++
3947
- }
3948
-
3949
- return parsedCohort.data
3950
- })
3951
-
3952
- // Handle product and coupon creation outside transaction
3953
- let product: any = null
3954
- if (
3955
- input.createProduct &&
3956
- input.pricing.price &&
3957
- input.pricing.price > 0
3958
- ) {
3959
- try {
3960
- product = await adapter.createProduct({
3961
- name: input.cohort.title,
3962
- price: input.pricing.price,
3963
- quantityAvailable: -1,
3964
- type: 'cohort',
3965
- state: 'published',
3966
- visibility: 'public',
3967
- openEnrollment: new Date().toISOString(),
3968
- closeEnrollment: input.dates.start.toISOString(),
3969
- })
3970
-
3971
- if (product) {
3972
- // Link product to cohort resource
3973
- await client.insert(contentResourceProduct).values({
3974
- resourceId: cohortId,
3975
- productId: product.id,
3976
- position: 0,
3977
- metadata: {
3978
- addedBy: userId,
3979
- },
3980
- })
3981
-
3982
- // Create coupon if enabled
3983
- if (
3984
- input.coupon?.enabled &&
3985
- input.coupon.percentageDiscount &&
3986
- input.coupon.expires
3987
- ) {
3988
- try {
3989
- const finalExpires = normalizeExpirationDate(
3990
- input.coupon.expires,
3991
- )
3992
- await adapter.createCoupon({
3993
- percentageDiscount: input.coupon.percentageDiscount,
3994
- expires: finalExpires || null,
3995
- restrictedToProductId: product.id,
3996
- default: true,
3997
- maxUses: -1,
3998
- quantity: '-1',
3999
- status: 1,
4000
- fields: {},
4001
- })
4002
- logger.debug('cohort.create.coupon.success', {
4003
- cohortId,
4004
- productId: product.id,
4005
- userId,
4006
- percentageDiscount: input.coupon.percentageDiscount,
4007
- })
4008
- } catch (couponError) {
4009
- logger.error(
4010
- new Error(
4011
- `Failed to create coupon for cohort: ${couponError}`,
4012
- ),
4013
- )
4014
- logger.debug('cohort.create.coupon.failed', {
4015
- cohortId,
4016
- productId: product.id,
4017
- userId,
4018
- })
4019
- // Don't throw - cohort creation should succeed even if coupon fails
4020
- }
4021
- }
4022
- }
4023
- } catch (productError) {
4024
- logger.error(
4025
- new Error(`Failed to create product for cohort: ${productError}`),
4026
- )
4027
- logger.debug('cohort.create.product.failed', {
4028
- cohortId,
4029
- userId,
4030
- price: input.pricing.price,
4031
- })
4032
- // Don't throw - cohort creation should succeed even if product fails
4033
- }
4034
- }
4035
-
4036
- return {
4037
- cohort,
4038
- product,
4039
- }
4040
- },
4041
- async createWorkshop(
4042
- input: {
4043
- workshop: {
4044
- title: string
4045
- description?: string
4046
- tagIds?:
4047
- | { id: string; fields: { label: string; name: string } }[]
4048
- | null
4049
- }
4050
- createProduct?: boolean
4051
- pricing: {
4052
- price?: number | null
4053
- quantity?: number | null
4054
- }
4055
- coupon?: {
4056
- enabled: boolean
4057
- percentageDiscount?: string
4058
- expires?: Date
4059
- }
4060
- structure: Array<
4061
- | {
4062
- type: 'section'
4063
- title: string
4064
- lessons: { title: string; videoResourceId?: string }[]
4065
- }
4066
- | { type: 'lesson'; title: string; videoResourceId?: string }
4067
- >
4068
- },
4069
- userId: string,
4070
- ) {
4071
- const hash = guid()
4072
- const workshopId = `workshop~${hash}`
4073
-
4074
- // Transaction only handles database operations
4075
- const result = await client.transaction(async (tx) => {
4076
- // Create workshop content resource
4077
- await tx.insert(contentResource).values({
4078
- id: workshopId,
4079
- type: 'workshop',
4080
- createdById: userId,
4081
- fields: {
4082
- title: input.workshop.title,
4083
- description: input.workshop.description,
4084
- state: 'draft',
4085
- visibility: 'unlisted',
4086
- slug: slugify(`${input.workshop.title}~${hash}`),
4087
- },
4088
- })
4089
-
4090
- // Fetch created workshop
4091
- const workshop = await tx.query.contentResource.findFirst({
4092
- where: eq(contentResource.id, workshopId),
4093
- with: {
4094
- resources: {
4095
- with: {
4096
- resource: {
4097
- with: {
4098
- resources: {
4099
- with: {
4100
- resource: true,
4101
- },
4102
- orderBy: asc(contentResourceResource.position),
4103
- },
4104
- },
4105
- },
4106
- },
4107
- orderBy: asc(contentResourceResource.position),
4108
- },
4109
- },
4110
- })
4111
-
4112
- if (!workshop) {
4113
- throw new Error('Failed to create workshop')
4114
- }
4115
-
4116
- const parsedWorkshop = ContentResourceSchema.safeParse(workshop)
4117
- if (!parsedWorkshop.success) {
4118
- throw new Error('Invalid workshop data')
4119
- }
4120
-
4121
- // Associate tags with workshop
4122
- if (input.workshop.tagIds) {
4123
- await tx.insert(contentResourceTag).values(
4124
- input.workshop.tagIds.map((tag) => ({
4125
- contentResourceId: workshopId,
4126
- tagId: tag.id,
4127
- createdAt: new Date(),
4128
- updatedAt: new Date(),
4129
- position: 0,
4130
- })),
4131
- )
4132
- }
4133
-
4134
- // Create sections and lessons
4135
- const createdSections: ContentResource[] = []
4136
- const createdLessons: ContentResource[] = []
4137
- let position = 0
4138
-
4139
- for (const item of input.structure) {
4140
- if (item.type === 'section') {
4141
- // Create section
4142
- const sectionHash = guid()
4143
- const sectionId = `section~${sectionHash}`
4144
-
4145
- await tx.insert(contentResource).values({
4146
- id: sectionId,
4147
- type: 'section',
4148
- createdById: userId,
4149
- fields: {
4150
- title: item.title,
4151
- state: 'draft',
4152
- visibility: 'unlisted',
4153
- slug: slugify(`${item.title}~${sectionHash}`),
4154
- },
4155
- })
4156
-
4157
- // Link section to workshop
4158
- await tx.insert(contentResourceResource).values({
4159
- resourceOfId: workshopId,
4160
- resourceId: sectionId,
4161
- position,
4162
- })
4163
-
4164
- const section = await tx.query.contentResource.findFirst({
4165
- where: eq(contentResource.id, sectionId),
4166
- })
4167
-
4168
- if (section) {
4169
- createdSections.push(section as ContentResource)
4170
- }
4171
-
4172
- // Create lessons in section
4173
- let lessonPosition = 0
4174
- for (const lessonData of item.lessons) {
4175
- const lessonHash = guid()
4176
- const lessonId = `lesson_${lessonHash}`
4177
-
4178
- await tx.insert(contentResource).values({
4179
- id: lessonId,
4180
- type: 'lesson',
4181
- createdById: userId,
4182
- fields: {
4183
- title: lessonData.title,
4184
- state: 'draft',
4185
- visibility: 'unlisted',
4186
- slug: slugify(`${lessonData.title}~${lessonHash}`),
4187
- lessonType: 'lesson',
4188
- },
4189
- })
4190
-
4191
- // Link video if provided
4192
- if (lessonData.videoResourceId) {
4193
- await tx.insert(contentResourceResource).values({
4194
- resourceOfId: lessonId,
4195
- resourceId: lessonData.videoResourceId,
4196
- position: 0,
4197
- })
4198
- }
4199
-
4200
- // Link lesson to section
4201
- await tx.insert(contentResourceResource).values({
4202
- resourceOfId: sectionId,
4203
- resourceId: lessonId,
4204
- position: lessonPosition,
4205
- })
4206
-
4207
- const lesson = await tx.query.contentResource.findFirst({
4208
- where: eq(contentResource.id, lessonId),
4209
- })
4210
-
4211
- if (lesson) {
4212
- createdLessons.push(lesson as ContentResource)
4213
- }
4214
-
4215
- lessonPosition++
4216
- }
4217
-
4218
- position++
4219
- } else {
4220
- // Create top-level lesson
4221
- const lessonHash = guid()
4222
- const lessonId = `lesson_${lessonHash}`
4223
-
4224
- await tx.insert(contentResource).values({
4225
- id: lessonId,
4226
- type: 'lesson',
4227
- createdById: userId,
4228
- fields: {
4229
- title: item.title,
4230
- state: 'draft',
4231
- visibility: 'unlisted',
4232
- slug: slugify(`${item.title}~${lessonHash}`),
4233
- lessonType: 'lesson',
4234
- },
4235
- })
4236
-
4237
- // Link video if provided
4238
- if (item.videoResourceId) {
4239
- await tx.insert(contentResourceResource).values({
4240
- resourceOfId: lessonId,
4241
- resourceId: item.videoResourceId,
4242
- position: 0,
4243
- })
4244
- }
4245
-
4246
- // Link lesson to workshop
4247
- await tx.insert(contentResourceResource).values({
4248
- resourceOfId: workshopId,
4249
- resourceId: lessonId,
4250
- position,
4251
- })
4252
-
4253
- const lesson = await tx.query.contentResource.findFirst({
4254
- where: eq(contentResource.id, lessonId),
4255
- })
4256
-
4257
- if (lesson) {
4258
- createdLessons.push(lesson as ContentResource)
4259
- }
4260
-
4261
- position++
4262
- }
4263
- }
4264
-
4265
- return {
4266
- workshop: parsedWorkshop.data,
4267
- sections: createdSections,
4268
- lessons: createdLessons,
4269
- }
4270
- })
4271
-
4272
- // Handle product and coupon creation outside transaction
4273
- let product: any = null
4274
- if (
4275
- input.createProduct &&
4276
- input.pricing.price &&
4277
- input.pricing.price > 0
4278
- ) {
4279
- try {
4280
- product = await adapter.createProduct({
4281
- name: input.workshop.title,
4282
- price: input.pricing.price,
4283
- quantityAvailable: input.pricing.quantity ?? -1,
4284
- type: 'self-paced',
4285
- state: 'published',
4286
- visibility: 'public',
4287
- })
4288
-
4289
- if (product) {
4290
- // Link product to workshop resource
4291
- await client.insert(contentResourceProduct).values({
4292
- resourceId: workshopId,
4293
- productId: product.id,
4294
- position: 0,
4295
- metadata: {
4296
- addedBy: userId,
4297
- },
4298
- })
4299
-
4300
- // Create coupon if enabled
4301
- if (
4302
- input.coupon?.enabled &&
4303
- input.coupon.percentageDiscount &&
4304
- input.coupon.expires
4305
- ) {
4306
- try {
4307
- const finalExpires = normalizeExpirationDate(
4308
- input.coupon.expires,
4309
- )
4310
- await adapter.createCoupon({
4311
- percentageDiscount: input.coupon.percentageDiscount,
4312
- expires: finalExpires || null,
4313
- restrictedToProductId: product.id,
4314
- default: true,
4315
- maxUses: -1,
4316
- quantity: '-1',
4317
- status: 1,
4318
- fields: {},
4319
- })
4320
- logger.debug('workshop.create.coupon.success', {
4321
- workshopId,
4322
- productId: product.id,
4323
- userId,
4324
- percentageDiscount: input.coupon.percentageDiscount,
4325
- })
4326
- } catch (couponError) {
4327
- logger.error(
4328
- new Error(
4329
- `Failed to create coupon for workshop: ${couponError}`,
4330
- ),
4331
- )
4332
- logger.debug('workshop.create.coupon.failed', {
4333
- workshopId,
4334
- productId: product.id,
4335
- userId,
4336
- })
4337
- // Don't throw - workshop creation should succeed even if coupon fails
4338
- }
4339
- }
4340
- }
4341
- } catch (productError) {
4342
- logger.error(
4343
- new Error(`Failed to create product for workshop: ${productError}`),
4344
- )
4345
- logger.debug('workshop.create.product.failed', {
4346
- workshopId,
4347
- userId,
4348
- price: input.pricing.price,
4349
- })
4350
- // Don't throw - workshop creation should succeed even if product fails
4351
- }
4352
- }
4353
-
4354
- return {
4355
- ...result,
4356
- product,
4357
- }
4358
- },
4359
- async createCoupon(input: {
4360
- quantity: string
4361
- maxUses: number
4362
- expires: Date | null
4363
- restrictedToProductId: string | null
4364
- percentageDiscount: string
4365
- status: number
4366
- default: boolean
4367
- fields: Record<string, any>
4368
- }) {
4369
- function findClosestDiscount(percentOff: number) {
4370
- // we want a fraction so if it is whole number, we make it fractional
4371
- percentOff = percentOff <= 1 ? percentOff : percentOff / 100
4372
- return [1, 0.95, 0.9, 0.75, 0.6, 0.5, 0.4, 0.25, 0.1].reduce((a, b) => {
4373
- let aDiff = Math.abs(a - percentOff)
4374
- let bDiff = Math.abs(b - percentOff)
4375
-
4376
- if (aDiff === bDiff) {
4377
- // Choose largest vs smallest (> vs <)
4378
- return a > b ? a : b
4379
- } else {
4380
- return bDiff < aDiff ? b : a
4381
- }
4382
- })
4383
- }
4384
-
4385
- const percentageDiscount = findClosestDiscount(
4386
- Number(input.percentageDiscount) * 100,
4387
- )
4388
-
4389
- const merchantCouponResult =
4390
- percentageDiscount < 1
4391
- ? await client.query.merchantCoupon.findFirst({
4392
- where: and(
4393
- eq(
4394
- merchantCoupon.percentageDiscount,
4395
- percentageDiscount.toString(),
4396
- ),
4397
- eq(merchantCoupon.type, 'special'),
4398
- ),
4399
- })
4400
- : null
4401
-
4402
- if (!merchantCouponResult) {
4403
- return []
4404
- }
4405
-
4406
- const codesArray: string[] = []
4407
- const requestedQuantity = Number(input.quantity)
4408
- const couponQuantity = requestedQuantity < 0 ? 1 : requestedQuantity
4409
-
4410
- await client.transaction(async (trx) => {
4411
- // insert coupon for CouponInput quantity
4412
- for (let i = 0; i < couponQuantity; i++) {
4413
- const id = `coupon_${guid()}`
4414
- await trx.insert(coupon).values({
4415
- ...input,
4416
- merchantCouponId: merchantCouponResult.id as string,
4417
- id,
4418
- })
4419
- codesArray.push(id)
4420
- }
4421
- })
4422
-
4423
- return codesArray
4424
- },
4425
- async createUser(data) {
4426
- const id = crypto.randomUUID()
4427
- try {
4428
- await client.insert(users).values({ ...data, id })
4429
-
4430
- await provisionPersonalOrganization(adapter, {
4431
- userId: id,
4432
- email: data.email,
4433
- })
4434
-
4435
- const user = await adapter.getUser?.(id)
4436
-
4437
- if (!user) {
4438
- throw new Error('Failed to get user')
4439
- }
4440
-
4441
- return user
4442
- } catch (error) {
4443
- logger.error(toError(error), {
4444
- email: data.email,
4445
- event: 'adapter.user.create.failed',
4446
- userId: id,
4447
- })
4448
- throw error
4449
- }
4450
- },
4451
- async getUser(data) {
4452
- return (
4453
- (await client
4454
- .select()
4455
- .from(users)
4456
- .where(eq(users.id, data))
4457
- .then((res) => res[0])) ?? null
4458
- )
4459
- },
4460
- async getUserByEmail(data) {
4461
- return (
4462
- (await client
4463
- .select()
4464
- .from(users)
4465
- .where(eq(users.email, data))
4466
- .then((res) => res[0])) ?? null
4467
- )
4468
- },
4469
- async createSession(data) {
4470
- await client.insert(sessions).values(data)
4471
-
4472
- return await client
4473
- .select()
4474
- .from(sessions)
4475
- .where(eq(sessions.sessionToken, data.sessionToken))
4476
- .then((res) => res[0] as CourseBuilderAuthSession)
4477
- },
4478
- async getSessionAndUser(data) {
4479
- return (
4480
- (await client
4481
- .select({
4482
- session: sessions,
4483
- user: users,
4484
- })
4485
- .from(sessions)
4486
- .where(eq(sessions.sessionToken, data))
4487
- .innerJoin(users, eq(users.id, sessions.userId))
4488
- .then((res) => res[0])) ?? null
4489
- )
4490
- },
4491
- async updateUser(data) {
4492
- if (!data.id) {
4493
- throw new Error('No user id.')
4494
- }
4495
-
4496
- await client.update(users).set(data).where(eq(users.id, data.id))
4497
-
4498
- return await client
4499
- .select()
4500
- .from(users)
4501
- .where(eq(users.id, data.id))
4502
- .then((res) => res[0] as CourseBuilderUser)
4503
- },
4504
- async updateSession(data) {
4505
- await client
4506
- .update(sessions)
4507
- .set(data)
4508
- .where(eq(sessions.sessionToken, data.sessionToken))
4509
-
4510
- return await client
4511
- .select()
4512
- .from(sessions)
4513
- .where(eq(sessions.sessionToken, data.sessionToken))
4514
- .then((res) => res[0])
4515
- },
4516
- async linkAccount(rawAccount) {
4517
- await client.insert(accounts).values(rawAccount)
4518
- },
4519
- async getUserByAccount(account) {
4520
- const dbAccount =
4521
- (await client
4522
- .select()
4523
- .from(accounts)
4524
- .where(
4525
- and(
4526
- eq(accounts.providerAccountId, account.providerAccountId),
4527
- eq(accounts.provider, account.provider),
4528
- ),
4529
- )
4530
- .leftJoin(users, eq(accounts.userId, users.id))
4531
- .then((res) => res[0])) ?? null
4532
-
4533
- if (!dbAccount) {
4534
- return null
4535
- }
4536
-
4537
- return dbAccount.User
4538
- },
4539
- async deleteSession(sessionToken) {
4540
- const session =
4541
- (await client
4542
- .select()
4543
- .from(sessions)
4544
- .where(eq(sessions.sessionToken, sessionToken))
4545
- .then((res) => res[0])) ?? null
4546
-
4547
- await client
4548
- .delete(sessions)
4549
- .where(eq(sessions.sessionToken, sessionToken))
4550
-
4551
- return session
4552
- },
4553
- async createVerificationToken(token) {
4554
- await client.insert(verificationTokens).values(token)
4555
-
4556
- return await client
4557
- .select()
4558
- .from(verificationTokens)
4559
- .where(eq(verificationTokens.identifier, token.identifier))
4560
- .then((res) => res[0])
4561
- },
4562
- async useVerificationToken(token) {
4563
- try {
4564
- const deletedToken =
4565
- (await client
4566
- .select()
4567
- .from(verificationTokens)
4568
- .where(
4569
- and(
4570
- eq(verificationTokens.identifier, token.identifier),
4571
- eq(verificationTokens.token, token.token),
4572
- ),
4573
- )
4574
- .then((res) => res[0])) ?? null
4575
-
4576
- if (deletedToken?.createdAt) {
4577
- const TIMEOUT_IN_SECONDS = 90
4578
- const expireMultipleClicks = addSeconds(
4579
- deletedToken.createdAt,
4580
- TIMEOUT_IN_SECONDS,
4581
- )
4582
- const now = new Date()
4583
-
4584
- if (isAfter(expireMultipleClicks, now)) {
4585
- // @ts-ignore
4586
- const { id: _, ...verificationToken } = token
4587
- return deletedToken
4588
- } else {
4589
- await client
4590
- .delete(verificationTokens)
4591
- .where(
4592
- and(
4593
- eq(verificationTokens.identifier, token.identifier),
4594
- eq(verificationTokens.token, token.token),
4595
- ),
4596
- )
4597
- return deletedToken
4598
- }
4599
- }
4600
-
4601
- return deletedToken
4602
- } catch (err) {
4603
- throw new Error('No verification token found.')
4604
- }
4605
- },
4606
- async deleteUser(id) {
4607
- const user = await client
4608
- .select()
4609
- .from(users)
4610
- .where(eq(users.id, id))
4611
- .then((res) => res[0] ?? null)
4612
-
4613
- await client.delete(users).where(eq(users.id, id))
4614
- await client.delete(sessions).where(eq(sessions.userId, id))
4615
- await client.delete(accounts).where(eq(accounts.userId, id))
4616
-
4617
- return user
4618
- },
4619
- async unlinkAccount(account) {
4620
- await client
4621
- .delete(accounts)
4622
- .where(
4623
- and(
4624
- eq(accounts.providerAccountId, account.providerAccountId),
4625
- eq(accounts.provider, account.provider),
4626
- ),
4627
- )
4628
-
4629
- return undefined
4630
- },
4631
- createOrganization: async (options: { name: string }) => {
4632
- const organizationId = crypto.randomUUID()
4633
- await client.insert(organizationTable).values({
4634
- ...options,
4635
- id: organizationId,
4636
- })
4637
-
4638
- return adapter.getOrganization(organizationId)
4639
- },
4640
- getOrganization: async (organizationId: string) => {
4641
- return OrganizationSchema.parse(
4642
- await client.query.organization.findFirst({
4643
- where: eq(organizationTable.id, organizationId),
4644
- }),
4645
- )
4646
- },
4647
- addMemberToOrganization: async (options: {
4648
- organizationId: string
4649
- userId: string
4650
- invitedById: string
4651
- }) => {
4652
- const currentMembership =
4653
- await client.query.organizationMemberships.findFirst({
4654
- where: and(
4655
- eq(
4656
- organizationMembershipTable.organizationId,
4657
- options.organizationId,
4658
- ),
4659
- eq(organizationMembershipTable.userId, options.userId),
4660
- ),
4661
- with: {
4662
- organization: true,
4663
- user: true,
4664
- },
4665
- })
4666
-
4667
- if (currentMembership) {
4668
- return OrganizationMemberSchema.parse(currentMembership)
4669
- } else {
4670
- const id = crypto.randomUUID()
4671
- await client.insert(organizationMembershipTable).values({
4672
- ...options,
4673
- id,
4674
- })
4675
-
4676
- return OrganizationMemberSchema.parse(
4677
- await client.query.organizationMemberships.findFirst({
4678
- where: eq(organizationMembershipTable.id, id),
4679
- with: {
4680
- organization: true,
4681
- user: true,
4682
- },
4683
- }),
4684
- )
4685
- }
4686
- },
4687
- removeMemberFromOrganization: async (options: {
4688
- organizationId: string
4689
- userId: string
4690
- }) => {
4691
- await client
4692
- .delete(organizationMembershipTable)
4693
- .where(
4694
- and(
4695
- eq(
4696
- organizationMembershipTable.organizationId,
4697
- options.organizationId,
4698
- ),
4699
- eq(organizationMembershipTable.userId, options.userId),
4700
- ),
4701
- )
4702
- },
4703
- addRoleForMember: async (options: {
4704
- organizationId: string
4705
- memberId: string
4706
- role: string
4707
- }) => {
4708
- const existingRole = z
4709
- .object({
4710
- id: z.string(),
4711
- })
4712
- .nullish()
4713
- .parse(
4714
- await client.query.roles.findFirst({
4715
- where: and(
4716
- eq(roleTable.organizationId, options.organizationId),
4717
- eq(roleTable.name, options.role),
4718
- ),
4719
- }),
4720
- )
4721
-
4722
- const roleId = existingRole?.id || crypto.randomUUID()
4723
-
4724
- if (!existingRole) {
4725
- await client.insert(roleTable).values({
4726
- name: options.role,
4727
- organizationId: options.organizationId,
4728
- id: roleId,
4729
- })
4730
- }
4731
-
4732
- const currentOrgMembershipRole =
4733
- await client.query.organizationMembershipRoles.findFirst({
4734
- where: and(
4735
- eq(
4736
- organizationMembershipRoleTable.organizationMembershipId,
4737
- options.memberId,
4738
- ),
4739
- eq(organizationMembershipRoleTable.roleId, roleId),
4740
- ),
4741
- })
4742
-
4743
- if (!currentOrgMembershipRole) {
4744
- await client.insert(organizationMembershipRoleTable).values({
4745
- organizationId: options.organizationId,
4746
- organizationMembershipId: options.memberId,
4747
- roleId,
4748
- })
4749
- }
4750
- },
4751
- removeRoleForMember: async (options: {
4752
- organizationId: string
4753
- memberId: string
4754
- role: string
4755
- }) => {
4756
- const existingRole = z
4757
- .object({
4758
- id: z.string(),
4759
- })
4760
- .nullable()
4761
- .parse(
4762
- await client.query.roles.findFirst({
4763
- where: and(
4764
- eq(roleTable.organizationId, options.organizationId),
4765
- eq(roleTable.name, options.role),
4766
- ),
4767
- }),
4768
- )
4769
-
4770
- const roleId = existingRole?.id
4771
-
4772
- if (roleId) {
4773
- await client
4774
- .delete(organizationMembershipRoleTable)
4775
- .where(eq(organizationMembershipRoleTable.roleId, roleId))
4776
- }
4777
- },
4778
- getMembershipsForUser: async (userId: string) => {
4779
- return OrganizationMemberSchema.array().parse(
4780
- (await client.query.organizationMemberships.findMany({
4781
- where: eq(organizationMembershipTable.userId, userId),
4782
- with: {
4783
- organization: true,
4784
- user: true,
4785
- },
4786
- })) || [],
4787
- )
4788
- },
4789
- getOrganizationMembers: async (organizationId: string) => {
4790
- return OrganizationMemberSchema.array().parse(
4791
- (await client.query.organizationMemberships.findMany({
4792
- where: eq(organizationMembershipTable.organizationId, organizationId),
4793
- })) || [],
4794
- )
4795
- },
4796
- createSubscription: async (options: {
4797
- organizationId: string
4798
- merchantSubscriptionId: string
4799
- productId: string
4800
- }) => {
4801
- const id = `sub_${crypto.randomUUID()}`
4802
- await client.insert(subscriptionTable).values({
4803
- ...options,
4804
- id,
4805
- })
4806
-
4807
- return SubscriptionSchema.parse(
4808
- await client.query.subscription.findFirst({
4809
- where: eq(subscriptionTable.id, id),
4810
- with: {
4811
- product: true,
4812
- },
4813
- }),
4814
- )
4815
- },
4816
- getMerchantSubscription: async (merchantSubscriptionId: string) => {
4817
- return MerchantSubscriptionSchema.parse(
4818
- await client.query.merchantSubscription.findFirst({
4819
- where: eq(merchantSubscriptionTable.id, merchantSubscriptionId),
4820
- }),
4821
- )
4822
- },
4823
- createMerchantSubscription: async (options: {
4824
- merchantAccountId: string
4825
- merchantCustomerId: string
4826
- merchantProductId: string
4827
- identifier: string
4828
- }) => {
4829
- const id = crypto.randomUUID()
4830
- await client.insert(merchantSubscriptionTable).values({
4831
- ...options,
4832
- id,
4833
- })
4834
-
4835
- return MerchantSubscriptionSchema.parse(
4836
- await client.query.merchantSubscription.findFirst({
4837
- where: eq(merchantSubscriptionTable.id, id),
4838
- }),
4839
- )
4840
- },
4841
- updateMerchantSubscription: async (options: {
4842
- merchantSubscriptionId: string
4843
- status: string
4844
- }) => {
4845
- throw new Error('Not implemented')
4846
- },
4847
- deleteMerchantSubscription: async (merchantSubscriptionId: string) => {
4848
- throw new Error('Not implemented')
4849
- },
4850
- getSubscriptionForStripeId: async (stripeSubscriptionId: string) => {
4851
- const merchantSubscriptionParsed = MerchantSubscriptionSchema.safeParse(
4852
- await client.query.merchantSubscription.findFirst({
4853
- where: eq(merchantSubscriptionTable.identifier, stripeSubscriptionId),
4854
- }),
4855
- )
4856
-
4857
- if (!merchantSubscriptionParsed.success) {
4858
- throw new Error(
4859
- `No merchant subscription found for stripe id ${stripeSubscriptionId} ${merchantSubscriptionParsed.error}`,
4860
- )
4861
- }
4862
-
4863
- const subscriptionParsed = SubscriptionSchema.safeParse(
4864
- await client.query.subscription.findFirst({
4865
- where: eq(
4866
- subscriptionTable.merchantSubscriptionId,
4867
- merchantSubscriptionParsed.data.id,
4868
- ),
4869
- with: {
4870
- product: true,
4871
- },
4872
- }),
4873
- )
4874
-
4875
- if (!subscriptionParsed.success) {
4876
- throw new Error(
4877
- `No subscription found for merchant subscription ${merchantSubscriptionParsed.data.id} ${subscriptionParsed.error}`,
4878
- )
4879
- }
4880
-
4881
- return subscriptionParsed.data
4882
- },
4883
- updateSubscriptionStatus: async (
4884
- subscriptionId: string,
4885
- status: string,
4886
- ) => {
4887
- await client
4888
- .update(subscriptionTable)
4889
- .set({ status })
4890
- .where(eq(subscriptionTable.id, subscriptionId))
4891
- },
4892
- }
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
+ }
4893
138
 
4894
139
  return adapter
4895
140
  }