@coursebuilder/adapter-drizzle 1.0.4 → 1.0.7

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 (231) hide show
  1. package/dist/{chunk-KPBVZ44W.js → chunk-2I7NIOKI.js} +2 -2
  2. package/dist/chunk-2X5XVNDN.js +12 -0
  3. package/dist/{chunk-3GONHHMY.js → chunk-3RQH7IPC.js} +4 -4
  4. package/dist/{chunk-BLDW3QUS.js → chunk-5NNBQJUP.js} +2 -2
  5. package/dist/{chunk-HWIQUI7O.js → chunk-5WMUBNWJ.js} +6 -5
  6. package/dist/{chunk-WJWJ6E5Y.js → chunk-65H5Y4PD.js} +2 -2
  7. package/dist/{chunk-MP4CUWYC.js → chunk-6SJORFGN.js} +2 -2
  8. package/dist/chunk-6XFVP2Q4.js +101 -0
  9. package/dist/chunk-7223HR2E.js +112 -0
  10. package/dist/{chunk-ET3QAIY7.js → chunk-CCXVDMTA.js} +2 -2
  11. package/dist/{chunk-DKJO4Y3K.js → chunk-DNBW5D64.js} +2 -2
  12. package/dist/{chunk-ARHWIBFG.js → chunk-DP6YPJZ4.js} +2804 -1252
  13. package/dist/{chunk-OBUGOZKM.js → chunk-EJ7UDN4T.js} +6 -5
  14. package/dist/{chunk-YQNIXCD3.js → chunk-ENIKVP6O.js} +6 -5
  15. package/dist/chunk-HEQ5MII4.js +12764 -0
  16. package/dist/{chunk-LPJ2P2KJ.js → chunk-JX53ECWI.js} +69 -48
  17. package/dist/chunk-LV3LIBNU.js +12764 -0
  18. package/dist/chunk-MQSLKVLJ.js +101 -0
  19. package/dist/chunk-MUFYDXA7.js +2143 -0
  20. package/dist/{chunk-WPJZAYX7.js → chunk-NON5CXDH.js} +4 -3
  21. package/dist/{chunk-FPAQINSA.js → chunk-O4PMFTPJ.js} +3 -3
  22. package/dist/{chunk-ORRM7KVW.js → chunk-OFZECDCQ.js} +2 -2
  23. package/dist/chunk-OK4NHV6Z.js +58 -0
  24. package/dist/{chunk-LERR6HQ3.js → chunk-OUJPUONP.js} +2 -2
  25. package/dist/{chunk-PBTEHA7F.js → chunk-P76GFFVH.js} +6 -5
  26. package/dist/{chunk-MGZMGDQ5.js → chunk-PMCOC5PM.js} +2 -2
  27. package/dist/{chunk-EHRARBRS.js → chunk-QKFITIXC.js} +2 -2
  28. package/dist/{chunk-6MRFUIH5.js → chunk-SIZFEXIS.js} +2 -2
  29. package/dist/chunk-SOOAUY4E.js +63 -0
  30. package/dist/chunk-SUOPB24V.js +78 -0
  31. package/dist/{chunk-253RMSB3.js → chunk-U6XVAMCK.js} +6 -5
  32. package/dist/chunk-UIQCNODZ.js +100 -0
  33. package/dist/{chunk-LZHZDFNZ.js → chunk-UWUDJCJM.js} +2 -2
  34. package/dist/{chunk-6YPHJWS2.js → chunk-WMCZWSAG.js} +2 -2
  35. package/dist/chunk-XKCPXM63.js +79 -0
  36. package/dist/chunk-XM5PWOCV.js +63 -0
  37. package/dist/chunk-YMVONZZW.js +49 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.js +26 -24
  40. package/dist/lib/mysql/index.cjs +3243 -1533
  41. package/dist/lib/mysql/index.cjs.map +1 -1
  42. package/dist/lib/mysql/index.d.cts +759 -238
  43. package/dist/lib/mysql/index.d.ts +759 -238
  44. package/dist/lib/mysql/index.js +30 -26
  45. package/dist/lib/mysql/schemas/auth/accounts.cjs +3 -2
  46. package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
  47. package/dist/lib/mysql/schemas/auth/accounts.d.cts +2 -1
  48. package/dist/lib/mysql/schemas/auth/accounts.d.ts +2 -1
  49. package/dist/lib/mysql/schemas/auth/accounts.js +12 -12
  50. package/dist/lib/mysql/schemas/auth/device-access-token.cjs +3 -2
  51. package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
  52. package/dist/lib/mysql/schemas/auth/device-access-token.d.cts +2 -1
  53. package/dist/lib/mysql/schemas/auth/device-access-token.d.ts +2 -1
  54. package/dist/lib/mysql/schemas/auth/device-access-token.js +13 -13
  55. package/dist/lib/mysql/schemas/auth/device-verification.cjs +3 -2
  56. package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
  57. package/dist/lib/mysql/schemas/auth/device-verification.d.cts +2 -1
  58. package/dist/lib/mysql/schemas/auth/device-verification.d.ts +2 -1
  59. package/dist/lib/mysql/schemas/auth/device-verification.js +13 -13
  60. package/dist/lib/mysql/schemas/auth/permissions.js +2 -2
  61. package/dist/lib/mysql/schemas/auth/profiles.cjs +3 -2
  62. package/dist/lib/mysql/schemas/auth/profiles.cjs.map +1 -1
  63. package/dist/lib/mysql/schemas/auth/profiles.d.cts +2 -1
  64. package/dist/lib/mysql/schemas/auth/profiles.d.ts +2 -1
  65. package/dist/lib/mysql/schemas/auth/profiles.js +12 -12
  66. package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
  67. package/dist/lib/mysql/schemas/auth/role-permissions.js +13 -13
  68. package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
  69. package/dist/lib/mysql/schemas/auth/roles.js +12 -12
  70. package/dist/lib/mysql/schemas/auth/sessions.cjs +3 -2
  71. package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
  72. package/dist/lib/mysql/schemas/auth/sessions.d.cts +2 -1
  73. package/dist/lib/mysql/schemas/auth/sessions.d.ts +2 -1
  74. package/dist/lib/mysql/schemas/auth/sessions.js +13 -13
  75. package/dist/lib/mysql/schemas/auth/user-permissions.cjs +3 -2
  76. package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
  77. package/dist/lib/mysql/schemas/auth/user-permissions.d.cts +2 -1
  78. package/dist/lib/mysql/schemas/auth/user-permissions.d.ts +2 -1
  79. package/dist/lib/mysql/schemas/auth/user-permissions.js +12 -12
  80. package/dist/lib/mysql/schemas/auth/user-prefs.cjs +3 -2
  81. package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
  82. package/dist/lib/mysql/schemas/auth/user-prefs.d.cts +2 -1
  83. package/dist/lib/mysql/schemas/auth/user-prefs.d.ts +2 -1
  84. package/dist/lib/mysql/schemas/auth/user-prefs.js +12 -12
  85. package/dist/lib/mysql/schemas/auth/user-roles.cjs +3 -2
  86. package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
  87. package/dist/lib/mysql/schemas/auth/user-roles.d.cts +2 -1
  88. package/dist/lib/mysql/schemas/auth/user-roles.d.ts +2 -1
  89. package/dist/lib/mysql/schemas/auth/user-roles.js +12 -12
  90. package/dist/lib/mysql/schemas/auth/users.cjs +11 -3
  91. package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
  92. package/dist/lib/mysql/schemas/auth/users.d.cts +5 -2
  93. package/dist/lib/mysql/schemas/auth/users.d.ts +5 -2
  94. package/dist/lib/mysql/schemas/auth/users.js +12 -12
  95. package/dist/lib/mysql/schemas/auth/verification-tokens.js +2 -2
  96. package/dist/lib/mysql/schemas/commerce/coupon.cjs +4 -2
  97. package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
  98. package/dist/lib/mysql/schemas/commerce/coupon.d.cts +17 -1
  99. package/dist/lib/mysql/schemas/commerce/coupon.d.ts +17 -1
  100. package/dist/lib/mysql/schemas/commerce/coupon.js +12 -12
  101. package/dist/lib/mysql/schemas/commerce/merchant-account.js +2 -2
  102. package/dist/lib/mysql/schemas/commerce/merchant-charge.cjs.map +1 -1
  103. package/dist/lib/mysql/schemas/commerce/merchant-charge.js +12 -12
  104. package/dist/lib/mysql/schemas/commerce/merchant-coupon.cjs +2 -1
  105. package/dist/lib/mysql/schemas/commerce/merchant-coupon.cjs.map +1 -1
  106. package/dist/lib/mysql/schemas/commerce/merchant-coupon.d.cts +17 -1
  107. package/dist/lib/mysql/schemas/commerce/merchant-coupon.d.ts +17 -1
  108. package/dist/lib/mysql/schemas/commerce/merchant-coupon.js +2 -2
  109. package/dist/lib/mysql/schemas/commerce/merchant-customer.js +2 -2
  110. package/dist/lib/mysql/schemas/commerce/merchant-events.cjs +120 -0
  111. package/dist/lib/mysql/schemas/commerce/merchant-events.cjs.map +1 -0
  112. package/dist/lib/mysql/schemas/commerce/merchant-events.d.cts +101 -0
  113. package/dist/lib/mysql/schemas/commerce/merchant-events.d.ts +101 -0
  114. package/dist/lib/mysql/schemas/commerce/merchant-events.js +11 -0
  115. package/dist/lib/mysql/schemas/commerce/merchant-events.js.map +1 -0
  116. package/dist/lib/mysql/schemas/commerce/merchant-price.js +2 -2
  117. package/dist/lib/mysql/schemas/commerce/merchant-product.js +2 -2
  118. package/dist/lib/mysql/schemas/commerce/merchant-session.js +2 -2
  119. package/dist/lib/mysql/schemas/commerce/merchant-subscription.cjs.map +1 -1
  120. package/dist/lib/mysql/schemas/commerce/merchant-subscription.js +12 -12
  121. package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
  122. package/dist/lib/mysql/schemas/commerce/price.js +12 -12
  123. package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
  124. package/dist/lib/mysql/schemas/commerce/product.js +12 -12
  125. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs +3 -2
  126. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
  127. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.d.cts +2 -1
  128. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.d.ts +2 -1
  129. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +13 -13
  130. package/dist/lib/mysql/schemas/commerce/purchase.cjs +5 -3
  131. package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
  132. package/dist/lib/mysql/schemas/commerce/purchase.d.cts +2 -1
  133. package/dist/lib/mysql/schemas/commerce/purchase.d.ts +2 -1
  134. package/dist/lib/mysql/schemas/commerce/purchase.js +12 -12
  135. package/dist/lib/mysql/schemas/commerce/subscription.cjs.map +1 -1
  136. package/dist/lib/mysql/schemas/commerce/subscription.js +12 -12
  137. package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
  138. package/dist/lib/mysql/schemas/commerce/upgradable-products.js +13 -13
  139. package/dist/lib/mysql/schemas/communication/comment.cjs +3 -2
  140. package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
  141. package/dist/lib/mysql/schemas/communication/comment.d.cts +2 -1
  142. package/dist/lib/mysql/schemas/communication/comment.d.ts +2 -1
  143. package/dist/lib/mysql/schemas/communication/comment.js +12 -12
  144. package/dist/lib/mysql/schemas/communication/communication-channel.js +2 -2
  145. package/dist/lib/mysql/schemas/communication/communication-preference-types.js +2 -2
  146. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs +3 -2
  147. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
  148. package/dist/lib/mysql/schemas/communication/communication-preferences.d.cts +2 -1
  149. package/dist/lib/mysql/schemas/communication/communication-preferences.d.ts +2 -1
  150. package/dist/lib/mysql/schemas/communication/communication-preferences.js +12 -12
  151. package/dist/lib/mysql/schemas/communication/question-response.cjs +351 -0
  152. package/dist/lib/mysql/schemas/communication/question-response.cjs.map +1 -0
  153. package/dist/lib/mysql/schemas/communication/question-response.d.cts +200 -0
  154. package/dist/lib/mysql/schemas/communication/question-response.d.ts +200 -0
  155. package/dist/lib/mysql/schemas/communication/question-response.js +21 -0
  156. package/dist/lib/mysql/schemas/communication/question-response.js.map +1 -0
  157. package/dist/lib/mysql/schemas/content/content-contributions.cjs +11 -3
  158. package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
  159. package/dist/lib/mysql/schemas/content/content-contributions.d.cts +2 -1
  160. package/dist/lib/mysql/schemas/content/content-contributions.d.ts +2 -1
  161. package/dist/lib/mysql/schemas/content/content-contributions.js +12 -12
  162. package/dist/lib/mysql/schemas/content/content-resource-product.cjs +8 -1
  163. package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
  164. package/dist/lib/mysql/schemas/content/content-resource-product.js +12 -12
  165. package/dist/lib/mysql/schemas/content/content-resource-resource.cjs +8 -1
  166. package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
  167. package/dist/lib/mysql/schemas/content/content-resource-resource.js +12 -12
  168. package/dist/lib/mysql/schemas/content/content-resource-tag.cjs +8 -1
  169. package/dist/lib/mysql/schemas/content/content-resource-tag.cjs.map +1 -1
  170. package/dist/lib/mysql/schemas/content/content-resource-tag.js +12 -12
  171. package/dist/lib/mysql/schemas/content/content-resource-version.cjs +11 -3
  172. package/dist/lib/mysql/schemas/content/content-resource-version.cjs.map +1 -1
  173. package/dist/lib/mysql/schemas/content/content-resource-version.d.cts +2 -1
  174. package/dist/lib/mysql/schemas/content/content-resource-version.d.ts +2 -1
  175. package/dist/lib/mysql/schemas/content/content-resource-version.js +12 -12
  176. package/dist/lib/mysql/schemas/content/content-resource.cjs +11 -3
  177. package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
  178. package/dist/lib/mysql/schemas/content/content-resource.d.cts +18 -1
  179. package/dist/lib/mysql/schemas/content/content-resource.d.ts +18 -1
  180. package/dist/lib/mysql/schemas/content/content-resource.js +12 -12
  181. package/dist/lib/mysql/schemas/content/contribution-types.js +2 -2
  182. package/dist/lib/mysql/schemas/content/lesson-progress.js +2 -2
  183. package/dist/lib/mysql/schemas/content/resource-progress.js +2 -2
  184. package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -1
  185. package/dist/lib/mysql/schemas/content/tag-tag.js +12 -12
  186. package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -1
  187. package/dist/lib/mysql/schemas/content/tag.js +12 -12
  188. package/dist/lib/mysql/schemas/entitlements/entitlement-type.js +2 -2
  189. package/dist/lib/mysql/schemas/entitlements/entitlement.cjs +3 -2
  190. package/dist/lib/mysql/schemas/entitlements/entitlement.cjs.map +1 -1
  191. package/dist/lib/mysql/schemas/entitlements/entitlement.d.cts +2 -1
  192. package/dist/lib/mysql/schemas/entitlements/entitlement.d.ts +2 -1
  193. package/dist/lib/mysql/schemas/entitlements/entitlement.js +13 -13
  194. package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs.map +1 -1
  195. package/dist/lib/mysql/schemas/org/organization-membership-roles.js +12 -12
  196. package/dist/lib/mysql/schemas/org/organization-memberships.cjs +3 -2
  197. package/dist/lib/mysql/schemas/org/organization-memberships.cjs.map +1 -1
  198. package/dist/lib/mysql/schemas/org/organization-memberships.d.cts +2 -1
  199. package/dist/lib/mysql/schemas/org/organization-memberships.d.ts +2 -1
  200. package/dist/lib/mysql/schemas/org/organization-memberships.js +12 -12
  201. package/dist/lib/mysql/schemas/org/organizations.cjs.map +1 -1
  202. package/dist/lib/mysql/schemas/org/organizations.js +12 -12
  203. package/dist/lib/mysql/utils.js +1 -1
  204. package/dist/lib/utils.d.cts +1 -0
  205. package/dist/lib/utils.d.ts +1 -0
  206. package/dist/lib/utils.js +1 -1
  207. package/package.json +7 -7
  208. package/src/lib/mysql/index.ts +1887 -153
  209. package/src/lib/mysql/schemas/auth/accounts.ts +7 -3
  210. package/src/lib/mysql/schemas/auth/device-access-token.ts +7 -3
  211. package/src/lib/mysql/schemas/auth/device-verification.ts +7 -3
  212. package/src/lib/mysql/schemas/auth/profiles.ts +7 -3
  213. package/src/lib/mysql/schemas/auth/sessions.ts +7 -3
  214. package/src/lib/mysql/schemas/auth/user-permissions.ts +7 -3
  215. package/src/lib/mysql/schemas/auth/user-prefs.ts +7 -3
  216. package/src/lib/mysql/schemas/auth/user-roles.ts +7 -3
  217. package/src/lib/mysql/schemas/auth/users.ts +17 -3
  218. package/src/lib/mysql/schemas/commerce/coupon.ts +2 -1
  219. package/src/lib/mysql/schemas/commerce/merchant-coupon.ts +2 -1
  220. package/src/lib/mysql/schemas/commerce/merchant-events.ts +62 -0
  221. package/src/lib/mysql/schemas/commerce/purchase-user-transfer.ts +4 -2
  222. package/src/lib/mysql/schemas/commerce/purchase.ts +7 -3
  223. package/src/lib/mysql/schemas/communication/comment.ts +7 -3
  224. package/src/lib/mysql/schemas/communication/communication-preferences.ts +4 -2
  225. package/src/lib/mysql/schemas/communication/question-response.ts +90 -0
  226. package/src/lib/mysql/schemas/content/content-contributions.ts +4 -2
  227. package/src/lib/mysql/schemas/content/content-resource-version.ts +4 -2
  228. package/src/lib/mysql/schemas/content/content-resource.ts +13 -3
  229. package/src/lib/mysql/schemas/entitlements/entitlement.ts +7 -3
  230. package/src/lib/mysql/schemas/org/organization-memberships.ts +4 -2
  231. package/dist/chunk-H736K5TN.js +0 -7
