@coursebuilder/adapter-drizzle 0.5.0 → 0.5.1

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 (69) hide show
  1. package/dist/{chunk-BM5UKWXI.js → chunk-7ISZNMPU.js} +2 -2
  2. package/dist/{chunk-TBM3AK3P.js → chunk-7X7TMG4A.js} +2 -2
  3. package/dist/{chunk-CQNKTFXE.js → chunk-DAP5KOAO.js} +2 -2
  4. package/dist/{chunk-JZQ666KH.js → chunk-KDOSK7KN.js} +2 -2
  5. package/dist/{chunk-O6JECJRY.js → chunk-QPXUGIXO.js} +2 -2
  6. package/dist/{chunk-FRA4ZCRA.js → chunk-UHH4OVKA.js} +4 -14
  7. package/dist/{chunk-QTGVGIRO.js → chunk-UK6EHYS5.js} +2 -2
  8. package/dist/{chunk-Z245DYMP.js → chunk-VUS77FG3.js} +493 -198
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +12 -12
  11. package/dist/lib/mysql/index.cjs +498 -202
  12. package/dist/lib/mysql/index.cjs.map +1 -1
  13. package/dist/lib/mysql/index.d.cts +8 -30
  14. package/dist/lib/mysql/index.d.ts +8 -30
  15. package/dist/lib/mysql/index.js +10 -8
  16. package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
  17. package/dist/lib/mysql/schemas/auth/accounts.js +1 -1
  18. package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
  19. package/dist/lib/mysql/schemas/auth/device-access-token.js +2 -2
  20. package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
  21. package/dist/lib/mysql/schemas/auth/device-verification.js +2 -2
  22. package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
  23. package/dist/lib/mysql/schemas/auth/role-permissions.js +2 -2
  24. package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
  25. package/dist/lib/mysql/schemas/auth/roles.js +1 -1
  26. package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
  27. package/dist/lib/mysql/schemas/auth/sessions.js +2 -2
  28. package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
  29. package/dist/lib/mysql/schemas/auth/user-permissions.js +1 -1
  30. package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
  31. package/dist/lib/mysql/schemas/auth/user-prefs.js +1 -1
  32. package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
  33. package/dist/lib/mysql/schemas/auth/user-roles.js +1 -1
  34. package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
  35. package/dist/lib/mysql/schemas/auth/users.js +1 -1
  36. package/dist/lib/mysql/schemas/commerce/coupon.cjs +3 -13
  37. package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
  38. package/dist/lib/mysql/schemas/commerce/coupon.d.cts +2 -14
  39. package/dist/lib/mysql/schemas/commerce/coupon.d.ts +2 -14
  40. package/dist/lib/mysql/schemas/commerce/coupon.js +1 -1
  41. package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
  42. package/dist/lib/mysql/schemas/commerce/price.js +1 -1
  43. package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
  44. package/dist/lib/mysql/schemas/commerce/product.js +1 -1
  45. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
  46. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +2 -2
  47. package/dist/lib/mysql/schemas/commerce/purchase.cjs +0 -4
  48. package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
  49. package/dist/lib/mysql/schemas/commerce/purchase.js +1 -1
  50. package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
  51. package/dist/lib/mysql/schemas/commerce/upgradable-products.js +2 -2
  52. package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
  53. package/dist/lib/mysql/schemas/communication/comment.js +1 -1
  54. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
  55. package/dist/lib/mysql/schemas/communication/communication-preferences.js +1 -1
  56. package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
  57. package/dist/lib/mysql/schemas/content/content-contributions.js +1 -1
  58. package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
  59. package/dist/lib/mysql/schemas/content/content-resource-product.js +1 -1
  60. package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
  61. package/dist/lib/mysql/schemas/content/content-resource-resource.js +1 -1
  62. package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
  63. package/dist/lib/mysql/schemas/content/content-resource.js +1 -1
  64. package/dist/lib/utils.d.cts +1 -0
  65. package/dist/lib/utils.d.ts +1 -0
  66. package/package.json +4 -2
  67. package/src/index.ts +14 -4
  68. package/src/lib/mysql/index.ts +601 -182
  69. package/src/lib/mysql/schemas/commerce/coupon.ts +3 -9
@@ -1,4 +1,5 @@
1
1
  import type { AdapterSession, AdapterUser } from '@auth/core/adapters'
2
+ import slugify from '@sindresorhus/slugify'
2
3
  import { addSeconds, isAfter } from 'date-fns'
3
4
  import {
4
5
  and,
@@ -19,6 +20,7 @@ import {
19
20
  MySqlDatabase,
20
21
  MySqlTableFn,
21
22
  } from 'drizzle-orm/mysql-core'
23
+ import { customAlphabet } from 'nanoid'
22
24
  import { v4 } from 'uuid'
23
25
  import { z } from 'zod'
24
26
 
@@ -34,6 +36,7 @@ import {
34
36
  merchantPriceSchema,
35
37
  MerchantProduct,
36
38
  merchantProductSchema,
39
+ NewProduct,
37
40
  Price,
38
41
  priceSchema,
39
42
  Product,
@@ -51,15 +54,16 @@ import {
51
54
  userSchema,
52
55
  } from '@coursebuilder/core/schemas'
53
56
  import {
57
+ ContentResourceProductSchema,
54
58
  ContentResourceResourceSchema,
55
59
  ContentResourceSchema,
56
60
  type ContentResource,
57
- type ContentResourceResource,
58
61
  } from '@coursebuilder/core/schemas/content-resource-schema'
59
62
  import { merchantAccountSchema } from '@coursebuilder/core/schemas/merchant-account-schema'
60
63
  import { merchantCustomerSchema } from '@coursebuilder/core/schemas/merchant-customer-schema'
61
64
  import { type ModuleProgress } from '@coursebuilder/core/schemas/resource-progress-schema'
62
65
  import { VideoResourceSchema } from '@coursebuilder/core/schemas/video-resource'
66
+ import { PaymentsProviderConfig } from '@coursebuilder/core/types'
63
67
  import { logger } from '@coursebuilder/core/utils/logger'
64
68
  import { validateCoupon } from '@coursebuilder/core/utils/validate-coupon'
65
69
 
@@ -172,6 +176,8 @@ import {
172
176
  import { getLessonProgressSchema } from './schemas/content/lesson-progress.js'
173
177
  import { getResourceProgressSchema } from './schemas/content/resource-progress.js'
174
178
 
179
+ export const guid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 5)
180
+
175
181
  export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
176
182
  return {
177
183
  accounts: getAccountsSchema(mysqlTable),
@@ -254,6 +260,7 @@ export type DefaultSchema = ReturnType<typeof createTables>
254
260
  export function mySqlDrizzleAdapter(
255
261
  client: InstanceType<typeof MySqlDatabase>,
256
262
  tableFn = defaultMySqlTableFn,
263
+ paymentProvider?: PaymentsProviderConfig,
257
264
  ): CourseBuilderAdapter<typeof MySqlDatabase> {
258
265
  const {
259
266
  users,
@@ -262,6 +269,7 @@ export function mySqlDrizzleAdapter(
262
269
  verificationTokens,
263
270
  contentResource,
264
271
  contentResourceResource,
272
+ contentResourceProduct,
265
273
  purchases: purchaseTable,
266
274
  purchaseUserTransfer,
267
275
  coupon,
@@ -304,7 +312,7 @@ export function mySqlDrizzleAdapter(
304
312
  if (coupon && couponValidation.isRedeemable) {
305
313
  // if the Coupon is the Bulk Coupon of a Bulk Purchase,
306
314
  // then a bulk coupon is being redeemed
307
- const bulkCouponRedemption = Boolean(coupon.bulkPurchase?.bulkCouponId)
315
+ const bulkCouponRedemption = Boolean(coupon.maxUses > 1)
308
316
 
309
317
  const { user } = await adapter.findOrCreateUser(email)
310
318
 
@@ -325,8 +333,18 @@ export function mySqlDrizzleAdapter(
325
333
  productId,
326
334
  })
327
335
 
328
- if (existingPurchases.length > 0)
329
- throw new Error(`already-purchased-${email}`)
336
+ if (existingPurchases.length > 0) {
337
+ const errorMessage = `already-purchased-${email}`
338
+ console.error(errorMessage)
339
+ return {
340
+ error: {
341
+ message: errorMessage,
342
+ },
343
+ redeemingForCurrentUser,
344
+ purchase: null,
345
+ }
346
+ throw new Error(errorMessage)
347
+ }
330
348
 
331
349
  const purchaseId = `purchase-${v4()}`
332
350
 
@@ -410,7 +428,10 @@ export function mySqlDrizzleAdapter(
410
428
  },
411
429
  getMerchantPriceForProductId: async (productId) => {
412
430
  const merchantPriceData = await client.query.merchantPrice.findFirst({
413
- where: eq(merchantPrice.merchantProductId, productId),
431
+ where: and(
432
+ eq(merchantPrice.merchantProductId, productId),
433
+ eq(merchantPrice.status, 1),
434
+ ),
414
435
  })
415
436
 
416
437
  const parsedMerchantPrice =
@@ -463,7 +484,7 @@ export function mySqlDrizzleAdapter(
463
484
  )
464
485
 
465
486
  if (previousPurchaseProductIds.length > 0) {
466
- return await client.query.upgradableProducts.findMany({
487
+ return client.query.upgradableProducts.findMany({
467
488
  where: and(
468
489
  eq(upgradableProducts.upgradableToId, productId),
469
490
  inArray(
@@ -871,20 +892,21 @@ export function mySqlDrizzleAdapter(
871
892
  },
872
893
  async getCouponWithBulkPurchases(couponId: string): Promise<
873
894
  | (Coupon & {
874
- bulkPurchase?: Purchase | null
875
- bulkCouponPurchases: { bulkCouponId?: string | null }[]
895
+ bulkPurchases?: Purchase[] | null
896
+ redeemedBulkCouponPurchases: { bulkCouponId?: string | null }[]
876
897
  })
877
898
  | null
878
899
  > {
879
900
  logger.debug('getCouponWithBulkPurchases', { couponId })
880
901
  let couponData
902
+ let bulkCouponPurchases
881
903
  try {
882
904
  couponData =
883
905
  (await client.query.coupon.findFirst({
884
906
  where: eq(coupon.id, couponId),
885
907
  with: {
886
- bulkPurchase: true,
887
- bulkCouponPurchases: true,
908
+ bulkPurchases: true,
909
+ redeemedBulkCouponPurchases: true,
888
910
  },
889
911
  })) || null
890
912
  } catch (e) {
@@ -892,6 +914,19 @@ export function mySqlDrizzleAdapter(
892
914
  logger.error(e as Error)
893
915
  }
894
916
 
917
+ try {
918
+ bulkCouponPurchases = await client.query.purchases.findMany({
919
+ where: eq(purchaseTable.redeemedBulkCouponId, couponId),
920
+ with: {
921
+ user: true,
922
+ },
923
+ })
924
+ console.log('purchases with redeemedBulkCouponId', bulkCouponPurchases)
925
+ } catch (e) {
926
+ console.log('getCouponWithBulkPurchases')
927
+ logger.error(e as Error)
928
+ }
929
+
895
930
  if (!couponData) {
896
931
  logger.debug('getCouponWithBulkPurchases', {
897
932
  couponId,
@@ -900,20 +935,29 @@ export function mySqlDrizzleAdapter(
900
935
  return null
901
936
  }
902
937
 
938
+ const couponWithBulkPurchases = {
939
+ ...couponData,
940
+ redeemedBulkCouponPurchases: bulkCouponPurchases || [],
941
+ }
942
+
903
943
  const parsedCoupon = couponSchema
904
944
  .merge(
905
945
  z.object({
906
- bulkCouponPurchases: z.array(purchaseSchema),
907
- bulkPurchase: purchaseSchema.nullable().optional(),
946
+ redeemedBulkCouponPurchases: z.array(purchaseSchema),
908
947
  }),
909
948
  )
910
949
  .nullable()
911
- .safeParse(couponData)
950
+ .safeParse(couponWithBulkPurchases)
912
951
 
913
952
  if (!parsedCoupon.success) {
914
- console.error('Error parsing coupon', couponData)
953
+ console.error(
954
+ 'Error parsing coupon',
955
+ JSON.stringify(parsedCoupon.error),
956
+ couponData,
957
+ )
915
958
  return null
916
959
  }
960
+
917
961
  return parsedCoupon.data
918
962
  },
919
963
  async getDefaultCoupon(productIds?: string[]): Promise<{
@@ -982,189 +1026,114 @@ export function mySqlDrizzleAdapter(
982
1026
  async getModuleProgressForUser(
983
1027
  userIdOrEmail: string,
984
1028
  moduleIdOrSlug: string,
985
- ): Promise<ModuleProgress> {
986
- const ALLOWED_MODULE_RESOURCE_TYPES = ['lesson', 'exercise', 'solution']
987
- const COUNTABLE_MODULE_RESOURCE_TYPES = ['lesson', 'exercise']
988
-
989
- const module = await client.query.contentResource.findFirst({
990
- where: or(
991
- eq(contentResource.id, moduleIdOrSlug),
992
- eq(
993
- sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
994
- moduleIdOrSlug,
995
- ),
996
- ),
997
- with: {
998
- resources: {
999
- with: {
1000
- resource: {
1001
- with: {
1002
- resources: {
1003
- with: {
1004
- resource: {
1005
- with: {
1006
- resources: {
1007
- with: {
1008
- resource: true,
1009
- },
1010
- },
1011
- },
1012
- },
1013
- },
1014
- orderBy: asc(contentResourceResource.position),
1015
- },
1016
- },
1017
- },
1018
- },
1019
- orderBy: asc(contentResourceResource.position),
1020
- },
1029
+ ): Promise<ModuleProgress | null> {
1030
+ // First, get the user ID
1031
+ const user = await client.query.users.findFirst({
1032
+ where: or(eq(users.id, userIdOrEmail), eq(users.email, userIdOrEmail)),
1033
+ columns: {
1034
+ id: true,
1021
1035
  },
1022
1036
  })
1023
1037
 
1024
- const parsedModule = ContentResourceSchema.parse(module)
1025
-
1026
- function flattenResources(
1027
- resources: ContentResourceResource[],
1028
- ): ContentResource[] {
1029
- const result: ContentResource[] = []
1030
-
1031
- function recurse(resources: ContentResourceResource[]) {
1032
- for (const nestedResource of resources) {
1033
- const resource = nestedResource.resource
1034
- result.push(resource)
1035
- if (resource.resources) {
1036
- recurse(resource.resources)
1037
- }
1038
- }
1039
- }
1040
-
1041
- recurse(resources)
1042
- return result
1038
+ if (!user) {
1039
+ return null
1043
1040
  }
1044
1041
 
1045
- const allResources =
1046
- parsedModule?.resources && flattenResources(parsedModule.resources)
1047
- const filteredResources = allResources?.filter((resource) => {
1048
- return ALLOWED_MODULE_RESOURCE_TYPES.includes(resource.type)
1042
+ const ResultRowSchema = z.object({
1043
+ resource_id: z.string(),
1044
+ resource_type: z.enum(['lesson', 'exercise']),
1045
+ resource_slug: z.string().nullable(),
1046
+ completed_at: z
1047
+ .string()
1048
+ .nullable()
1049
+ .transform((val) => (val ? new Date(val) : null)),
1049
1050
  })
1050
1051
 
1051
- const parsedResources = z
1052
- .array(ContentResourceSchema)
1053
- .safeParse(filteredResources)
1054
-
1055
- if (!parsedResources.success) {
1056
- console.error('Error parsing module resources', parsedResources.error)
1052
+ // Execute the optimized query
1053
+ const results: any = await client.execute(sql`
1054
+ SELECT
1055
+ cr.id AS resource_id,
1056
+ cr.type AS resource_type,
1057
+ cr.fields->>'$.slug' AS resource_slug,
1058
+ rp.completedAt AS completed_at
1059
+ FROM
1060
+ (SELECT id, fields->>'$.slug' AS slug
1061
+ FROM ${contentResource}
1062
+ WHERE id = ${moduleIdOrSlug}
1063
+ OR fields->>'$.slug' = ${moduleIdOrSlug}) AS w
1064
+ LEFT JOIN
1065
+ (SELECT
1066
+ crr.resourceOfId AS workshop_id,
1067
+ crr.resourceId,
1068
+ crr.position
1069
+ FROM ${contentResourceResource} crr) AS tlr ON w.id = tlr.workshop_id
1070
+ LEFT JOIN ${contentResource} sections ON sections.id = tlr.resourceId AND sections.type = 'section'
1071
+ LEFT JOIN
1072
+ (SELECT
1073
+ crr.resourceOfId AS section_id,
1074
+ crr.resourceId AS resource_id,
1075
+ crr.position AS position_in_section,
1076
+ section_crr.position AS section_position
1077
+ FROM ${contentResourceResource} crr
1078
+ JOIN ${contentResourceResource} section_crr ON crr.resourceOfId = section_crr.resourceId) AS sr
1079
+ ON sr.section_id = sections.id
1080
+ JOIN ${contentResource} cr ON cr.id = COALESCE(sr.resource_id, tlr.resourceId)
1081
+ LEFT JOIN ${resourceProgress} rp ON rp.resourceId = cr.id
1082
+ AND rp.userId = ${user.id}
1083
+ WHERE
1084
+ cr.type IN ('lesson', 'exercise')
1085
+
1086
+ `)
1087
+
1088
+ // Process the results
1089
+ const completedLessons: ResourceProgress[] = []
1090
+ let nextResource: Partial<ContentResource> | null = null
1091
+ let completedLessonsCount = 0
1092
+ let totalLessonsCount = results.rows.length
1093
+
1094
+ const parsedRows = z.array(ResultRowSchema).safeParse(results.rows)
1095
+
1096
+ if (!parsedRows.success) {
1097
+ console.error('Error parsing rows', parsedRows.error)
1057
1098
  return {
1058
1099
  completedLessons: [],
1059
1100
  nextResource: null,
1060
1101
  percentCompleted: 0,
1061
1102
  completedLessonsCount: 0,
1062
- totalLessonsCount: 0,
1103
+ totalLessonsCount,
1063
1104
  }
1064
1105
  }
1065
1106
 
1066
- const progressResources = parsedResources.data.filter((r) => {
1067
- return COUNTABLE_MODULE_RESOURCE_TYPES.includes(r.type)
1068
- })
1069
-
1070
- const totalLessonsCount = progressResources.length
1071
-
1072
- const user = await client.query.users
1073
- .findFirst({
1074
- where: or(
1075
- eq(users.id, userIdOrEmail),
1076
- eq(users.email, userIdOrEmail),
1077
- ),
1078
- with: {
1079
- roles: {
1080
- with: {
1081
- role: true,
1082
- },
1107
+ for (const row of parsedRows.data) {
1108
+ if (row.completed_at) {
1109
+ completedLessonsCount++
1110
+ completedLessons.push({
1111
+ userId: user.id as string,
1112
+ resourceId: row.resource_id,
1113
+ completedAt: new Date(row.completed_at),
1114
+ // Add other fields as needed
1115
+ })
1116
+ } else if (!nextResource) {
1117
+ nextResource = {
1118
+ id: row.resource_id,
1119
+ type: row.resource_type,
1120
+ fields: {
1121
+ slug: row.resource_slug,
1083
1122
  },
1084
- },
1085
- })
1086
- .then(async (res) => {
1087
- if (res) {
1088
- return {
1089
- ...res,
1090
- roles: res.roles.map((r) => r.role),
1091
- }
1092
1123
  }
1093
- })
1094
-
1095
- if (!user) {
1096
- console.error('User not found', userIdOrEmail)
1097
- return {
1098
- completedLessons: [],
1099
- nextResource: null,
1100
- percentCompleted: 0,
1101
- completedLessonsCount: 0,
1102
- totalLessonsCount,
1103
- }
1104
- }
1105
-
1106
- const parsedUser = userSchema.safeParse(user)
1107
-
1108
- if (!parsedUser.success) {
1109
- console.error('Error parsing user', parsedUser.error)
1110
- return {
1111
- completedLessons: [],
1112
- nextResource: null,
1113
- percentCompleted: 0,
1114
- completedLessonsCount: 0,
1115
- totalLessonsCount,
1116
1124
  }
1117
1125
  }
1118
1126
 
1119
- const userProgress = await client.query.resourceProgress.findMany({
1120
- where: and(
1121
- eq(resourceProgress.userId, parsedUser.data.id),
1122
- isNotNull(resourceProgress.completedAt),
1123
- inArray(
1124
- resourceProgress.resourceId,
1125
- progressResources.map((r) => r.id),
1126
- ),
1127
- ),
1128
- orderBy: asc(resourceProgress.completedAt),
1129
- })
1130
-
1131
- const nextResourceId = parsedResources.data
1132
- .filter((r) => {
1133
- return COUNTABLE_MODULE_RESOURCE_TYPES.includes(r.type)
1134
- })
1135
- .find((r) => !userProgress.find((p) => p.resourceId === r.id))?.id
1136
-
1137
- const nextResource = await client.query.contentResource.findFirst({
1138
- where: eq(contentResource.id, nextResourceId as string),
1139
- })
1140
-
1141
- const parsedNextResource = ContentResourceSchema.nullable()
1142
- .optional()
1143
- .default(null)
1144
- .parse(nextResource)
1145
-
1146
- const parsedProgress = z
1147
- .array(resourceProgressSchema)
1148
- .safeParse(userProgress)
1149
- if (!parsedProgress.success) {
1150
- console.error('Error parsing user progress', parsedProgress.error)
1151
- return {
1152
- completedLessons: [],
1153
- nextResource: null,
1154
- percentCompleted: 0,
1155
- completedLessonsCount: 0,
1156
- totalLessonsCount,
1157
- }
1158
- }
1159
- const percentCompleted = Math.round(
1160
- (parsedProgress.data.length / parsedResources.data.length) * 100,
1161
- )
1127
+ const percentCompleted =
1128
+ totalLessonsCount > 0
1129
+ ? Math.ceil((completedLessonsCount / totalLessonsCount) * 100)
1130
+ : 0
1162
1131
 
1163
1132
  return {
1164
- completedLessons: parsedProgress.data,
1165
- nextResource: parsedNextResource,
1133
+ completedLessons,
1134
+ nextResource,
1166
1135
  percentCompleted,
1167
- completedLessonsCount: parsedProgress.data.length,
1136
+ completedLessonsCount,
1168
1137
  totalLessonsCount,
1169
1138
  }
1170
1139
  },
@@ -1179,7 +1148,7 @@ export function mySqlDrizzleAdapter(
1179
1148
  })
1180
1149
  const parsed = merchantChargeSchema.safeParse(mCharge)
1181
1150
  if (!parsed.success) {
1182
- console.error('Error parsing merchantCharge', mCharge)
1151
+ console.info('Error parsing merchantCharge', mCharge)
1183
1152
  return null
1184
1153
  }
1185
1154
  return parsed.data
@@ -1258,12 +1227,356 @@ export function mySqlDrizzleAdapter(
1258
1227
  }),
1259
1228
  )
1260
1229
  },
1261
- async getProduct(productId: string): Promise<Product | null> {
1262
- return productSchema.nullable().parse(
1263
- await client.query.products.findFirst({
1264
- where: eq(products.id, productId),
1230
+ async archiveProduct(productId) {
1231
+ if (!paymentProvider) throw new Error('Payment provider not found')
1232
+ const product = await adapter.getProduct(productId)
1233
+
1234
+ if (!product) {
1235
+ throw new Error(`Product not found for id (${productId})`)
1236
+ }
1237
+
1238
+ if (!product.price) {
1239
+ throw new Error(`Product has no price`)
1240
+ }
1241
+
1242
+ await client
1243
+ .update(products)
1244
+ .set({ status: 0, name: `${product.name} (Archived)` })
1245
+ .where(eq(products.id, productId))
1246
+
1247
+ await client
1248
+ .update(prices)
1249
+ .set({ status: 0, nickname: `${product.name} (Archived)` })
1250
+ .where(eq(prices.productId, productId))
1251
+
1252
+ await client
1253
+ .update(merchantProduct)
1254
+ .set({ status: 0 })
1255
+ .where(eq(merchantProduct.productId, productId))
1256
+
1257
+ await client
1258
+ .update(merchantPrice)
1259
+ .set({ status: 0 })
1260
+ .where(eq(merchantPrice.priceId, product.price.id))
1261
+
1262
+ const currentMerchantProduct = merchantProductSchema.nullish().parse(
1263
+ await client.query.merchantProduct.findFirst({
1264
+ where: eq(merchantProduct.productId, productId),
1265
+ }),
1266
+ )
1267
+
1268
+ if (!currentMerchantProduct || !currentMerchantProduct.identifier) {
1269
+ throw new Error(`Merchant product not found for id (${productId})`)
1270
+ }
1271
+
1272
+ await paymentProvider.updateProduct(currentMerchantProduct.identifier, {
1273
+ active: false,
1274
+ })
1275
+
1276
+ const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1277
+ await client.query.merchantPrice.findFirst({
1278
+ where: and(
1279
+ eq(merchantPrice.priceId, product.price.id),
1280
+ eq(merchantPrice.status, 1),
1281
+ ),
1282
+ }),
1283
+ )
1284
+
1285
+ if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1286
+ throw new Error(`Merchant price not found for id (${productId})`)
1287
+ }
1288
+
1289
+ await paymentProvider.updatePrice(currentMerchantPrice.identifier, {
1290
+ active: false,
1291
+ })
1292
+
1293
+ return adapter.getProduct(productId)
1294
+ },
1295
+ async updateProduct(input: Product) {
1296
+ if (!paymentProvider) throw new Error('Payment provider not found')
1297
+ const currentProduct = await adapter.getProduct(input.id)
1298
+ if (!currentProduct) {
1299
+ throw new Error(`Product not found`)
1300
+ }
1301
+ if (!currentProduct.price) {
1302
+ throw new Error(`Product has no price`)
1303
+ }
1304
+
1305
+ const merchantProduct = merchantProductSchema.nullish().parse(
1306
+ await client.query.merchantProduct.findFirst({
1307
+ where: (merchantProduct, { eq }) =>
1308
+ eq(merchantProduct.productId, input.id),
1309
+ }),
1310
+ )
1311
+
1312
+ if (!merchantProduct || !merchantProduct.identifier) {
1313
+ throw new Error(`Merchant product not found`)
1314
+ }
1315
+
1316
+ // TODO: handle upgrades
1317
+
1318
+ const stripeProduct = await paymentProvider.getProduct(
1319
+ merchantProduct.identifier,
1320
+ )
1321
+
1322
+ const priceChanged =
1323
+ currentProduct.price.unitAmount.toString() !==
1324
+ input.price?.unitAmount.toString()
1325
+
1326
+ if (priceChanged) {
1327
+ const currentMerchantPrice = merchantPriceSchema.nullish().parse(
1328
+ await client.query.merchantPrice.findFirst({
1329
+ where: (merchantPrice, { eq, and }) =>
1330
+ and(
1331
+ eq(merchantPrice.merchantProductId, merchantProduct.id),
1332
+ eq(merchantPrice.status, 1),
1333
+ ),
1334
+ }),
1335
+ )
1336
+
1337
+ if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
1338
+ throw new Error(`Merchant price not found`)
1339
+ }
1340
+
1341
+ const currentStripePrice = await paymentProvider.getPrice(
1342
+ currentMerchantPrice.identifier,
1343
+ )
1344
+
1345
+ const newStripePrice = await paymentProvider.createPrice({
1346
+ product: stripeProduct.id,
1347
+ unit_amount: Math.floor(Number(input.price?.unitAmount || 0) * 100),
1348
+ currency: 'usd',
1349
+ metadata: {
1350
+ slug: input.fields.slug,
1351
+ },
1352
+ active: true,
1353
+ })
1354
+
1355
+ await paymentProvider.updateProduct(stripeProduct.id, {
1356
+ default_price: newStripePrice.id,
1357
+ })
1358
+
1359
+ const newMerchantPriceId = `mprice_${v4()}`
1360
+ await client.insert(merchantPrice).values({
1361
+ id: newMerchantPriceId,
1362
+ merchantProductId: merchantProduct.id,
1363
+ merchantAccountId: merchantProduct.merchantAccountId,
1364
+ priceId: currentProduct.price.id,
1365
+ status: 1,
1366
+ identifier: newStripePrice.id,
1367
+ })
1368
+
1369
+ if (currentMerchantPrice) {
1370
+ await client
1371
+ .update(merchantPrice)
1372
+ .set({
1373
+ status: 0,
1374
+ })
1375
+ .where(eq(merchantPrice.id, currentMerchantPrice.id))
1376
+ }
1377
+
1378
+ await client
1379
+ .update(prices)
1380
+ .set({
1381
+ unitAmount: Math.floor(
1382
+ Number(input.price?.unitAmount || 0),
1383
+ ).toString(),
1384
+ nickname: input.name,
1385
+ })
1386
+ .where(eq(prices.id, currentProduct.price.id))
1387
+
1388
+ if (currentStripePrice) {
1389
+ await paymentProvider.updatePrice(currentStripePrice.id, {
1390
+ active: false,
1391
+ })
1392
+ }
1393
+ }
1394
+
1395
+ await paymentProvider.updateProduct(stripeProduct.id, {
1396
+ name: input.name,
1397
+ active: true,
1398
+ images: input.fields.image?.url ? [input.fields.image.url] : undefined,
1399
+ description: input.fields.description || '',
1400
+ metadata: {
1401
+ slug: input.fields.slug,
1402
+ },
1403
+ })
1404
+
1405
+ const { image, ...fieldsNoImage } = input.fields
1406
+
1407
+ await client
1408
+ .update(products)
1409
+ .set({
1410
+ name: input.name,
1411
+ quantityAvailable: input.quantityAvailable,
1412
+ status: 1,
1413
+ fields: {
1414
+ ...fieldsNoImage,
1415
+ ...(image?.url && { image }),
1416
+ },
1417
+ })
1418
+ .where(eq(products.id, currentProduct.id))
1419
+
1420
+ return adapter.getProduct(currentProduct.id)
1421
+ },
1422
+ async createProduct(input: NewProduct) {
1423
+ if (!paymentProvider) throw new Error('Payment provider not found')
1424
+ const merchantAccount = merchantAccountSchema.nullish().parse(
1425
+ await client.query.merchantAccount.findFirst({
1426
+ where: (merchantAccount, { eq }) =>
1427
+ eq(merchantAccount.label, 'stripe'),
1265
1428
  }),
1266
1429
  )
1430
+
1431
+ if (!merchantAccount) {
1432
+ throw new Error('Merchant account not found')
1433
+ }
1434
+
1435
+ const hash = guid()
1436
+ const newProductId = slugify(`product-${hash}`)
1437
+
1438
+ const newProduct = {
1439
+ id: newProductId,
1440
+ name: input.name,
1441
+ status: 1,
1442
+ type: 'self-paced',
1443
+ quantityAvailable: input.quantityAvailable,
1444
+ fields: {
1445
+ state: 'draft',
1446
+ visibility: 'unlisted',
1447
+ slug: slugify(`${input.name}-${hash}`),
1448
+ },
1449
+ }
1450
+
1451
+ await client.insert(products).values(newProduct)
1452
+
1453
+ const priceHash = guid()
1454
+ const newPriceId = `price-${priceHash}`
1455
+
1456
+ await client.insert(prices).values({
1457
+ id: newPriceId,
1458
+ productId: newProductId,
1459
+ unitAmount: input.price.toString(),
1460
+ status: 1,
1461
+ })
1462
+
1463
+ const product = await adapter.getProduct(newProductId)
1464
+
1465
+ const stripeProduct = await paymentProvider.createProduct({
1466
+ name: input.name,
1467
+ metadata: {
1468
+ slug: product?.fields?.slug || null,
1469
+ },
1470
+ })
1471
+
1472
+ const stripePrice = await paymentProvider.createPrice({
1473
+ product: stripeProduct.id,
1474
+ unit_amount: Math.floor(Number(input.price) * 100),
1475
+ currency: 'usd',
1476
+ nickname: input.name,
1477
+ metadata: {
1478
+ slug: product?.fields?.slug || null,
1479
+ },
1480
+ })
1481
+
1482
+ const newMerchantProductId = `mproduct_${v4()}`
1483
+
1484
+ await client.insert(merchantProduct).values({
1485
+ id: newMerchantProductId,
1486
+ merchantAccountId: merchantAccount.id,
1487
+ productId: newProductId,
1488
+ identifier: stripeProduct.id,
1489
+ status: 1,
1490
+ })
1491
+
1492
+ const newMerchantPriceId = `mprice_${v4()}`
1493
+ await client.insert(merchantPrice).values({
1494
+ id: newMerchantPriceId,
1495
+ merchantAccountId: merchantAccount.id,
1496
+ merchantProductId: newMerchantProductId,
1497
+ priceId: newPriceId,
1498
+ identifier: stripePrice.id,
1499
+ status: 1,
1500
+ })
1501
+
1502
+ // TODO: handle upgrades
1503
+
1504
+ return product
1505
+ },
1506
+ async getProduct(
1507
+ productSlugOrId?: string,
1508
+ withResources: boolean = true,
1509
+ ): Promise<Product | null> {
1510
+ if (!productSlugOrId) {
1511
+ return null
1512
+ }
1513
+
1514
+ try {
1515
+ const productData = await client.query.products.findFirst({
1516
+ where: and(
1517
+ or(
1518
+ eq(
1519
+ sql`JSON_EXTRACT (${products.fields}, "$.slug")`,
1520
+ `${productSlugOrId}`,
1521
+ ),
1522
+ eq(products.id, productSlugOrId),
1523
+ ),
1524
+ ),
1525
+ with: {
1526
+ price: true,
1527
+ ...(withResources && {
1528
+ resources: {
1529
+ with: {
1530
+ resource: {
1531
+ with: {
1532
+ resources: true,
1533
+ },
1534
+ },
1535
+ },
1536
+ },
1537
+ }),
1538
+ },
1539
+ })
1540
+ const parsedProduct = productSchema.safeParse(productData)
1541
+ if (!parsedProduct.success) {
1542
+ console.error(
1543
+ 'Error parsing product',
1544
+ JSON.stringify(parsedProduct.error),
1545
+ JSON.stringify(productData),
1546
+ )
1547
+ return null
1548
+ }
1549
+ return parsedProduct.data
1550
+ } catch (e) {
1551
+ console.log('getProduct error', e)
1552
+ return null
1553
+ }
1554
+ },
1555
+ async getProductResources(
1556
+ productId: string,
1557
+ ): Promise<ContentResource[] | null> {
1558
+ const contentResourceProductsForProduct = z
1559
+ .array(ContentResourceProductSchema)
1560
+ .nullable()
1561
+ .parse(
1562
+ await client.query.contentResourceProduct.findMany({
1563
+ where: eq(contentResourceProduct.productId, productId),
1564
+ }),
1565
+ )
1566
+
1567
+ if (!contentResourceProductsForProduct) {
1568
+ return null
1569
+ } else {
1570
+ const contentResources = z.array(ContentResourceSchema).parse(
1571
+ await client.query.contentResource.findMany({
1572
+ where: inArray(
1573
+ contentResource.id,
1574
+ contentResourceProductsForProduct.map((crp) => crp.resourceId),
1575
+ ),
1576
+ }),
1577
+ )
1578
+ return contentResources
1579
+ }
1267
1580
  },
1268
1581
  async getPurchaseCountForProduct(productId: string): Promise<number> {
1269
1582
  return await client.query.purchases
@@ -1306,7 +1619,11 @@ export function mySqlDrizzleAdapter(
1306
1619
  )
1307
1620
 
1308
1621
  if (!chargeForPurchase) {
1309
- logger.error(new Error('No charge found for purchase'))
1622
+ logger.error(
1623
+ new Error(
1624
+ `No charge found for purchase: Stripe Charge ID: ${stripeChargeId}`,
1625
+ ),
1626
+ )
1310
1627
  return null
1311
1628
  }
1312
1629
 
@@ -1516,6 +1833,34 @@ export function mySqlDrizzleAdapter(
1516
1833
  purchase: parsedPurchase.data,
1517
1834
  }
1518
1835
  },
1836
+ async getUserWithPurchasersByEmail(email: string): Promise<any | null> {
1837
+ const user = await client.query.users.findFirst({
1838
+ where: eq(users.email, email.trim().toLowerCase()),
1839
+ with: {
1840
+ // merchantCustomer: true,
1841
+ roles: {
1842
+ with: {
1843
+ role: true,
1844
+ },
1845
+ },
1846
+ purchases: {
1847
+ with: {
1848
+ bulkCoupon: true,
1849
+ merchantCharge: true,
1850
+ product: {
1851
+ columns: {
1852
+ id: true,
1853
+ name: true,
1854
+ key: true,
1855
+ },
1856
+ },
1857
+ },
1858
+ },
1859
+ },
1860
+ })
1861
+
1862
+ return user
1863
+ },
1519
1864
  async getUserById(userId: string): Promise<User | null> {
1520
1865
  const user = await client.query.users
1521
1866
  .findFirst({
@@ -1638,11 +1983,56 @@ export function mySqlDrizzleAdapter(
1638
1983
  }): Promise<unknown> {
1639
1984
  throw new Error('Method not implemented.')
1640
1985
  },
1641
- updatePurchaseStatusForCharge(
1986
+ async updatePurchaseStatusForCharge(
1642
1987
  chargeId: string,
1643
1988
  status: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',
1644
1989
  ): Promise<Purchase | undefined> {
1645
- throw new Error('updatePurchaseStatusForCharge Method not implemented.')
1990
+ try {
1991
+ console.log('updatePurchaseStatusForCharge', { chargeId, status })
1992
+ const merchantChargeForPurchase =
1993
+ await client.query.merchantCharge.findFirst({
1994
+ where: or(
1995
+ eq(merchantCharge.identifier, chargeId),
1996
+ eq(merchantCharge.id, chargeId),
1997
+ ),
1998
+ })
1999
+
2000
+ console.log('merchantChargeForPurchase', { merchantChargeForPurchase })
2001
+
2002
+ const parsedMerchantChargeForPurchase = merchantChargeSchema.parse(
2003
+ merchantChargeForPurchase,
2004
+ )
2005
+
2006
+ if (!parsedMerchantChargeForPurchase)
2007
+ throw new Error(`no-charge-found-for-purchase ${chargeId}`)
2008
+
2009
+ const purchase = await client.query.purchases.findFirst({
2010
+ where: eq(
2011
+ purchaseTable.merchantChargeId,
2012
+ parsedMerchantChargeForPurchase.id,
2013
+ ),
2014
+ })
2015
+
2016
+ const parsedPurchase = purchaseSchema.nullable().parse(purchase)
2017
+
2018
+ if (parsedPurchase) {
2019
+ await client
2020
+ .update(purchaseTable)
2021
+ .set({ status: status })
2022
+ .where(eq(purchaseTable.id, parsedPurchase.id))
2023
+
2024
+ const newPurchase = await client.query.purchases.findFirst({
2025
+ where: eq(purchaseTable.id, parsedPurchase.id),
2026
+ })
2027
+
2028
+ return purchaseSchema.optional().parse(newPurchase)
2029
+ } else {
2030
+ throw new Error(`no-purchase-found-for-charge ${chargeId}`)
2031
+ }
2032
+ } catch (e) {
2033
+ console.log('error updating purchase status', e)
2034
+ throw e
2035
+ }
1646
2036
  },
1647
2037
  async updatePurchaseUserTransferTransferState(options: {
1648
2038
  id: string
@@ -1706,6 +2096,35 @@ export function mySqlDrizzleAdapter(
1706
2096
 
1707
2097
  return parsedResourceJoin.data
1708
2098
  },
2099
+ async removeResourceFromResource(options) {
2100
+ const { childResourceId, parentResourceId } = options
2101
+
2102
+ const resourceJoin = await client.query.contentResourceResource.findFirst(
2103
+ {
2104
+ where: and(
2105
+ eq(contentResourceResource.resourceOfId, parentResourceId),
2106
+ eq(contentResourceResource.resourceId, childResourceId),
2107
+ ),
2108
+ },
2109
+ )
2110
+
2111
+ const parsedResourceJoin =
2112
+ ContentResourceResourceSchema.safeParse(resourceJoin)
2113
+ if (!parsedResourceJoin.success) {
2114
+ return null
2115
+ }
2116
+
2117
+ await client
2118
+ .delete(contentResourceResource)
2119
+ .where(
2120
+ and(
2121
+ eq(contentResourceResource.resourceOfId, parentResourceId),
2122
+ eq(contentResourceResource.resourceId, childResourceId),
2123
+ ),
2124
+ )
2125
+
2126
+ return parsedResourceJoin.data.resource
2127
+ },
1709
2128
  async updateContentResourceFields(options) {
1710
2129
  if (!options.id) {
1711
2130
  throw new Error('No content resource id.')