@@ -28,11 +28,15 @@ import { type CourseBuilderAdapter } from '@coursebuilder/core/adapters'
28
28
  import {
29
29
  Coupon,
30
30
  couponSchema,
31
+ Entitlement,
32
+ entitlementSchema,
31
33
  MerchantCharge,
32
34
  merchantChargeSchema,
33
35
  MerchantCoupon,
34
36
  merchantCouponSchema,
35
37
  MerchantCustomer,
38
+ MerchantEvents,
39
+ merchantEventsSchema,
36
40
  merchantPriceSchema,
37
41
  MerchantProduct,
38
42
  merchantProductSchema,
@@ -72,7 +76,7 @@ import { type ModuleProgress } from '@coursebuilder/core/schemas/resource-progre
72
76
  import { SubscriptionSchema } from '@coursebuilder/core/schemas/subscription'
73
77
  import { VideoResourceSchema } from '@coursebuilder/core/schemas/video-resource'
74
78
  import { PaymentsProviderConfig } from '@coursebuilder/core/types'
75
- import { logger } from '@coursebuilder/core/utils/logger'
79
+ import { logger as baseLogger } from '@coursebuilder/core/utils/logger'
76
80
  import { validateCoupon } from '@coursebuilder/core/utils/validate-coupon'
77
81
 
78
82
  import {
@@ -122,6 +126,7 @@ import {
122
126
  import {
123
127
  getUsersRelationsSchema,
124
128
  getUsersSchema,
129
+ type UsersSchemaOverrides,
125
130
  } from './schemas/auth/users.js'
126
131
  import { getVerificationTokensSchema } from './schemas/auth/verification-tokens.js'
127
132
  import {
@@ -135,6 +140,10 @@ import {
135
140
  } from './schemas/commerce/merchant-charge.js'
136
141
  import { getMerchantCouponSchema } from './schemas/commerce/merchant-coupon.js'
137
142
  import { getMerchantCustomerSchema } from './schemas/commerce/merchant-customer.js'
143
+ import {
144
+ getMerchantEventsRelationsSchema,
145
+ getMerchantEventsSchema,
146
+ } from './schemas/commerce/merchant-events.js'
138
147
  import { getMerchantPriceSchema } from './schemas/commerce/merchant-price.js'
139
148
  import { getMerchantProductSchema } from './schemas/commerce/merchant-product.js'
140
149
  import { getMerchantSessionSchema } from './schemas/commerce/merchant-session.js'
@@ -173,6 +182,10 @@ import {
173
182
  getCommunicationPreferencesRelationsSchema,
174
183
  getCommunicationPreferencesSchema,
175
184
  } from './schemas/communication/communication-preferences.js'
185
+ import {
186
+ getQuestionResponseRelationsSchema,
187
+ getQuestionResponseSchema,
188
+ } from './schemas/communication/question-response.js'
176
189
  import {
177
190
  getContentContributionRelationsSchema,
178
191
  getContentContributionsSchema,
@@ -228,10 +241,60 @@ import {
228
241
 
229
242
  export const guid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 5)
230
243
 
231
- export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
244
+ export const normalizeExpirationDate = (date: Date | undefined) => {
245
+ if (!date) return undefined
246
+
247
+ // Create a new Date object for 23:59:59 UTC on the date part of the input date
248
+ // The input date should be a JS Date representing 00:00:00 LA time for the chosen day.
249
+ // Its UTC date parts (getUTCFullYear, etc.) will give us the correct calendar day.
250
+ return new Date(
251
+ Date.UTC(
252
+ date.getUTCFullYear(),
253
+ date.getUTCMonth(), // 0-indexed
254
+ date.getUTCDate(),
255
+ 23, // hours
256
+ 59, // minutes
257
+ 59, // seconds
258
+ 0, // milliseconds
259
+ ),
260
+ )
261
+ }
262
+
263
+ function toError(error: unknown) {
264
+ return error instanceof Error ? error : new Error(String(error))
265
+ }
266
+
267
+ const logger = baseLogger as unknown as {
268
+ debug: (message: string, attrs?: Record<string, unknown>) => void
269
+ error: (error: Error, attrs?: Record<string, unknown>) => void
270
+ info: (message: string, attrs?: Record<string, unknown>) => void
271
+ }
272
+
273
+ export type CourseBuilderSchemaOverrides<
274
+ TUsers extends ReturnType<typeof getUsersSchema> = ReturnType<
275
+ typeof getUsersSchema
276
+ >,
277
+ > = UsersSchemaOverrides<TUsers>
278
+
279
+ export function getCourseBuilderSchema<
280
+ TUsers extends ReturnType<typeof getUsersSchema> = ReturnType<
281
+ typeof getUsersSchema
282
+ >,
283
+ >(
284
+ mysqlTable: MySqlTableFn,
285
+ overrides: CourseBuilderSchemaOverrides<TUsers> = {},
286
+ ) {
287
+ const resolvedGetUsersSchema = (overrides.getUsersSchema ??
288
+ getUsersSchema) as (mysqlTable: MySqlTableFn) => TUsers
289
+ const userRelationsOverrides: UsersSchemaOverrides<TUsers> = {
290
+ getUsersSchema: resolvedGetUsersSchema,
291
+ }
232
292
  return {
233
293
  accounts: getAccountsSchema(mysqlTable),
234
- accountsRelations: getAccountsRelationsSchema(mysqlTable),
294
+ accountsRelations: getAccountsRelationsSchema(
295
+ mysqlTable,
296
+ userRelationsOverrides,
297
+ ),
235
298
  permissions: getPermissionsSchema(mysqlTable),
236
299
  permissionsRelations: getPermissionsRelationsSchema(mysqlTable),
237
300
  rolePermissions: getRolePermissionsSchema(mysqlTable),
@@ -239,13 +302,22 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
239
302
  roles: getRolesSchema(mysqlTable),
240
303
  rolesRelations: getRolesRelationsSchema(mysqlTable),
241
304
  sessions: getSessionsSchema(mysqlTable),
242
- sessionsRelations: getSessionRelationsSchema(mysqlTable),
305
+ sessionsRelations: getSessionRelationsSchema(
306
+ mysqlTable,
307
+ userRelationsOverrides,
308
+ ),
243
309
  userPermissions: getUserPermissionsSchema(mysqlTable),
244
- userPermissionsRelations: getUserPermissionsRelationsSchema(mysqlTable),
310
+ userPermissionsRelations: getUserPermissionsRelationsSchema(
311
+ mysqlTable,
312
+ userRelationsOverrides,
313
+ ),
245
314
  userRoles: getUserRolesSchema(mysqlTable),
246
- userRolesRelations: getUserRolesRelationsSchema(mysqlTable),
247
- users: getUsersSchema(mysqlTable),
248
- usersRelations: getUsersRelationsSchema(mysqlTable),
315
+ userRolesRelations: getUserRolesRelationsSchema(
316
+ mysqlTable,
317
+ userRelationsOverrides,
318
+ ),
319
+ users: resolvedGetUsersSchema(mysqlTable),
320
+ usersRelations: getUsersRelationsSchema(mysqlTable, userRelationsOverrides),
249
321
  verificationTokens: getVerificationTokensSchema(mysqlTable),
250
322
  coupon: getCouponSchema(mysqlTable),
251
323
  couponRelations: getCouponRelationsSchema(mysqlTable),
@@ -253,6 +325,8 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
253
325
  merchantAccount: getMerchantAccountSchema(mysqlTable),
254
326
  merchantCharge: getMerchantChargeSchema(mysqlTable),
255
327
  merchantChargeRelations: getMerchantChargeRelationsSchema(mysqlTable),
328
+ merchantEvents: getMerchantEventsSchema(mysqlTable),
329
+ merchantEventsRelations: getMerchantEventsRelationsSchema(mysqlTable),
256
330
  merchantCoupon: getMerchantCouponSchema(mysqlTable),
257
331
  merchantCustomer: getMerchantCustomerSchema(mysqlTable),
258
332
  merchantPrice: getMerchantPriceSchema(mysqlTable),
@@ -261,24 +335,39 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
261
335
  prices: getPriceSchema(mysqlTable),
262
336
  products: getProductSchema(mysqlTable),
263
337
  purchases: getPurchaseSchema(mysqlTable),
264
- purchaseRelations: getPurchaseRelationsSchema(mysqlTable),
338
+ purchaseRelations: getPurchaseRelationsSchema(
339
+ mysqlTable,
340
+ userRelationsOverrides,
341
+ ),
265
342
  purchaseUserTransfer: getPurchaseUserTransferSchema(mysqlTable),
266
- purchaseUserTransferRelations:
267
- getPurchaseUserTransferRelationsSchema(mysqlTable),
343
+ purchaseUserTransferRelations: getPurchaseUserTransferRelationsSchema(
344
+ mysqlTable,
345
+ userRelationsOverrides,
346
+ ),
268
347
  communicationChannel: getCommunicationChannelSchema(mysqlTable),
269
348
  communicationPreferenceTypes:
270
349
  getCommunicationPreferenceTypesSchema(mysqlTable),
271
350
  communicationPreferences: getCommunicationPreferencesSchema(mysqlTable),
272
351
  communicationPreferencesRelations:
273
- getCommunicationPreferencesRelationsSchema(mysqlTable),
352
+ getCommunicationPreferencesRelationsSchema(
353
+ mysqlTable,
354
+ userRelationsOverrides,
355
+ ),
274
356
  contentContributions: getContentContributionsSchema(mysqlTable),
275
- contentContributionRelations:
276
- getContentContributionRelationsSchema(mysqlTable),
357
+ contentContributionRelations: getContentContributionRelationsSchema(
358
+ mysqlTable,
359
+ userRelationsOverrides,
360
+ ),
277
361
  contentResource: getContentResourceSchema(mysqlTable),
278
362
  contentResourceVersion: getContentResourceVersionSchema(mysqlTable),
279
- contentResourceVersionRelations:
280
- getContentResourceVersionRelationsSchema(mysqlTable),
281
- contentResourceRelations: getContentResourceRelationsSchema(mysqlTable),
363
+ contentResourceVersionRelations: getContentResourceVersionRelationsSchema(
364
+ mysqlTable,
365
+ userRelationsOverrides,
366
+ ),
367
+ contentResourceRelations: getContentResourceRelationsSchema(
368
+ mysqlTable,
369
+ userRelationsOverrides,
370
+ ),
282
371
  contentResourceResource: getContentResourceResourceSchema(mysqlTable),
283
372
  contentResourceResourceRelations:
284
373
  getContentResourceResourceRelationsSchema(mysqlTable),
@@ -288,6 +377,11 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
288
377
  contributionTypes: getContributionTypesSchema(mysqlTable),
289
378
  contributionTypesRelations: getContributionTypesRelationsSchema(mysqlTable),
290
379
  resourceProgress: getResourceProgressSchema(mysqlTable),
380
+ questionResponse: getQuestionResponseSchema(mysqlTable),
381
+ questionResponseRelations: getQuestionResponseRelationsSchema(
382
+ mysqlTable,
383
+ userRelationsOverrides,
384
+ ),
291
385
  upgradableProducts: getUpgradableProductsSchema(mysqlTable),
292
386
  upgradableProductsRelations:
293
387
  getUpgradableProductsRelationsSchema(mysqlTable),
@@ -296,23 +390,36 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
296
390
  getContentResourceProductRelationsSchema(mysqlTable),
297
391
  productRelations: getProductRelationsSchema(mysqlTable),
298
392
  comments: getCommentsSchema(mysqlTable),
299
- commentsRelations: getCommentRelationsSchema(mysqlTable),
393
+ commentsRelations: getCommentRelationsSchema(
394
+ mysqlTable,
395
+ userRelationsOverrides,
396
+ ),
300
397
  deviceVerifications: getDeviceVerificationSchema(mysqlTable),
301
- deviceVerificationRelations:
302
- getDeviceVerificationRelationsSchema(mysqlTable),
398
+ deviceVerificationRelations: getDeviceVerificationRelationsSchema(
399
+ mysqlTable,
400
+ userRelationsOverrides,
401
+ ),
303
402
  deviceAccessToken: getDeviceAccessTokenSchema(mysqlTable),
304
- deviceAccessTokenRelations: getDeviceAccessTokenRelationsSchema(mysqlTable),
403
+ deviceAccessTokenRelations: getDeviceAccessTokenRelationsSchema(
404
+ mysqlTable,
405
+ userRelationsOverrides,
406
+ ),
305
407
  tag: getTagSchema(mysqlTable),
306
408
  tagRelations: getTagRelationsSchema(mysqlTable),
307
409
  tagTag: getTagTagSchema(mysqlTable),
308
410
  tagTagRelations: getTagTagRelationsSchema(mysqlTable),
309
411
  userPrefs: getUserPrefsSchema(mysqlTable),
310
- userPrefsRelations: getUserPrefsRelationsSchema(mysqlTable),
412
+ userPrefsRelations: getUserPrefsRelationsSchema(
413
+ mysqlTable,
414
+ userRelationsOverrides,
415
+ ),
311
416
  organization: getOrganizationsSchema(mysqlTable),
312
417
  organizationRelations: getOrganizationsRelationsSchema(mysqlTable),
313
418
  organizationMemberships: getOrganizationMembershipsSchema(mysqlTable),
314
- organizationMembershipRelations:
315
- getOrganizationMembershipsRelationsSchema(mysqlTable),
419
+ organizationMembershipRelations: getOrganizationMembershipsRelationsSchema(
420
+ mysqlTable,
421
+ userRelationsOverrides,
422
+ ),
316
423
  organizationMembershipRoles:
317
424
  getOrganizationMembershipRolesSchema(mysqlTable),
318
425
  organizationMembershipRolesRelations:
@@ -323,10 +430,16 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
323
430
  subscription: getSubscriptionSchema(mysqlTable),
324
431
  subscriptionRelations: getSubscriptionRelationsSchema(mysqlTable),
325
432
  profiles: getProfilesSchema(mysqlTable),
326
- profilesRelations: getProfilesRelationsSchema(mysqlTable),
433
+ profilesRelations: getProfilesRelationsSchema(
434
+ mysqlTable,
435
+ userRelationsOverrides,
436
+ ),
327
437
  entitlementTypes: getEntitlementTypesSchema(mysqlTable),
328
438
  entitlements: getEntitlementsSchema(mysqlTable),
329
- entitlementsRelations: getEntitlementRelationsSchema(mysqlTable),
439
+ entitlementsRelations: getEntitlementRelationsSchema(
440
+ mysqlTable,
441
+ userRelationsOverrides,
442
+ ),
330
443
  } as const
331
444
  }
332
445
 
@@ -349,12 +462,14 @@ export function mySqlDrizzleAdapter(
349
462
  contentResource,
350
463
  contentResourceResource,
351
464
  contentResourceProduct,
465
+ contentResourceTag,
352
466
  purchases: purchaseTable,
353
467
  purchaseUserTransfer,
354
468
  coupon,
355
469
  merchantCoupon,
356
470
  merchantCharge,
357
471
  merchantAccount,
472
+ merchantEvents,
358
473
  merchantPrice,
359
474
  merchantCustomer,
360
475
  merchantSession,
@@ -370,6 +485,8 @@ export function mySqlDrizzleAdapter(
370
485
  roles: roleTable,
371
486
  merchantSubscription: merchantSubscriptionTable,
372
487
  subscription: subscriptionTable,
488
+ entitlements: entitlementTable,
489
+ entitlementTypes,
373
490
  } = createTables(tableFn)
374
491
 
375
492
  const adapter: CourseBuilderAdapter = {
@@ -420,7 +537,13 @@ export function mySqlDrizzleAdapter(
420
537
 
421
538
  if (existingPurchases.length > 0) {
422
539
  const errorMessage = `already-purchased-${email}`
423
- console.error(errorMessage)
540
+ logger.info('adapter.coupon.redeem.already_purchased', {
541
+ couponId: coupon.id,
542
+ currentUserId,
543
+ email,
544
+ productId,
545
+ userId: user.id,
546
+ })
424
547
  return {
425
548
  error: {
426
549
  message: errorMessage,
@@ -510,6 +633,21 @@ export function mySqlDrizzleAdapter(
510
633
  }),
511
634
  )
512
635
  },
636
+ createMerchantCoupon: async (options) => {
637
+ await client.insert(merchantCoupon).values({
638
+ id: `mcoupon_${v4()}`,
639
+ identifier: options.identifier,
640
+ merchantAccountId: options.merchantAccountId,
641
+ type: options.type,
642
+ amountDiscount: Math.floor(options.amountDiscount),
643
+ status: 1,
644
+ })
645
+ return merchantCouponSchema.parse(
646
+ await client.query.merchantCoupon.findFirst({
647
+ where: eq(merchantCoupon.identifier, options.identifier),
648
+ }),
649
+ )
650
+ },
513
651
  getMerchantAccount: async (options) => {
514
652
  return merchantAccountSchema.parse(
515
653
  await client.query.merchantAccount.findFirst({
@@ -529,10 +667,10 @@ export function mySqlDrizzleAdapter(
529
667
  merchantPriceSchema.safeParse(merchantPriceData)
530
668
 
531
669
  if (!parsedMerchantPrice.success) {
532
- console.error(
533
- 'Error parsing merchant price',
534
- JSON.stringify(parsedMerchantPrice.error),
535
- )
670
+ logger.error(parsedMerchantPrice.error, {
671
+ event: 'adapter.merchantPrice.parse_failed',
672
+ productId,
673
+ })
536
674
  return null
537
675
  }
538
676
 
@@ -638,7 +776,11 @@ export function mySqlDrizzleAdapter(
638
776
  resourceProgressSchema.safeParse(lessonProgress)
639
777
 
640
778
  if (!parsedLessonProgress.success) {
641
- console.error('Error parsing lesson progress', lessonProgress)
779
+ logger.error(parsedLessonProgress.error, {
780
+ event: 'adapter.lessonProgress.parse_failed',
781
+ lessonId: options.lessonId,
782
+ userId: options.userId,
783
+ })
642
784
  return null
643
785
  }
644
786
 
@@ -680,10 +822,11 @@ export function mySqlDrizzleAdapter(
680
822
  .safeParse(couponForIdOrCode)
681
823
 
682
824
  if (!parsedCoupon.success) {
683
- console.error(
684
- 'Error parsing coupon',
685
- JSON.stringify(parsedCoupon.error),
686
- )
825
+ logger.error(parsedCoupon.error, {
826
+ code: options.code,
827
+ couponId: options.couponId,
828
+ event: 'adapter.coupon.parse_failed',
829
+ })
687
830
  return null
688
831
  }
689
832
 
@@ -817,15 +960,35 @@ export function mySqlDrizzleAdapter(
817
960
  })
818
961
  .where(eq(coupon.id, bulkCouponId))
819
962
  } else {
820
- const merchantCouponToUse = stripeCouponId
963
+ // Try to find merchant coupon by stripeCouponId first
964
+ let merchantCouponToUse = stripeCouponId
821
965
  ? merchantCouponSchema.nullable().parse(
822
- await client.query.merchantCoupon.findFirst({
966
+ (await client.query.merchantCoupon.findFirst({
823
967
  where: eq(merchantCoupon.identifier, stripeCouponId),
824
- }),
968
+ })) || null,
825
969
  )
826
970
  : null
827
971
 
828
- couponToUpdate = await client.insert(coupon).values({
972
+ // Fallback: If not found, try to find via usedCouponId from checkout session metadata
973
+ if (!merchantCouponToUse && usedCouponId) {
974
+ // First get the coupon record to find its merchantCouponId
975
+ const usedCoupon = couponSchema.nullable().parse(
976
+ (await client.query.coupon.findFirst({
977
+ where: eq(coupon.id, usedCouponId),
978
+ })) || null,
979
+ )
980
+
981
+ // Then get the merchant coupon
982
+ if (usedCoupon?.merchantCouponId) {
983
+ merchantCouponToUse = merchantCouponSchema.nullable().parse(
984
+ (await client.query.merchantCoupon.findFirst({
985
+ where: eq(merchantCoupon.id, usedCoupon.merchantCouponId),
986
+ })) || null,
987
+ )
988
+ }
989
+ }
990
+
991
+ const bulkCouponValues = {
829
992
  id: bulkCouponId as string,
830
993
  percentageDiscount: '1.0',
831
994
  restrictedToProductId: productId,
@@ -837,7 +1000,11 @@ export function mySqlDrizzleAdapter(
837
1000
  merchantCouponId: merchantCouponToUse.id,
838
1001
  }
839
1002
  : {}),
840
- })
1003
+ }
1004
+
1005
+ couponToUpdate = await client
1006
+ .insert(coupon)
1007
+ .values(bulkCouponValues)
841
1008
  }
842
1009
  }
843
1010
 
@@ -909,7 +1076,14 @@ export function mySqlDrizzleAdapter(
909
1076
 
910
1077
  return purchaseId
911
1078
  } catch (error) {
912
- console.error(error)
1079
+ logger.error(toError(error), {
1080
+ checkoutSessionId: options.checkoutSessionId,
1081
+ event: 'adapter.purchase.create.failed',
1082
+ organizationId: options.organizationId,
1083
+ productId: options.productId,
1084
+ stripeChargeId: options.stripeChargeId,
1085
+ userId: options.userId,
1086
+ })
913
1087
  trx.rollback()
914
1088
  throw error
915
1089
  }
@@ -922,11 +1096,12 @@ export function mySqlDrizzleAdapter(
922
1096
  )
923
1097
 
924
1098
  if (!parsedPurchase.success) {
925
- console.error(
926
- 'Error parsing purchase',
927
- parsedPurchase,
928
- JSON.stringify(parsedPurchase, null, 2),
929
- )
1099
+ logger.error(parsedPurchase.error, {
1100
+ event: 'adapter.purchase.parse_failed',
1101
+ productId: options.productId,
1102
+ purchaseId: purchaseId as string,
1103
+ userId: options.userId,
1104
+ })
930
1105
  throw new Error('Error parsing purchase')
931
1106
  }
932
1107
 
@@ -957,6 +1132,45 @@ export function mySqlDrizzleAdapter(
957
1132
  userId: options.user.id,
958
1133
  })
959
1134
  },
1135
+ async createMerchantEvent(options: {
1136
+ merchantAccountId: string
1137
+ identifier: string
1138
+ payload: Record<string, any>
1139
+ }): Promise<MerchantEvents> {
1140
+ const eventId = `me_${v4()}`
1141
+
1142
+ await client.insert(merchantEvents).values({
1143
+ id: eventId,
1144
+ merchantAccountId: options.merchantAccountId,
1145
+ identifier: options.identifier,
1146
+ payload: options.payload,
1147
+ })
1148
+
1149
+ const createdEvent = await client.query.merchantEvents.findFirst({
1150
+ where: eq(merchantEvents.id, eventId),
1151
+ })
1152
+
1153
+ return merchantEventsSchema.parse(createdEvent)
1154
+ },
1155
+ async getMerchantEventByIdentifier(
1156
+ identifier: string,
1157
+ ): Promise<MerchantEvents | null> {
1158
+ const event = await client.query.merchantEvents.findFirst({
1159
+ where: eq(merchantEvents.identifier, identifier),
1160
+ })
1161
+
1162
+ return event ? merchantEventsSchema.parse(event) : null
1163
+ },
1164
+ async getMerchantEventsByAccount(
1165
+ merchantAccountId: string,
1166
+ ): Promise<MerchantEvents[]> {
1167
+ const events = await client.query.merchantEvents.findMany({
1168
+ where: eq(merchantEvents.merchantAccountId, merchantAccountId),
1169
+ orderBy: [desc(merchantEvents.createdAt)],
1170
+ })
1171
+
1172
+ return events.map((event) => merchantEventsSchema.parse(event))
1173
+ },
960
1174
  async findOrCreateUser(
961
1175
  email: string,
962
1176
  name?: string | null,
@@ -973,10 +1187,13 @@ export function mySqlDrizzleAdapter(
973
1187
  name,
974
1188
  emailVerified: null,
975
1189
  })
976
- console.log('newUser', { newUser })
977
1190
  if (!newUser) {
978
1191
  throw new Error('Could not create user')
979
1192
  }
1193
+ logger.info('adapter.user.created', {
1194
+ email,
1195
+ userId: newUser.id,
1196
+ })
980
1197
  return {
981
1198
  user: newUser as User,
982
1199
  isNewUser: true,
@@ -997,7 +1214,10 @@ export function mySqlDrizzleAdapter(
997
1214
  ),
998
1215
  })) || null
999
1216
 
1000
- logger.debug('loadedCoupon', { loadedCoupon })
1217
+ logger.debug('getCoupon', {
1218
+ couponIdOrCode,
1219
+ found: !!loadedCoupon,
1220
+ })
1001
1221
 
1002
1222
  return couponSchema.nullable().parse(loadedCoupon)
1003
1223
  },
@@ -1022,7 +1242,7 @@ export function mySqlDrizzleAdapter(
1022
1242
  > {
1023
1243
  logger.debug('getCouponWithBulkPurchases', { couponId })
1024
1244
  let couponData
1025
- let bulkCouponPurchases
1245
+ let bulkCouponPurchases: unknown[] = []
1026
1246
  try {
1027
1247
  couponData =
1028
1248
  (await client.query.coupon.findFirst({
@@ -1033,8 +1253,11 @@ export function mySqlDrizzleAdapter(
1033
1253
  },
1034
1254
  })) || null
1035
1255
  } catch (e) {
1036
- console.log('getCouponWithBulkPurchases')
1037
- logger.error(e as Error)
1256
+ logger.error(e as Error, {
1257
+ method: 'getCouponWithBulkPurchases',
1258
+ couponId,
1259
+ step: 'load-coupon',
1260
+ })
1038
1261
  }
1039
1262
 
1040
1263
  try {
@@ -1044,10 +1267,16 @@ export function mySqlDrizzleAdapter(
1044
1267
  user: true,
1045
1268
  },
1046
1269
  })
1047
- console.log('purchases with redeemedBulkCouponId', bulkCouponPurchases)
1270
+ logger.debug('getCouponWithBulkPurchases.redeemed', {
1271
+ couponId,
1272
+ count: bulkCouponPurchases?.length ?? 0,
1273
+ })
1048
1274
  } catch (e) {
1049
- console.log('getCouponWithBulkPurchases')
1050
- logger.error(e as Error)
1275
+ logger.error(e as Error, {
1276
+ method: 'getCouponWithBulkPurchases',
1277
+ couponId,
1278
+ step: 'load-redeemed-purchases',
1279
+ })
1051
1280
  }
1052
1281
 
1053
1282
  if (!couponData) {
@@ -1073,11 +1302,11 @@ export function mySqlDrizzleAdapter(
1073
1302
  .safeParse(couponWithBulkPurchases)
1074
1303
 
1075
1304
  if (!parsedCoupon.success) {
1076
- console.error(
1077
- 'Error parsing coupon',
1078
- JSON.stringify(parsedCoupon.error),
1079
- couponData,
1080
- )
1305
+ logger.error(parsedCoupon.error, {
1306
+ couponId: couponData?.id ?? couponId,
1307
+ event: 'adapter.coupon.parse_failed',
1308
+ redeemedBulkCouponPurchasesCount: bulkCouponPurchases?.length ?? 0,
1309
+ })
1081
1310
  return null
1082
1311
  }
1083
1312
 
@@ -1141,7 +1370,10 @@ export function mySqlDrizzleAdapter(
1141
1370
  })
1142
1371
  const parsed = z.array(resourceProgressSchema).safeParse(userProgress)
1143
1372
  if (!parsed.success) {
1144
- console.error('Error parsing user progress', userProgress)
1373
+ logger.error(parsed.error, {
1374
+ event: 'adapter.userProgress.parse_failed',
1375
+ userId,
1376
+ })
1145
1377
  return []
1146
1378
  }
1147
1379
  return parsed.data
@@ -1172,41 +1404,63 @@ export function mySqlDrizzleAdapter(
1172
1404
  .transform((val) => (val ? new Date(val) : null)),
1173
1405
  })
1174
1406
 
1175
- // Execute the optimized query
1407
+ // Execute the optimized query - create a flattened, properly ordered list
1176
1408
  const results: any = await client.execute(sql`
1177
- SELECT
1178
- cr.id AS resource_id,
1409
+ WITH RECURSIVE workshop AS (
1410
+ SELECT id, fields->>'$.slug' AS slug
1411
+ FROM ${contentResource}
1412
+ WHERE id = ${moduleIdOrSlug} OR fields->>'$.slug' = ${moduleIdOrSlug}
1413
+ ),
1414
+ -- Get all workshop resources with their positions
1415
+ workshop_structure AS (
1416
+ SELECT
1417
+ w.id AS workshop_id,
1418
+ crr.resourceId,
1419
+ crr.position AS position,
1179
1420
  cr.type AS resource_type,
1180
1421
  cr.fields->>'$.slug' AS resource_slug,
1181
- rp.completedAt AS completed_at
1182
- FROM
1183
- (SELECT id, fields->>'$.slug' AS slug
1184
- FROM ${contentResource}
1185
- WHERE id = ${moduleIdOrSlug}
1186
- OR fields->>'$.slug' = ${moduleIdOrSlug}) AS w
1187
- LEFT JOIN
1188
- (SELECT
1189
- crr.resourceOfId AS workshop_id,
1190
- crr.resourceId,
1191
- crr.position
1192
- FROM ${contentResourceResource} crr) AS tlr ON w.id = tlr.workshop_id
1193
- LEFT JOIN ${contentResource} sections ON sections.id = tlr.resourceId AND sections.type = 'section'
1194
- LEFT JOIN
1195
- (SELECT
1196
- crr.resourceOfId AS section_id,
1197
- crr.resourceId AS resource_id,
1198
- crr.position AS position_in_section,
1199
- section_crr.position AS section_position
1200
- FROM ${contentResourceResource} crr
1201
- JOIN ${contentResourceResource} section_crr ON crr.resourceOfId = section_crr.resourceId) AS sr
1202
- ON sr.section_id = sections.id
1203
- JOIN ${contentResource} cr ON cr.id = COALESCE(sr.resource_id, tlr.resourceId)
1204
- LEFT JOIN ${resourceProgress} rp ON rp.resourceId = cr.id
1205
- AND rp.userId = ${user.id}
1206
- WHERE
1207
- cr.type IN ('lesson', 'exercise', 'post')
1208
-
1209
- `)
1422
+ cr.id AS resource_content_id
1423
+ FROM workshop w
1424
+ JOIN ${contentResourceResource} crr ON w.id = crr.resourceOfId
1425
+ JOIN ${contentResource} cr ON crr.resourceId = cr.id
1426
+ ORDER BY crr.position
1427
+ ),
1428
+ -- Recursively expand sections and maintain global ordering
1429
+ expanded_resources AS (
1430
+ -- Base case: direct lessons/posts from workshop
1431
+ SELECT
1432
+ workshop_id,
1433
+ position * 1000 AS global_order, -- Multiply by 1000 to leave room for section items
1434
+ resource_content_id AS resource_id,
1435
+ resource_type,
1436
+ resource_slug
1437
+ FROM workshop_structure
1438
+ WHERE resource_type IN ('lesson', 'post')
1439
+ UNION ALL
1440
+ -- Recursive case: lessons within sections
1441
+ SELECT
1442
+ ws.workshop_id,
1443
+ ws.position * 1000 + crr.position AS global_order, -- Section position * 1000 + lesson position
1444
+ cr.id AS resource_id,
1445
+ cr.type AS resource_type,
1446
+ cr.fields->>'$.slug' AS resource_slug
1447
+ FROM workshop_structure ws
1448
+ JOIN ${contentResourceResource} crr ON ws.resource_content_id = crr.resourceOfId
1449
+ JOIN ${contentResource} cr ON crr.resourceId = cr.id
1450
+ WHERE ws.resource_type = 'section'
1451
+ AND cr.type IN ('lesson', 'post')
1452
+ )
1453
+ SELECT
1454
+ er.resource_id,
1455
+ er.resource_type,
1456
+ er.resource_slug,
1457
+ rp.completedAt AS completed_at,
1458
+ er.global_order
1459
+ FROM expanded_resources er
1460
+ LEFT JOIN ${resourceProgress} rp ON rp.resourceId = er.resource_id
1461
+ AND rp.userId = ${user.id}
1462
+ ORDER BY er.global_order ASC
1463
+ `)
1210
1464
  // Process the results
1211
1465
  const completedLessons: ResourceProgress[] = []
1212
1466
  let nextResource: Partial<ContentResource> | null = null
@@ -1216,7 +1470,11 @@ export function mySqlDrizzleAdapter(
1216
1470
  const parsedRows = z.array(ResultRowSchema).safeParse(results.rows)
1217
1471
 
1218
1472
  if (!parsedRows.success) {
1219
- console.error('Error parsing rows', parsedRows.error)
1473
+ logger.error(parsedRows.error, {
1474
+ event: 'adapter.moduleProgress.rows.parse_failed',
1475
+ moduleIdOrSlug,
1476
+ userId: user.id,
1477
+ })
1220
1478
  return {
1221
1479
  completedLessons: [],
1222
1480
  nextResource: null,
@@ -1270,7 +1528,10 @@ export function mySqlDrizzleAdapter(
1270
1528
  })
1271
1529
  const parsed = merchantChargeSchema.safeParse(mCharge)
1272
1530
  if (!parsed.success) {
1273
- console.info('Error parsing merchantCharge', mCharge)
1531
+ logger.error(parsed.error, {
1532
+ event: 'adapter.merchantCharge.parse_failed',
1533
+ merchantChargeId,
1534
+ })
1274
1535
  return null
1275
1536
  }
1276
1537
  return parsed.data
@@ -1314,6 +1575,26 @@ export function mySqlDrizzleAdapter(
1314
1575
 
1315
1576
  return null
1316
1577
  },
1578
+ async getMerchantCouponForTypeAndAmount(params: {
1579
+ type: string
1580
+ amountDiscount: number
1581
+ }): Promise<MerchantCoupon | null> {
1582
+ const foundMerchantCoupon = await client.query.merchantCoupon.findFirst({
1583
+ where: and(
1584
+ eq(merchantCoupon.type, params.type),
1585
+ eq(merchantCoupon.amountDiscount, params.amountDiscount),
1586
+ ),
1587
+ })
1588
+
1589
+ const parsed = merchantCouponSchema
1590
+ .nullable()
1591
+ .safeParse(foundMerchantCoupon)
1592
+ if (parsed.success) {
1593
+ return parsed.data
1594
+ }
1595
+
1596
+ return null
1597
+ },
1317
1598
  async getMerchantCoupon(
1318
1599
  merchantCouponId: string,
1319
1600
  ): Promise<MerchantCoupon | null> {
@@ -1445,26 +1726,56 @@ export function mySqlDrizzleAdapter(
1445
1726
  currentProduct.price.unitAmount.toString() !==
1446
1727
  input.price?.unitAmount.toString()
1447
1728
 
1448
- if (priceChanged) {
1449
- const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1450
- await client.query.merchantPrice.findFirst({
1451
- where: (merchantPrice, { eq, and }) =>
1452
- and(
1453
- eq(merchantPrice.merchantProductId, merchantProduct.id),
1454
- eq(merchantPrice.status, 1),
1455
- ),
1456
- }),
1457
- )
1729
+ // Get current merchant price and Stripe price to check if it needs updating
1730
+ const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1731
+ await client.query.merchantPrice.findFirst({
1732
+ where: (merchantPrice, { eq, and }) =>
1733
+ and(
1734
+ eq(merchantPrice.merchantProductId, merchantProduct.id),
1735
+ eq(merchantPrice.status, 1),
1736
+ ),
1737
+ }),
1738
+ )
1458
1739
 
1459
- if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1460
- throw new Error(`Merchant price not found`)
1461
- }
1740
+ if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1741
+ throw new Error(`Merchant price not found`)
1742
+ }
1462
1743
 
1463
- const currentStripePrice = await paymentProvider.getPrice(
1464
- currentMerchantPrice.identifier,
1465
- )
1744
+ const currentStripePrice = await paymentProvider.getPrice(
1745
+ currentMerchantPrice.identifier,
1746
+ )
1466
1747
 
1467
- const newStripePrice = await paymentProvider.createPrice({
1748
+ // Check if type changed to/from membership (requires Stripe price recreation)
1749
+ const membershipTypeChanged =
1750
+ currentProduct.type !== input.type &&
1751
+ (input.type === 'membership' || currentProduct.type === 'membership')
1752
+
1753
+ // Check if billing interval changed for membership products
1754
+ const billingIntervalChanged =
1755
+ input.type === 'membership' &&
1756
+ currentProduct.type === 'membership' &&
1757
+ currentProduct.fields?.billingInterval !== input.fields?.billingInterval
1758
+
1759
+ // Check if Stripe price recurring status doesn't match expected state for input type:
1760
+ // - membership products require recurring=true
1761
+ // - non-membership products require recurring=false (null/undefined)
1762
+ const isMembership = input.type === 'membership'
1763
+ const stripePriceMismatch =
1764
+ isMembership !== !!currentStripePrice.recurring
1765
+
1766
+ // Need to recreate price if: price changed, type changed to/from membership,
1767
+ // billing interval changed, or Stripe price doesn't match expected recurring status
1768
+ const needsPriceRecreation =
1769
+ priceChanged ||
1770
+ membershipTypeChanged ||
1771
+ billingIntervalChanged ||
1772
+ stripePriceMismatch
1773
+
1774
+ if (needsPriceRecreation) {
1775
+ // Get billing interval from input or default to 'year'
1776
+ const billingInterval = input.fields?.billingInterval || 'year'
1777
+
1778
+ const priceParams = {
1468
1779
  product: stripeProduct.id,
1469
1780
  unit_amount: Math.floor(Number(input.price?.unitAmount || 0) * 100),
1470
1781
  currency: 'usd',
@@ -1472,8 +1783,26 @@ export function mySqlDrizzleAdapter(
1472
1783
  slug: input.fields.slug,
1473
1784
  },
1474
1785
  active: true,
1786
+ // Membership products get recurring billing
1787
+ ...(input.type === 'membership' && {
1788
+ recurring: { interval: billingInterval },
1789
+ }),
1790
+ }
1791
+
1792
+ logger.debug('adapter.product.price_recreation.started', {
1793
+ billingInterval,
1794
+ currency: priceParams.currency,
1795
+ currentMerchantPriceId: currentMerchantPrice.id,
1796
+ isMembership: input.type === 'membership',
1797
+ merchantProductId: merchantProduct.id,
1798
+ productId: input.id,
1799
+ productSlug: input.fields.slug,
1800
+ stripeProductId: stripeProduct.id,
1801
+ unitAmount: priceParams.unit_amount,
1475
1802
  })
1476
1803
 
1804
+ const newStripePrice = await paymentProvider.createPrice(priceParams)
1805
+
1477
1806
  await paymentProvider.updateProduct(stripeProduct.id, {
1478
1807
  default_price: newStripePrice.id,
1479
1808
  })
@@ -1568,6 +1897,16 @@ export function mySqlDrizzleAdapter(
1568
1897
  state: input.state || 'draft',
1569
1898
  visibility: input.visibility || 'unlisted',
1570
1899
  slug: slugify(`${input.name}-${hash}`),
1900
+ ...(input.openEnrollment && { openEnrollment: input.openEnrollment }),
1901
+ ...(input.closeEnrollment && {
1902
+ closeEnrollment: input.closeEnrollment,
1903
+ }),
1904
+ ...(typeof input.availableAfterDays === 'number' && {
1905
+ availableAfterDays: input.availableAfterDays,
1906
+ }),
1907
+ ...(typeof input.accessDurationDays === 'number' && {
1908
+ accessDurationDays: input.accessDurationDays,
1909
+ }),
1571
1910
  },
1572
1911
  }
1573
1912
 
@@ -1600,6 +1939,10 @@ export function mySqlDrizzleAdapter(
1600
1939
  metadata: {
1601
1940
  slug: product?.fields?.slug || null,
1602
1941
  },
1942
+ // Membership products get recurring billing
1943
+ ...(input.type === 'membership' && {
1944
+ recurring: { interval: input.billingInterval || 'year' },
1945
+ }),
1603
1946
  })
1604
1947
 
1605
1948
  const newMerchantProductId = `mproduct_${v4()}`
@@ -1662,16 +2005,20 @@ export function mySqlDrizzleAdapter(
1662
2005
  })
1663
2006
  const parsedProduct = productSchema.safeParse(productData)
1664
2007
  if (!parsedProduct.success) {
1665
- console.error(
1666
- 'Error parsing product',
1667
- JSON.stringify(parsedProduct.error),
1668
- JSON.stringify(productData),
1669
- )
2008
+ logger.error(parsedProduct.error, {
2009
+ event: 'adapter.product.parse_failed',
2010
+ productId: productData?.id,
2011
+ productSlugOrId,
2012
+ })
1670
2013
  return null
1671
2014
  }
1672
2015
  return parsedProduct.data
1673
2016
  } catch (e) {
1674
- console.log('getProduct error', e)
2017
+ logger.error(toError(e), {
2018
+ event: 'adapter.product.lookup.failed',
2019
+ productSlugOrId,
2020
+ withResources,
2021
+ })
1675
2022
  return null
1676
2023
  }
1677
2024
  },
@@ -1742,11 +2089,14 @@ export function mySqlDrizzleAdapter(
1742
2089
  )
1743
2090
 
1744
2091
  if (!chargeForPurchase) {
1745
- logger.error(
1746
- new Error(
1747
- `No charge found for purchase: Stripe Charge ID: ${stripeChargeId}`,
1748
- ),
1749
- )
2092
+ // Expected during initial checkout processing — the merchant
2093
+ // charge hasn't been created yet. Also expected when the
2094
+ // payment-status-checker passes a PaymentIntent ID (pi_xxx)
2095
+ // instead of a Charge ID (ch_xxx).
2096
+ logger.debug('getPurchaseForStripeCharge: no matching charge', {
2097
+ stripeChargeId,
2098
+ idPrefix: stripeChargeId?.substring(0, 3),
2099
+ })
1750
2100
  return null
1751
2101
  }
1752
2102
 
@@ -1767,6 +2117,40 @@ export function mySqlDrizzleAdapter(
1767
2117
 
1768
2118
  return purchase.data
1769
2119
  },
2120
+ async getPurchaseByCheckoutSessionId(
2121
+ checkoutSessionId: string,
2122
+ ): Promise<Purchase | null> {
2123
+ logger.debug('getPurchaseByCheckoutSessionId', { checkoutSessionId })
2124
+
2125
+ // Find the MerchantSession by its identifier (the Stripe checkout session ID)
2126
+ const session = await client.query.merchantSession.findFirst({
2127
+ where: eq(merchantSession.identifier, checkoutSessionId),
2128
+ })
2129
+
2130
+ if (!session) {
2131
+ logger.debug('getPurchaseByCheckoutSessionId: no matching session', {
2132
+ checkoutSessionId,
2133
+ })
2134
+ return null
2135
+ }
2136
+
2137
+ const purchase = purchaseSchema.safeParse(
2138
+ await client.query.purchases.findFirst({
2139
+ where: eq(purchaseTable.merchantSessionId, String(session.id)),
2140
+ with: {
2141
+ user: true,
2142
+ product: true,
2143
+ bulkCoupon: true,
2144
+ },
2145
+ }),
2146
+ )
2147
+
2148
+ if (!purchase.success) {
2149
+ return null
2150
+ }
2151
+
2152
+ return purchase.data
2153
+ },
1770
2154
  async getPurchaseUserTransferById(options: { id: string }): Promise<
1771
2155
  | (PurchaseUserTransfer & {
1772
2156
  sourceUser: User
@@ -1817,7 +2201,10 @@ export function mySqlDrizzleAdapter(
1817
2201
  .safeParse(purchaseData)
1818
2202
 
1819
2203
  if (!parsedPurchase.success) {
1820
- console.error('Error parsing purchase', parsedPurchase.error)
2204
+ logger.error(parsedPurchase.error, {
2205
+ event: 'adapter.purchase.parse_failed',
2206
+ purchaseId,
2207
+ })
1821
2208
  return null
1822
2209
  }
1823
2210
  return parsedPurchase.data
@@ -1845,15 +2232,85 @@ export function mySqlDrizzleAdapter(
1845
2232
  const parsedPurchases = z.array(purchaseSchema).safeParse(userPurchases)
1846
2233
 
1847
2234
  if (!parsedPurchases.success) {
1848
- console.error(
1849
- 'Error parsing purchases',
1850
- JSON.stringify(parsedPurchases.error),
1851
- )
2235
+ logger.error(parsedPurchases.error, {
2236
+ event: 'adapter.purchase.list.parse_failed',
2237
+ purchaseCount: userPurchases.length,
2238
+ userId,
2239
+ })
1852
2240
  return []
1853
2241
  }
1854
2242
 
1855
2243
  return parsedPurchases.data
1856
2244
  },
2245
+ async getEntitlementsForUser(params: {
2246
+ userId: string
2247
+ sourceType?: string
2248
+ entitlementType?: string
2249
+ }): Promise<Entitlement[]> {
2250
+ const { userId, sourceType, entitlementType } = params
2251
+
2252
+ if (!userId) {
2253
+ return []
2254
+ }
2255
+
2256
+ const conditions = [eq(entitlementTable.userId, userId)]
2257
+
2258
+ if (sourceType) {
2259
+ conditions.push(eq(entitlementTable.sourceType, sourceType))
2260
+ }
2261
+
2262
+ if (entitlementType) {
2263
+ conditions.push(eq(entitlementTable.entitlementType, entitlementType))
2264
+ }
2265
+
2266
+ // Only return active entitlements (not deleted, not expired)
2267
+ conditions.push(isNull(entitlementTable.deletedAt))
2268
+ const expiresCondition = or(
2269
+ isNull(entitlementTable.expiresAt),
2270
+ gte(entitlementTable.expiresAt, sql`CURRENT_TIMESTAMP`),
2271
+ )
2272
+ if (expiresCondition) {
2273
+ conditions.push(expiresCondition)
2274
+ }
2275
+
2276
+ const userEntitlements = await client.query.entitlements.findMany({
2277
+ where: conditions.length > 0 ? and(...conditions) : undefined,
2278
+ orderBy: asc(entitlementTable.createdAt),
2279
+ })
2280
+
2281
+ const parsedEntitlements = z
2282
+ .array(entitlementSchema)
2283
+ .safeParse(userEntitlements)
2284
+
2285
+ if (!parsedEntitlements.success) {
2286
+ logger.error(parsedEntitlements.error, {
2287
+ entitlementCount: userEntitlements.length,
2288
+ entitlementType,
2289
+ event: 'adapter.entitlement.list.parse_failed',
2290
+ sourceType,
2291
+ userId,
2292
+ })
2293
+ return []
2294
+ }
2295
+
2296
+ return parsedEntitlements.data
2297
+ },
2298
+ async getEntitlementTypeByName(
2299
+ name: string,
2300
+ ): Promise<{ id: string; name: string } | null> {
2301
+ const entitlementType = await client.query.entitlementTypes.findFirst({
2302
+ where: eq(entitlementTypes.name, name),
2303
+ })
2304
+
2305
+ if (!entitlementType) {
2306
+ return null
2307
+ }
2308
+
2309
+ return {
2310
+ id: String(entitlementType.id),
2311
+ name: String(entitlementType.name),
2312
+ }
2313
+ },
1857
2314
  async getPurchaseDetails(
1858
2315
  purchaseId: string,
1859
2316
  userId: string,
@@ -1896,7 +2353,11 @@ export function mySqlDrizzleAdapter(
1896
2353
  const parsedPurchase = purchaseSchema.safeParse(thePurchase)
1897
2354
 
1898
2355
  if (!parsedPurchase.success) {
1899
- console.error('Error parsing purchase', parsedPurchase)
2356
+ logger.error(parsedPurchase.error, {
2357
+ event: 'adapter.purchase.details.parse_failed',
2358
+ purchaseId,
2359
+ userId,
2360
+ })
1900
2361
  return {
1901
2362
  availableUpgrades: [],
1902
2363
  }
@@ -2095,7 +2556,11 @@ export function mySqlDrizzleAdapter(
2095
2556
  resourceProgressSchema.safeParse(lessonProgress)
2096
2557
 
2097
2558
  if (!parsedLessonProgress.success) {
2098
- console.error('Error parsing lesson progress', lessonProgress)
2559
+ logger.error(parsedLessonProgress.error, {
2560
+ event: 'adapter.lessonProgress.parse_failed',
2561
+ lessonId: options.lessonId,
2562
+ userId: options.userId,
2563
+ })
2099
2564
  return null
2100
2565
  }
2101
2566
 
@@ -2206,7 +2671,10 @@ export function mySqlDrizzleAdapter(
2206
2671
  status: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',
2207
2672
  ): Promise<Purchase | undefined> {
2208
2673
  try {
2209
- console.log('updatePurchaseStatusForCharge', { chargeId, status })
2674
+ logger.debug('adapter.purchase.status_update.started', {
2675
+ chargeId,
2676
+ status,
2677
+ })
2210
2678
  const merchantChargeForPurchase =
2211
2679
  await client.query.merchantCharge.findFirst({
2212
2680
  where: or(
@@ -2215,7 +2683,12 @@ export function mySqlDrizzleAdapter(
2215
2683
  ),
2216
2684
  })
2217
2685
 
2218
- console.log('merchantChargeForPurchase', { merchantChargeForPurchase })
2686
+ logger.debug('adapter.purchase.status_update.charge_resolved', {
2687
+ chargeId,
2688
+ merchantChargeId: merchantChargeForPurchase?.id ?? null,
2689
+ merchantChargeIdentifier:
2690
+ merchantChargeForPurchase?.identifier ?? null,
2691
+ })
2219
2692
 
2220
2693
  const parsedMerchantChargeForPurchase = merchantChargeSchema.parse(
2221
2694
  merchantChargeForPurchase,
@@ -2248,7 +2721,11 @@ export function mySqlDrizzleAdapter(
2248
2721
  throw new Error(`no-purchase-found-for-charge ${chargeId}`)
2249
2722
  }
2250
2723
  } catch (e) {
2251
- console.log('error updating purchase status', e)
2724
+ logger.error(toError(e), {
2725
+ chargeId,
2726
+ event: 'adapter.purchase.status_update.failed',
2727
+ status,
2728
+ })
2252
2729
  throw e
2253
2730
  }
2254
2731
  },
@@ -2383,7 +2860,10 @@ export function mySqlDrizzleAdapter(
2383
2860
  const parsedResource = ContentResourceSchema.safeParse(resource)
2384
2861
 
2385
2862
  if (!parsedResource.success) {
2386
- console.error('Error parsing resource', resource)
2863
+ logger.error(parsedResource.error, {
2864
+ event: 'adapter.contentResource.parse_failed',
2865
+ resourceId: options.id,
2866
+ })
2387
2867
  return null
2388
2868
  }
2389
2869
 
@@ -2418,10 +2898,63 @@ export function mySqlDrizzleAdapter(
2418
2898
  return parsedResource.success ? parsedResource.data : null
2419
2899
  })
2420
2900
  .catch((error) => {
2421
- console.error(error)
2901
+ logger.error(toError(error), {
2902
+ event: 'adapter.videoResource.lookup.failed',
2903
+ resourceId: id,
2904
+ })
2422
2905
  throw error
2423
2906
  })
2424
2907
  },
2908
+ async getParentResourceOfVideoResource(
2909
+ videoResourceId: string,
2910
+ ): Promise<ContentResource | null> {
2911
+ if (!videoResourceId) {
2912
+ return null
2913
+ }
2914
+
2915
+ // Use a SQL query to find the parent post or lesson of this video resource
2916
+ const results = await client
2917
+ .select()
2918
+ .from(contentResource)
2919
+ .innerJoin(
2920
+ contentResourceResource,
2921
+ eq(contentResourceResource.resourceOfId, contentResource.id),
2922
+ )
2923
+ .where(
2924
+ and(
2925
+ eq(contentResourceResource.resourceId, videoResourceId),
2926
+ or(
2927
+ eq(contentResource.type, 'post'),
2928
+ eq(contentResource.type, 'lesson'),
2929
+ ),
2930
+ ),
2931
+ )
2932
+ .limit(1)
2933
+
2934
+ if (!results || results.length === 0) {
2935
+ return null
2936
+ }
2937
+
2938
+ const resourceData = results[0]?.ContentResource
2939
+
2940
+ if (!resourceData) {
2941
+ return null
2942
+ }
2943
+
2944
+ // Fetch the full resource with relationships
2945
+ const fullResource = await client.query.contentResource.findFirst({
2946
+ where: eq(contentResource.id, resourceData.id),
2947
+ with: {
2948
+ resources: true,
2949
+ },
2950
+ })
2951
+
2952
+ if (!fullResource) {
2953
+ return null
2954
+ }
2955
+
2956
+ return ContentResourceSchema.parse(fullResource)
2957
+ },
2425
2958
  async createContentResource(data) {
2426
2959
  const id = data.id || crypto.randomUUID()
2427
2960
 
@@ -2451,7 +2984,11 @@ export function mySqlDrizzleAdapter(
2451
2984
  const parsedResource = ContentResourceSchema.safeParse(resource)
2452
2985
 
2453
2986
  if (!parsedResource.success) {
2454
- console.error('Error parsing resource', resource)
2987
+ logger.error(parsedResource.error, {
2988
+ event: 'adapter.contentResource.parse_failed',
2989
+ resourceId: id,
2990
+ resourceType: data.type,
2991
+ })
2455
2992
  throw new Error('Error parsing resource')
2456
2993
  }
2457
2994
 
@@ -2496,32 +3033,1216 @@ export function mySqlDrizzleAdapter(
2496
3033
  const parsedResource = ContentResourceSchema.safeParse(resource)
2497
3034
 
2498
3035
  if (!parsedResource.success) {
2499
- console.error('Error parsing resource', resource)
3036
+ logger.error(parsedResource.error, {
3037
+ event: 'adapter.contentResource.parse_failed',
3038
+ resourceId: resource?.id,
3039
+ resourceIdOrSlug: data,
3040
+ })
2500
3041
  return null
2501
3042
  }
2502
3043
 
2503
3044
  return parsedResource.data
2504
3045
  },
2505
- async createUser(data) {
2506
- try {
2507
- const id = crypto.randomUUID()
3046
+ async getEvent(
3047
+ eventIdOrSlug: string,
3048
+ options?: {
3049
+ withResources?: boolean
3050
+ withTags?: boolean
3051
+ withProducts?: boolean
3052
+ withPricing?: boolean
3053
+ },
3054
+ ): Promise<ContentResource | null> {
3055
+ const {
3056
+ withResources = false,
3057
+ withTags = false,
3058
+ withProducts = false,
3059
+ withPricing = false,
3060
+ } = options || {}
2508
3061
 
2509
- await client.insert(users).values({ ...data, id })
3062
+ const eventQuery = await client.query.contentResource.findFirst({
3063
+ where: and(
3064
+ or(
3065
+ eq(contentResource.type, 'event'),
3066
+ eq(contentResource.type, 'event-series'),
3067
+ ),
3068
+ or(
3069
+ eq(contentResource.id, eventIdOrSlug),
3070
+ eq(
3071
+ sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
3072
+ eventIdOrSlug,
3073
+ ),
3074
+ ),
3075
+ ),
3076
+ with: {
3077
+ ...(withResources && {
3078
+ resources: {
3079
+ with: { resource: true },
3080
+ orderBy: asc(contentResourceResource.position),
3081
+ },
3082
+ }),
3083
+ ...(withTags && {
3084
+ tags: {
3085
+ with: { tag: true },
3086
+ orderBy: asc(contentResourceTag.position),
3087
+ },
3088
+ }),
3089
+ ...(withProducts && {
3090
+ resourceProducts: {
3091
+ with: {
3092
+ product: {
3093
+ ...(withPricing && {
3094
+ with: { price: true },
3095
+ }),
3096
+ },
3097
+ },
3098
+ },
3099
+ }),
3100
+ },
3101
+ })
2510
3102
 
2511
- // creating a personal organization for the user
2512
- // every user gets an organization of their very own
2513
- const personalOrganization = await adapter.createOrganization({
2514
- name: `Personal (${data.email})`,
3103
+ const parsedEvent = ContentResourceSchema.safeParse(eventQuery)
3104
+ if (!parsedEvent.success) {
3105
+ logger.debug('Error parsing event', {
3106
+ eventQuery,
3107
+ error: parsedEvent.error,
2515
3108
  })
3109
+ return null
3110
+ }
2516
3111
 
2517
- if (!personalOrganization) {
2518
- throw new Error('Failed to create personal organization')
2519
- }
3112
+ return parsedEvent.data
3113
+ },
3114
+ async addResourceToProduct(options: {
3115
+ resource: ContentResource
3116
+ productId: string
3117
+ userId: string
3118
+ }) {
3119
+ const { resource, productId, userId } = options
3120
+ const product = await adapter.getProduct(productId)
2520
3121
 
2521
- const membership = await adapter.addMemberToOrganization({
2522
- organizationId: personalOrganization.id,
2523
- userId: id,
2524
- invitedById: id,
3122
+ if (!product) {
3123
+ throw new Error(`Product not found for id (${productId})`)
3124
+ }
3125
+
3126
+ await client.insert(contentResourceProduct).values({
3127
+ resourceId: resource.id,
3128
+ productId,
3129
+ position: product.resources?.length || 0,
3130
+ metadata: {
3131
+ addedBy: userId,
3132
+ },
3133
+ })
3134
+
3135
+ const resourceProduct =
3136
+ await client.query.contentResourceProduct.findFirst({
3137
+ where: eq(contentResourceProduct.resourceId, resource.id),
3138
+ with: {
3139
+ resource: true,
3140
+ product: true,
3141
+ },
3142
+ })
3143
+
3144
+ return (
3145
+ ContentResourceProductSchema.safeParse(resourceProduct).data || null
3146
+ )
3147
+ },
3148
+ async createEvent(
3149
+ input: {
3150
+ type: 'event'
3151
+ fields: {
3152
+ title: string
3153
+ startsAt?: Date | null | undefined
3154
+ endsAt?: Date | null | undefined
3155
+ description?: string | null | undefined
3156
+ price?: number | null | undefined
3157
+ quantity?: number | null | undefined
3158
+ state?: string | null | undefined
3159
+ visibility?: string | null | undefined
3160
+ slug?: string | null | undefined
3161
+ tagIds?:
3162
+ | { id: string; fields: Record<string, any> }[]
3163
+ | null
3164
+ | undefined
3165
+ }
3166
+ coupon?: {
3167
+ enabled: boolean
3168
+ percentageDiscount: string
3169
+ expires: Date
3170
+ }
3171
+ },
3172
+ userId: string,
3173
+ ) {
3174
+ const hash = guid()
3175
+ const newEventId = slugify(`${input.type}~${hash}`)
3176
+
3177
+ const newEvent = {
3178
+ id: newEventId,
3179
+ type: 'event',
3180
+ fields: {
3181
+ title: input.fields.title,
3182
+ state: 'draft',
3183
+ visibility: 'public',
3184
+ slug: slugify(`${input.fields.title}~${hash}`),
3185
+ description: input.fields.description,
3186
+ startsAt: input.fields.startsAt,
3187
+ endsAt: input.fields.endsAt,
3188
+ },
3189
+ createdById: userId,
3190
+ }
3191
+
3192
+ await client.insert(contentResource).values(newEvent)
3193
+ const event = await adapter.getEvent(newEventId, {
3194
+ withResources: true,
3195
+ withTags: true,
3196
+ withProducts: true,
3197
+ withPricing: true,
3198
+ })
3199
+
3200
+ const parsedEvent = ContentResourceSchema.safeParse(event)
3201
+
3202
+ if (!parsedEvent.success) {
3203
+ logger.error(new Error('Error parsing event resource'))
3204
+ throw new Error('Error parsing event resource')
3205
+ }
3206
+
3207
+ // if price is provided, create a product and associate it with the event
3208
+ if (input.fields.price && input.fields.price > 0) {
3209
+ try {
3210
+ const product = await adapter.createProduct({
3211
+ name: input.fields.title,
3212
+ price: input.fields.price,
3213
+ quantityAvailable: input.fields.quantity ?? -1,
3214
+ type: 'live',
3215
+ state: 'published',
3216
+ visibility: 'public',
3217
+ })
3218
+ if (product) {
3219
+ const resourceProduct = await adapter.addResourceToProduct({
3220
+ resource: parsedEvent.data,
3221
+ productId: product.id,
3222
+ userId,
3223
+ })
3224
+ if (!resourceProduct) {
3225
+ logger.error(new Error('Failed to add resource to product'))
3226
+ logger.debug('event.addResourceToProduct.failed', {
3227
+ eventId: newEventId,
3228
+ productId: product.id,
3229
+ userId,
3230
+ })
3231
+ } else {
3232
+ logger.debug('event.addResourceToProduct.success', {
3233
+ eventId: newEventId,
3234
+ productId: product.id,
3235
+ userId,
3236
+ })
3237
+ }
3238
+ // Create coupon if enabled
3239
+ if (
3240
+ input.coupon?.enabled &&
3241
+ input.coupon.percentageDiscount &&
3242
+ input.coupon.expires
3243
+ ) {
3244
+ try {
3245
+ const finalExpires = normalizeExpirationDate(
3246
+ input.coupon.expires,
3247
+ )
3248
+ const couponInput = {
3249
+ quantity: '1',
3250
+ maxUses: -1,
3251
+ expires: finalExpires || null,
3252
+ restrictedToProductId: product.id,
3253
+ percentageDiscount: input.coupon.percentageDiscount,
3254
+ status: 1,
3255
+ default: true,
3256
+ fields: {
3257
+ bypassSoldOut: false,
3258
+ },
3259
+ }
3260
+ await adapter.createCoupon(couponInput)
3261
+ logger.debug('event.create.coupon.success', {
3262
+ eventId: newEventId,
3263
+ productId: product.id,
3264
+ userId,
3265
+ percentageDiscount: input.coupon.percentageDiscount,
3266
+ })
3267
+ } catch (couponError) {
3268
+ logger.error(
3269
+ new Error(
3270
+ `Failed to create coupon for event: ${couponError}`,
3271
+ ),
3272
+ )
3273
+ logger.debug('event.create.coupon.failed', {
3274
+ eventId: newEventId,
3275
+ productId: product.id,
3276
+ userId,
3277
+ })
3278
+ // Don't throw here - event creation should succeed even if coupon fails
3279
+ }
3280
+ }
3281
+ } else {
3282
+ logger.error(new Error('Failed to create product'))
3283
+ logger.debug('event.create.product.failed', {
3284
+ eventId: newEventId,
3285
+ userId,
3286
+ price: input.fields.price,
3287
+ })
3288
+ }
3289
+ } catch (error) {
3290
+ logger.error(
3291
+ new Error(`Error creating and associating product: ${error}`),
3292
+ )
3293
+ logger.debug('event.create.product.failed', {
3294
+ eventId: newEventId,
3295
+ userId,
3296
+ price: input.fields.price,
3297
+ })
3298
+ }
3299
+ }
3300
+
3301
+ // if we provide tagIds, we need to associate them with the event
3302
+ if (input.fields.tagIds) {
3303
+ try {
3304
+ await client.insert(contentResourceTag).values(
3305
+ input.fields.tagIds.map((tag) => ({
3306
+ contentResourceId: newEventId,
3307
+ tagId: tag.id,
3308
+ createdAt: new Date(),
3309
+ updatedAt: new Date(),
3310
+ position: 0,
3311
+ })),
3312
+ )
3313
+ logger.debug('event.create.tags.success', {
3314
+ eventId: newEventId,
3315
+ userId,
3316
+ tagIds: input.fields.tagIds,
3317
+ })
3318
+ } catch (error) {
3319
+ logger.error(new Error(`Error associating tags with event: ${error}`))
3320
+ logger.debug('event.create.tags.failed', {
3321
+ eventId: newEventId,
3322
+ userId,
3323
+ tagIds: input.fields.tagIds,
3324
+ })
3325
+ }
3326
+ }
3327
+
3328
+ return parsedEvent.data
3329
+ },
3330
+ async createEventSeries(
3331
+ input: {
3332
+ eventSeries: {
3333
+ type: 'event-series'
3334
+ fields: {
3335
+ title: string
3336
+ description?: string | undefined
3337
+ tagIds?:
3338
+ | { id: string; fields: { label: string; name: string } }[]
3339
+ | null
3340
+ | undefined
3341
+ }
3342
+ sharedFields: {
3343
+ price: number | null | undefined
3344
+ quantity: number | null | undefined
3345
+ }
3346
+ }
3347
+ childEvents: Array<{
3348
+ type: 'event'
3349
+ fields: {
3350
+ title: string
3351
+ startsAt: Date | null | undefined
3352
+ endsAt: Date | null | undefined
3353
+ description?: string | undefined
3354
+ tagIds?:
3355
+ | { id: string; fields: { label: string; name: string } }[]
3356
+ | null
3357
+ | undefined
3358
+ }
3359
+ }>
3360
+ coupon?:
3361
+ | {
3362
+ enabled: boolean
3363
+ expires?: Date | undefined
3364
+ percentageDiscount?: string | undefined
3365
+ }
3366
+ | undefined
3367
+ },
3368
+ userId: string,
3369
+ ) {
3370
+ const { eventSeries: eventSeriesInput, childEvents: childEventsInput } =
3371
+ input
3372
+
3373
+ if (childEventsInput.length === 0) {
3374
+ throw new Error('At least one event is required')
3375
+ }
3376
+
3377
+ // Execute all database operations within a transaction
3378
+ const result = await client.transaction(async (tx) => {
3379
+ // Step 1: Create the event series resource
3380
+ const eventSeriesHash = guid()
3381
+ const eventSeriesResourceId = slugify(`event-series~${eventSeriesHash}`)
3382
+
3383
+ const newEventSeries = {
3384
+ id: eventSeriesResourceId,
3385
+ type: 'event-series',
3386
+ fields: {
3387
+ ...eventSeriesInput.fields,
3388
+ title: eventSeriesInput.fields.title,
3389
+ state: 'draft',
3390
+ visibility: 'public',
3391
+ slug: slugify(
3392
+ `${eventSeriesInput.fields.title}~${eventSeriesHash}`,
3393
+ ),
3394
+ },
3395
+ createdById: userId,
3396
+ }
3397
+
3398
+ await tx.insert(contentResource).values(newEventSeries)
3399
+
3400
+ // Fetch the created event series with relations
3401
+ const eventSeriesResource = await tx.query.contentResource.findFirst({
3402
+ where: eq(contentResource.id, eventSeriesResourceId),
3403
+ with: {
3404
+ resources: {
3405
+ with: {
3406
+ resource: {
3407
+ with: {
3408
+ resources: {
3409
+ with: {
3410
+ resource: true,
3411
+ },
3412
+ orderBy: asc(contentResourceResource.position),
3413
+ },
3414
+ },
3415
+ },
3416
+ },
3417
+ orderBy: asc(contentResourceResource.position),
3418
+ },
3419
+ tags: {
3420
+ with: {
3421
+ tag: true,
3422
+ },
3423
+ orderBy: asc(contentResourceTag.position),
3424
+ },
3425
+ resourceProducts: {
3426
+ with: {
3427
+ product: {
3428
+ with: {
3429
+ price: true,
3430
+ },
3431
+ },
3432
+ },
3433
+ },
3434
+ },
3435
+ })
3436
+
3437
+ const parsedEventSeries =
3438
+ ContentResourceSchema.safeParse(eventSeriesResource)
3439
+ if (!parsedEventSeries.success) {
3440
+ throw new Error('Error parsing event series resource')
3441
+ }
3442
+
3443
+ // Step 2: Associate tags with event series
3444
+ if (eventSeriesInput.fields.tagIds) {
3445
+ await tx.insert(contentResourceTag).values(
3446
+ eventSeriesInput.fields.tagIds.map((tag) => ({
3447
+ contentResourceId: eventSeriesResourceId,
3448
+ tagId: tag.id,
3449
+ createdAt: new Date(),
3450
+ updatedAt: new Date(),
3451
+ position: 0,
3452
+ })),
3453
+ )
3454
+ }
3455
+
3456
+ // Step 3: Create child events and associate them
3457
+ const createdChildEvents = []
3458
+
3459
+ for (let i = 0; i < childEventsInput.length; i++) {
3460
+ const childEventInput = childEventsInput[i]
3461
+
3462
+ if (!childEventInput) {
3463
+ throw new Error(`Child event input is required`)
3464
+ }
3465
+
3466
+ // Create child event
3467
+ const childEventHash = guid()
3468
+ const childEventResourceId = slugify(`event~${childEventHash}`)
3469
+
3470
+ const newChildEvent = {
3471
+ id: childEventResourceId,
3472
+ type: 'event',
3473
+ fields: {
3474
+ title: childEventInput.fields.title,
3475
+ startsAt: childEventInput.fields.startsAt,
3476
+ endsAt: childEventInput.fields.endsAt,
3477
+ description: childEventInput.fields.description,
3478
+ state: 'draft',
3479
+ visibility: 'public',
3480
+ slug: slugify(
3481
+ `${childEventInput.fields.title}~${childEventHash}`,
3482
+ ),
3483
+ // No price/quantity - they're on the event series
3484
+ price: null,
3485
+ quantity: null,
3486
+ },
3487
+ createdById: userId,
3488
+ }
3489
+
3490
+ await tx.insert(contentResource).values(newChildEvent)
3491
+
3492
+ // Fetch the created child event with relations
3493
+ const childEventResource = await tx.query.contentResource.findFirst({
3494
+ where: eq(contentResource.id, childEventResourceId),
3495
+ with: {
3496
+ resources: {
3497
+ with: {
3498
+ resource: {
3499
+ with: {
3500
+ resources: {
3501
+ with: {
3502
+ resource: true,
3503
+ },
3504
+ orderBy: asc(contentResourceResource.position),
3505
+ },
3506
+ },
3507
+ },
3508
+ },
3509
+ orderBy: asc(contentResourceResource.position),
3510
+ },
3511
+ tags: {
3512
+ with: {
3513
+ tag: true,
3514
+ },
3515
+ orderBy: asc(contentResourceTag.position),
3516
+ },
3517
+ resourceProducts: {
3518
+ with: {
3519
+ product: {
3520
+ with: {
3521
+ price: true,
3522
+ },
3523
+ },
3524
+ },
3525
+ },
3526
+ },
3527
+ })
3528
+
3529
+ const parsedChildEvent =
3530
+ ContentResourceSchema.safeParse(childEventResource)
3531
+ if (!parsedChildEvent.success) {
3532
+ throw new Error(`Error parsing child event resource ${i + 1}`)
3533
+ }
3534
+
3535
+ // Associate tags with child event
3536
+ if (childEventInput.fields.tagIds) {
3537
+ await tx.insert(contentResourceTag).values(
3538
+ childEventInput.fields.tagIds.map((tag) => ({
3539
+ contentResourceId: childEventResourceId,
3540
+ tagId: tag.id,
3541
+ createdAt: new Date(),
3542
+ updatedAt: new Date(),
3543
+ position: 0,
3544
+ })),
3545
+ )
3546
+ }
3547
+
3548
+ // Associate child event with event series
3549
+ await tx.insert(contentResourceResource).values({
3550
+ resourceOfId: eventSeriesResourceId,
3551
+ resourceId: childEventResourceId,
3552
+ position: i,
3553
+ })
3554
+
3555
+ createdChildEvents.push(parsedChildEvent.data)
3556
+ }
3557
+
3558
+ return {
3559
+ eventSeries: parsedEventSeries.data,
3560
+ childEvents: createdChildEvents,
3561
+ }
3562
+ })
3563
+
3564
+ const { eventSeries, childEvents } = result
3565
+
3566
+ // Step 4: Handle product creation outside transaction
3567
+ if (
3568
+ eventSeriesInput.sharedFields.price &&
3569
+ eventSeriesInput.sharedFields.price > 0
3570
+ ) {
3571
+ try {
3572
+ const product = await adapter.createProduct({
3573
+ name: eventSeriesInput.fields.title,
3574
+ price: eventSeriesInput.sharedFields.price,
3575
+ quantityAvailable: eventSeriesInput.sharedFields.quantity ?? -1,
3576
+ type: 'live',
3577
+ state: 'published',
3578
+ visibility: 'public',
3579
+ })
3580
+ if (product) {
3581
+ await adapter.addResourceToProduct({
3582
+ resource: eventSeries,
3583
+ productId: product.id,
3584
+ userId,
3585
+ })
3586
+
3587
+ // Create coupon if enabled
3588
+ if (
3589
+ input.coupon?.enabled &&
3590
+ input.coupon.percentageDiscount &&
3591
+ input.coupon.expires
3592
+ ) {
3593
+ try {
3594
+ let finalExpires = input.coupon.expires
3595
+ if (finalExpires instanceof Date) {
3596
+ finalExpires = new Date(
3597
+ Date.UTC(
3598
+ finalExpires.getUTCFullYear(),
3599
+ finalExpires.getUTCMonth(),
3600
+ finalExpires.getUTCDate(),
3601
+ 23,
3602
+ 59,
3603
+ 59,
3604
+ 0,
3605
+ ),
3606
+ )
3607
+ }
3608
+ const couponInput = {
3609
+ quantity: '1',
3610
+ maxUses: 1,
3611
+ expires: finalExpires,
3612
+ restrictedToProductId: product.id,
3613
+ percentageDiscount: input.coupon.percentageDiscount,
3614
+ status: 1,
3615
+ default: true,
3616
+ fields: {},
3617
+ }
3618
+ await adapter.createCoupon(couponInput)
3619
+ logger.debug('event.series.coupon.success', {
3620
+ eventSeriesId: eventSeries.id,
3621
+ productId: product.id,
3622
+ userId,
3623
+ percentageDiscount: input.coupon.percentageDiscount,
3624
+ })
3625
+ } catch (couponError) {
3626
+ logger.error(
3627
+ new Error(
3628
+ `Error creating coupon for event series: ${couponError}`,
3629
+ ),
3630
+ )
3631
+ logger.debug('event.series.coupon.failed', {
3632
+ eventSeriesId: eventSeries.id,
3633
+ productId: product.id,
3634
+ userId,
3635
+ })
3636
+ // Don't throw here - event series creation should succeed even if coupon fails
3637
+ }
3638
+ }
3639
+ } else {
3640
+ logger.error(new Error('Failed to create product'))
3641
+ logger.debug('event.series.product.failed', {
3642
+ eventSeriesId: eventSeries.id,
3643
+ userId,
3644
+ price: eventSeriesInput.sharedFields.price,
3645
+ })
3646
+ }
3647
+ } catch (error) {
3648
+ logger.error(
3649
+ new Error(`Error creating and associating product: ${error}`),
3650
+ )
3651
+ logger.debug('event.series.product.creation.failed', {
3652
+ eventSeriesId: eventSeries.id,
3653
+ userId,
3654
+ })
3655
+ // Note: We don't throw here since the core data is already committed
3656
+ }
3657
+ }
3658
+
3659
+ return { eventSeries, childEvents }
3660
+ },
3661
+ async createCohort(
3662
+ input: {
3663
+ cohort: {
3664
+ title: string
3665
+ description?: string
3666
+ tagIds?:
3667
+ | { id: string; fields: { label: string; name: string } }[]
3668
+ | null
3669
+ }
3670
+ dates: {
3671
+ start: Date
3672
+ end: Date
3673
+ }
3674
+ createProduct?: boolean
3675
+ pricing: {
3676
+ price?: number | null
3677
+ }
3678
+ coupon?: {
3679
+ enabled: boolean
3680
+ percentageDiscount?: string
3681
+ expires?: Date
3682
+ }
3683
+ workshops: { id: string }[]
3684
+ },
3685
+ userId: string,
3686
+ ) {
3687
+ const hash = guid()
3688
+ const cohortId = `cohort~${hash}`
3689
+
3690
+ // Transaction only handles database operations
3691
+ const cohort = await client.transaction(async (tx) => {
3692
+ // Create cohort content resource
3693
+ await tx.insert(contentResource).values({
3694
+ id: cohortId,
3695
+ type: 'cohort',
3696
+ createdById: userId,
3697
+ fields: {
3698
+ title: input.cohort.title,
3699
+ description: input.cohort.description,
3700
+ state: 'draft',
3701
+ visibility: 'unlisted',
3702
+ slug: slugify(`${input.cohort.title}~${hash}`),
3703
+ startsAt: input.dates.start,
3704
+ endsAt: input.dates.end,
3705
+ },
3706
+ })
3707
+
3708
+ // Fetch created cohort
3709
+ const cohort = await tx.query.contentResource.findFirst({
3710
+ where: eq(contentResource.id, cohortId),
3711
+ with: {
3712
+ resources: {
3713
+ with: {
3714
+ resource: true,
3715
+ },
3716
+ orderBy: asc(contentResourceResource.position),
3717
+ },
3718
+ },
3719
+ })
3720
+
3721
+ if (!cohort) {
3722
+ throw new Error('Failed to create cohort')
3723
+ }
3724
+
3725
+ const parsedCohort = ContentResourceSchema.safeParse(cohort)
3726
+ if (!parsedCohort.success) {
3727
+ throw new Error('Invalid cohort data')
3728
+ }
3729
+
3730
+ // Associate tags with cohort
3731
+ if (input.cohort.tagIds) {
3732
+ await tx.insert(contentResourceTag).values(
3733
+ input.cohort.tagIds.map((tag) => ({
3734
+ contentResourceId: cohortId,
3735
+ tagId: tag.id,
3736
+ createdAt: new Date(),
3737
+ updatedAt: new Date(),
3738
+ position: 0,
3739
+ })),
3740
+ )
3741
+ }
3742
+
3743
+ // Link workshops to cohort
3744
+ let position = 0
3745
+ for (const workshop of input.workshops) {
3746
+ await tx.insert(contentResourceResource).values({
3747
+ resourceOfId: cohortId,
3748
+ resourceId: workshop.id,
3749
+ position,
3750
+ })
3751
+ position++
3752
+ }
3753
+
3754
+ return parsedCohort.data
3755
+ })
3756
+
3757
+ // Handle product and coupon creation outside transaction
3758
+ let product: any = null
3759
+ if (
3760
+ input.createProduct &&
3761
+ input.pricing.price &&
3762
+ input.pricing.price > 0
3763
+ ) {
3764
+ try {
3765
+ product = await adapter.createProduct({
3766
+ name: input.cohort.title,
3767
+ price: input.pricing.price,
3768
+ quantityAvailable: -1,
3769
+ type: 'cohort',
3770
+ state: 'published',
3771
+ visibility: 'public',
3772
+ openEnrollment: new Date().toISOString(),
3773
+ closeEnrollment: input.dates.start.toISOString(),
3774
+ })
3775
+
3776
+ if (product) {
3777
+ // Link product to cohort resource
3778
+ await client.insert(contentResourceProduct).values({
3779
+ resourceId: cohortId,
3780
+ productId: product.id,
3781
+ position: 0,
3782
+ metadata: {
3783
+ addedBy: userId,
3784
+ },
3785
+ })
3786
+
3787
+ // Create coupon if enabled
3788
+ if (
3789
+ input.coupon?.enabled &&
3790
+ input.coupon.percentageDiscount &&
3791
+ input.coupon.expires
3792
+ ) {
3793
+ try {
3794
+ const finalExpires = normalizeExpirationDate(
3795
+ input.coupon.expires,
3796
+ )
3797
+ await adapter.createCoupon({
3798
+ percentageDiscount: input.coupon.percentageDiscount,
3799
+ expires: finalExpires || null,
3800
+ restrictedToProductId: product.id,
3801
+ default: true,
3802
+ maxUses: -1,
3803
+ quantity: '-1',
3804
+ status: 1,
3805
+ fields: {},
3806
+ })
3807
+ logger.debug('cohort.create.coupon.success', {
3808
+ cohortId,
3809
+ productId: product.id,
3810
+ userId,
3811
+ percentageDiscount: input.coupon.percentageDiscount,
3812
+ })
3813
+ } catch (couponError) {
3814
+ logger.error(
3815
+ new Error(
3816
+ `Failed to create coupon for cohort: ${couponError}`,
3817
+ ),
3818
+ )
3819
+ logger.debug('cohort.create.coupon.failed', {
3820
+ cohortId,
3821
+ productId: product.id,
3822
+ userId,
3823
+ })
3824
+ // Don't throw - cohort creation should succeed even if coupon fails
3825
+ }
3826
+ }
3827
+ }
3828
+ } catch (productError) {
3829
+ logger.error(
3830
+ new Error(`Failed to create product for cohort: ${productError}`),
3831
+ )
3832
+ logger.debug('cohort.create.product.failed', {
3833
+ cohortId,
3834
+ userId,
3835
+ price: input.pricing.price,
3836
+ })
3837
+ // Don't throw - cohort creation should succeed even if product fails
3838
+ }
3839
+ }
3840
+
3841
+ return {
3842
+ cohort,
3843
+ product,
3844
+ }
3845
+ },
3846
+ async createWorkshop(
3847
+ input: {
3848
+ workshop: {
3849
+ title: string
3850
+ description?: string
3851
+ tagIds?:
3852
+ | { id: string; fields: { label: string; name: string } }[]
3853
+ | null
3854
+ }
3855
+ createProduct?: boolean
3856
+ pricing: {
3857
+ price?: number | null
3858
+ quantity?: number | null
3859
+ }
3860
+ coupon?: {
3861
+ enabled: boolean
3862
+ percentageDiscount?: string
3863
+ expires?: Date
3864
+ }
3865
+ structure: Array<
3866
+ | {
3867
+ type: 'section'
3868
+ title: string
3869
+ lessons: { title: string; videoResourceId?: string }[]
3870
+ }
3871
+ | { type: 'lesson'; title: string; videoResourceId?: string }
3872
+ >
3873
+ },
3874
+ userId: string,
3875
+ ) {
3876
+ const hash = guid()
3877
+ const workshopId = `workshop~${hash}`
3878
+
3879
+ // Transaction only handles database operations
3880
+ const result = await client.transaction(async (tx) => {
3881
+ // Create workshop content resource
3882
+ await tx.insert(contentResource).values({
3883
+ id: workshopId,
3884
+ type: 'workshop',
3885
+ createdById: userId,
3886
+ fields: {
3887
+ title: input.workshop.title,
3888
+ description: input.workshop.description,
3889
+ state: 'draft',
3890
+ visibility: 'unlisted',
3891
+ slug: slugify(`${input.workshop.title}~${hash}`),
3892
+ },
3893
+ })
3894
+
3895
+ // Fetch created workshop
3896
+ const workshop = await tx.query.contentResource.findFirst({
3897
+ where: eq(contentResource.id, workshopId),
3898
+ with: {
3899
+ resources: {
3900
+ with: {
3901
+ resource: {
3902
+ with: {
3903
+ resources: {
3904
+ with: {
3905
+ resource: true,
3906
+ },
3907
+ orderBy: asc(contentResourceResource.position),
3908
+ },
3909
+ },
3910
+ },
3911
+ },
3912
+ orderBy: asc(contentResourceResource.position),
3913
+ },
3914
+ },
3915
+ })
3916
+
3917
+ if (!workshop) {
3918
+ throw new Error('Failed to create workshop')
3919
+ }
3920
+
3921
+ const parsedWorkshop = ContentResourceSchema.safeParse(workshop)
3922
+ if (!parsedWorkshop.success) {
3923
+ throw new Error('Invalid workshop data')
3924
+ }
3925
+
3926
+ // Associate tags with workshop
3927
+ if (input.workshop.tagIds) {
3928
+ await tx.insert(contentResourceTag).values(
3929
+ input.workshop.tagIds.map((tag) => ({
3930
+ contentResourceId: workshopId,
3931
+ tagId: tag.id,
3932
+ createdAt: new Date(),
3933
+ updatedAt: new Date(),
3934
+ position: 0,
3935
+ })),
3936
+ )
3937
+ }
3938
+
3939
+ // Create sections and lessons
3940
+ const createdSections: ContentResource[] = []
3941
+ const createdLessons: ContentResource[] = []
3942
+ let position = 0
3943
+
3944
+ for (const item of input.structure) {
3945
+ if (item.type === 'section') {
3946
+ // Create section
3947
+ const sectionHash = guid()
3948
+ const sectionId = `section~${sectionHash}`
3949
+
3950
+ await tx.insert(contentResource).values({
3951
+ id: sectionId,
3952
+ type: 'section',
3953
+ createdById: userId,
3954
+ fields: {
3955
+ title: item.title,
3956
+ state: 'draft',
3957
+ visibility: 'unlisted',
3958
+ slug: slugify(`${item.title}~${sectionHash}`),
3959
+ },
3960
+ })
3961
+
3962
+ // Link section to workshop
3963
+ await tx.insert(contentResourceResource).values({
3964
+ resourceOfId: workshopId,
3965
+ resourceId: sectionId,
3966
+ position,
3967
+ })
3968
+
3969
+ const section = await tx.query.contentResource.findFirst({
3970
+ where: eq(contentResource.id, sectionId),
3971
+ })
3972
+
3973
+ if (section) {
3974
+ createdSections.push(section as ContentResource)
3975
+ }
3976
+
3977
+ // Create lessons in section
3978
+ let lessonPosition = 0
3979
+ for (const lessonData of item.lessons) {
3980
+ const lessonHash = guid()
3981
+ const lessonId = `lesson_${lessonHash}`
3982
+
3983
+ await tx.insert(contentResource).values({
3984
+ id: lessonId,
3985
+ type: 'lesson',
3986
+ createdById: userId,
3987
+ fields: {
3988
+ title: lessonData.title,
3989
+ state: 'draft',
3990
+ visibility: 'unlisted',
3991
+ slug: slugify(`${lessonData.title}~${lessonHash}`),
3992
+ lessonType: 'lesson',
3993
+ },
3994
+ })
3995
+
3996
+ // Link video if provided
3997
+ if (lessonData.videoResourceId) {
3998
+ await tx.insert(contentResourceResource).values({
3999
+ resourceOfId: lessonId,
4000
+ resourceId: lessonData.videoResourceId,
4001
+ position: 0,
4002
+ })
4003
+ }
4004
+
4005
+ // Link lesson to section
4006
+ await tx.insert(contentResourceResource).values({
4007
+ resourceOfId: sectionId,
4008
+ resourceId: lessonId,
4009
+ position: lessonPosition,
4010
+ })
4011
+
4012
+ const lesson = await tx.query.contentResource.findFirst({
4013
+ where: eq(contentResource.id, lessonId),
4014
+ })
4015
+
4016
+ if (lesson) {
4017
+ createdLessons.push(lesson as ContentResource)
4018
+ }
4019
+
4020
+ lessonPosition++
4021
+ }
4022
+
4023
+ position++
4024
+ } else {
4025
+ // Create top-level lesson
4026
+ const lessonHash = guid()
4027
+ const lessonId = `lesson_${lessonHash}`
4028
+
4029
+ await tx.insert(contentResource).values({
4030
+ id: lessonId,
4031
+ type: 'lesson',
4032
+ createdById: userId,
4033
+ fields: {
4034
+ title: item.title,
4035
+ state: 'draft',
4036
+ visibility: 'unlisted',
4037
+ slug: slugify(`${item.title}~${lessonHash}`),
4038
+ lessonType: 'lesson',
4039
+ },
4040
+ })
4041
+
4042
+ // Link video if provided
4043
+ if (item.videoResourceId) {
4044
+ await tx.insert(contentResourceResource).values({
4045
+ resourceOfId: lessonId,
4046
+ resourceId: item.videoResourceId,
4047
+ position: 0,
4048
+ })
4049
+ }
4050
+
4051
+ // Link lesson to workshop
4052
+ await tx.insert(contentResourceResource).values({
4053
+ resourceOfId: workshopId,
4054
+ resourceId: lessonId,
4055
+ position,
4056
+ })
4057
+
4058
+ const lesson = await tx.query.contentResource.findFirst({
4059
+ where: eq(contentResource.id, lessonId),
4060
+ })
4061
+
4062
+ if (lesson) {
4063
+ createdLessons.push(lesson as ContentResource)
4064
+ }
4065
+
4066
+ position++
4067
+ }
4068
+ }
4069
+
4070
+ return {
4071
+ workshop: parsedWorkshop.data,
4072
+ sections: createdSections,
4073
+ lessons: createdLessons,
4074
+ }
4075
+ })
4076
+
4077
+ // Handle product and coupon creation outside transaction
4078
+ let product: any = null
4079
+ if (
4080
+ input.createProduct &&
4081
+ input.pricing.price &&
4082
+ input.pricing.price > 0
4083
+ ) {
4084
+ try {
4085
+ product = await adapter.createProduct({
4086
+ name: input.workshop.title,
4087
+ price: input.pricing.price,
4088
+ quantityAvailable: input.pricing.quantity ?? -1,
4089
+ type: 'self-paced',
4090
+ state: 'published',
4091
+ visibility: 'public',
4092
+ })
4093
+
4094
+ if (product) {
4095
+ // Link product to workshop resource
4096
+ await client.insert(contentResourceProduct).values({
4097
+ resourceId: workshopId,
4098
+ productId: product.id,
4099
+ position: 0,
4100
+ metadata: {
4101
+ addedBy: userId,
4102
+ },
4103
+ })
4104
+
4105
+ // Create coupon if enabled
4106
+ if (
4107
+ input.coupon?.enabled &&
4108
+ input.coupon.percentageDiscount &&
4109
+ input.coupon.expires
4110
+ ) {
4111
+ try {
4112
+ const finalExpires = normalizeExpirationDate(
4113
+ input.coupon.expires,
4114
+ )
4115
+ await adapter.createCoupon({
4116
+ percentageDiscount: input.coupon.percentageDiscount,
4117
+ expires: finalExpires || null,
4118
+ restrictedToProductId: product.id,
4119
+ default: true,
4120
+ maxUses: -1,
4121
+ quantity: '-1',
4122
+ status: 1,
4123
+ fields: {},
4124
+ })
4125
+ logger.debug('workshop.create.coupon.success', {
4126
+ workshopId,
4127
+ productId: product.id,
4128
+ userId,
4129
+ percentageDiscount: input.coupon.percentageDiscount,
4130
+ })
4131
+ } catch (couponError) {
4132
+ logger.error(
4133
+ new Error(
4134
+ `Failed to create coupon for workshop: ${couponError}`,
4135
+ ),
4136
+ )
4137
+ logger.debug('workshop.create.coupon.failed', {
4138
+ workshopId,
4139
+ productId: product.id,
4140
+ userId,
4141
+ })
4142
+ // Don't throw - workshop creation should succeed even if coupon fails
4143
+ }
4144
+ }
4145
+ }
4146
+ } catch (productError) {
4147
+ logger.error(
4148
+ new Error(`Failed to create product for workshop: ${productError}`),
4149
+ )
4150
+ logger.debug('workshop.create.product.failed', {
4151
+ workshopId,
4152
+ userId,
4153
+ price: input.pricing.price,
4154
+ })
4155
+ // Don't throw - workshop creation should succeed even if product fails
4156
+ }
4157
+ }
4158
+
4159
+ return {
4160
+ ...result,
4161
+ product,
4162
+ }
4163
+ },
4164
+ async createCoupon(input: {
4165
+ quantity: string
4166
+ maxUses: number
4167
+ expires: Date | null
4168
+ restrictedToProductId: string | null
4169
+ percentageDiscount: string
4170
+ status: number
4171
+ default: boolean
4172
+ fields: Record<string, any>
4173
+ }) {
4174
+ function findClosestDiscount(percentOff: number) {
4175
+ // we want a fraction so if it is whole number, we make it fractional
4176
+ percentOff = percentOff <= 1 ? percentOff : percentOff / 100
4177
+ return [1, 0.95, 0.9, 0.75, 0.6, 0.5, 0.4, 0.25, 0.1].reduce((a, b) => {
4178
+ let aDiff = Math.abs(a - percentOff)
4179
+ let bDiff = Math.abs(b - percentOff)
4180
+
4181
+ if (aDiff === bDiff) {
4182
+ // Choose largest vs smallest (> vs <)
4183
+ return a > b ? a : b
4184
+ } else {
4185
+ return bDiff < aDiff ? b : a
4186
+ }
4187
+ })
4188
+ }
4189
+
4190
+ const percentageDiscount = findClosestDiscount(
4191
+ Number(input.percentageDiscount) * 100,
4192
+ )
4193
+
4194
+ const merchantCouponResult =
4195
+ percentageDiscount < 1
4196
+ ? await client.query.merchantCoupon.findFirst({
4197
+ where: and(
4198
+ eq(
4199
+ merchantCoupon.percentageDiscount,
4200
+ percentageDiscount.toString(),
4201
+ ),
4202
+ eq(merchantCoupon.type, 'special'),
4203
+ ),
4204
+ })
4205
+ : null
4206
+
4207
+ if (!merchantCouponResult) {
4208
+ return []
4209
+ }
4210
+
4211
+ const codesArray: string[] = []
4212
+ await client.transaction(async (trx) => {
4213
+ // insert coupon for CouponInput quantity
4214
+ for (let i = 0; i < Number(input.quantity); i++) {
4215
+ const id = `coupon_${guid()}`
4216
+ await trx.insert(coupon).values({
4217
+ ...input,
4218
+ merchantCouponId: merchantCouponResult.id as string,
4219
+ id,
4220
+ })
4221
+ codesArray.push(id)
4222
+ }
4223
+ })
4224
+
4225
+ return codesArray
4226
+ },
4227
+ async createUser(data) {
4228
+ const id = crypto.randomUUID()
4229
+ try {
4230
+ await client.insert(users).values({ ...data, id })
4231
+
4232
+ // creating a personal organization for the user
4233
+ // every user gets an organization of their very own
4234
+ const personalOrganization = await adapter.createOrganization({
4235
+ name: `Personal (${data.email})`,
4236
+ })
4237
+
4238
+ if (!personalOrganization) {
4239
+ throw new Error('Failed to create personal organization')
4240
+ }
4241
+
4242
+ const membership = await adapter.addMemberToOrganization({
4243
+ organizationId: personalOrganization.id,
4244
+ userId: id,
4245
+ invitedById: id,
2525
4246
  })
2526
4247
 
2527
4248
  if (!membership) {
@@ -2542,7 +4263,11 @@ export function mySqlDrizzleAdapter(
2542
4263
 
2543
4264
  return user
2544
4265
  } catch (error) {
2545
- console.error(error)
4266
+ logger.error(toError(error), {
4267
+ email: data.email,
4268
+ event: 'adapter.user.create.failed',
4269
+ userId: id,
4270
+ })
2546
4271
  throw error
2547
4272
  }
2548
4273
  },
@@ -2978,6 +4703,15 @@ export function mySqlDrizzleAdapter(
2978
4703
 
2979
4704
  return subscriptionParsed.data
2980
4705
  },
4706
+ updateSubscriptionStatus: async (
4707
+ subscriptionId: string,
4708
+ status: string,
4709
+ ) => {
4710
+ await client
4711
+ .update(subscriptionTable)
4712
+ .set({ status })
4713
+ .where(eq(subscriptionTable.id, subscriptionId))
4714
+ },
2981
4715
  }
2982
4716
 
2983
4717
  return adapter