@coursebuilder/adapter-drizzle 1.0.6 → 2.0.0

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 (115) hide show
  1. package/dist/authjs.d.ts +27 -0
  2. package/dist/authjs.js +1 -0
  3. package/dist/chunk-3XHKEHGF.js +88 -0
  4. package/dist/{chunk-ENIKVP6O.js → chunk-4SMVVOU5.js} +2 -2
  5. package/dist/{chunk-EJ7UDN4T.js → chunk-52A4W4YI.js} +2 -2
  6. package/dist/{chunk-SUOPB24V.js → chunk-6VDGZ4KA.js} +2 -2
  7. package/dist/{chunk-MUFYDXA7.js → chunk-DB473RZJ.js} +9 -8
  8. package/dist/{chunk-XKCPXM63.js → chunk-DHII2GRW.js} +2 -2
  9. package/dist/{chunk-YMVONZZW.js → chunk-Q5NZ5UFZ.js} +2 -2
  10. package/dist/{chunk-5WMUBNWJ.js → chunk-QDWLBCD3.js} +2 -2
  11. package/dist/{chunk-MQSLKVLJ.js → chunk-RDAVEULA.js} +2 -2
  12. package/dist/{chunk-HEQ5MII4.js → chunk-ULJCXOOM.js} +564 -127
  13. package/dist/{chunk-U6XVAMCK.js → chunk-WPDVDHLH.js} +4 -3
  14. package/dist/index.d.ts +5 -3
  15. package/dist/index.js +15 -14
  16. package/dist/lib/mysql/index.cjs +967 -456
  17. package/dist/lib/mysql/index.cjs.map +1 -1
  18. package/dist/lib/mysql/index.d.cts +412 -6
  19. package/dist/lib/mysql/index.d.ts +412 -6
  20. package/dist/lib/mysql/index.js +15 -14
  21. package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
  22. package/dist/lib/mysql/schemas/auth/accounts.d.cts +2 -2
  23. package/dist/lib/mysql/schemas/auth/accounts.d.ts +2 -2
  24. package/dist/lib/mysql/schemas/auth/accounts.js +3 -3
  25. package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
  26. package/dist/lib/mysql/schemas/auth/device-access-token.js +4 -4
  27. package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
  28. package/dist/lib/mysql/schemas/auth/device-verification.js +4 -4
  29. package/dist/lib/mysql/schemas/auth/personal-access-token.cjs +288 -0
  30. package/dist/lib/mysql/schemas/auth/personal-access-token.cjs.map +1 -0
  31. package/dist/lib/mysql/schemas/auth/personal-access-token.d.cts +243 -0
  32. package/dist/lib/mysql/schemas/auth/personal-access-token.d.ts +243 -0
  33. package/dist/lib/mysql/schemas/auth/personal-access-token.js +21 -0
  34. package/dist/lib/mysql/schemas/auth/personal-access-token.js.map +1 -0
  35. package/dist/lib/mysql/schemas/auth/profiles.cjs.map +1 -1
  36. package/dist/lib/mysql/schemas/auth/profiles.js +3 -3
  37. package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
  38. package/dist/lib/mysql/schemas/auth/role-permissions.js +4 -4
  39. package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
  40. package/dist/lib/mysql/schemas/auth/roles.js +3 -3
  41. package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
  42. package/dist/lib/mysql/schemas/auth/sessions.js +4 -4
  43. package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
  44. package/dist/lib/mysql/schemas/auth/user-permissions.js +3 -3
  45. package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
  46. package/dist/lib/mysql/schemas/auth/user-prefs.js +3 -3
  47. package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
  48. package/dist/lib/mysql/schemas/auth/user-roles.js +3 -3
  49. package/dist/lib/mysql/schemas/auth/users.cjs +2 -1
  50. package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
  51. package/dist/lib/mysql/schemas/auth/users.js +3 -3
  52. package/dist/lib/mysql/schemas/commerce/coupon.cjs +2 -1
  53. package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
  54. package/dist/lib/mysql/schemas/commerce/coupon.js +3 -3
  55. package/dist/lib/mysql/schemas/commerce/merchant-charge.cjs.map +1 -1
  56. package/dist/lib/mysql/schemas/commerce/merchant-charge.js +3 -3
  57. package/dist/lib/mysql/schemas/commerce/merchant-subscription.cjs.map +1 -1
  58. package/dist/lib/mysql/schemas/commerce/merchant-subscription.js +3 -3
  59. package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
  60. package/dist/lib/mysql/schemas/commerce/price.js +3 -3
  61. package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
  62. package/dist/lib/mysql/schemas/commerce/product.js +3 -3
  63. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs +2 -1
  64. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
  65. package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +4 -4
  66. package/dist/lib/mysql/schemas/commerce/purchase.cjs +2 -1
  67. package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
  68. package/dist/lib/mysql/schemas/commerce/purchase.js +3 -3
  69. package/dist/lib/mysql/schemas/commerce/subscription.cjs.map +1 -1
  70. package/dist/lib/mysql/schemas/commerce/subscription.js +3 -3
  71. package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
  72. package/dist/lib/mysql/schemas/commerce/upgradable-products.js +4 -4
  73. package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
  74. package/dist/lib/mysql/schemas/communication/comment.js +3 -3
  75. package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
  76. package/dist/lib/mysql/schemas/communication/communication-preferences.js +3 -3
  77. package/dist/lib/mysql/schemas/communication/question-response.cjs.map +1 -1
  78. package/dist/lib/mysql/schemas/communication/question-response.js +4 -4
  79. package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
  80. package/dist/lib/mysql/schemas/content/content-contributions.js +3 -3
  81. package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
  82. package/dist/lib/mysql/schemas/content/content-resource-product.js +3 -3
  83. package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
  84. package/dist/lib/mysql/schemas/content/content-resource-resource.js +3 -3
  85. package/dist/lib/mysql/schemas/content/content-resource-tag.cjs.map +1 -1
  86. package/dist/lib/mysql/schemas/content/content-resource-tag.js +3 -3
  87. package/dist/lib/mysql/schemas/content/content-resource-version.cjs.map +1 -1
  88. package/dist/lib/mysql/schemas/content/content-resource-version.js +3 -3
  89. package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
  90. package/dist/lib/mysql/schemas/content/content-resource.js +3 -3
  91. package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -1
  92. package/dist/lib/mysql/schemas/content/tag-tag.js +3 -3
  93. package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -1
  94. package/dist/lib/mysql/schemas/content/tag.js +3 -3
  95. package/dist/lib/mysql/schemas/entitlements/entitlement.cjs +2 -1
  96. package/dist/lib/mysql/schemas/entitlements/entitlement.cjs.map +1 -1
  97. package/dist/lib/mysql/schemas/entitlements/entitlement.js +4 -4
  98. package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs.map +1 -1
  99. package/dist/lib/mysql/schemas/org/organization-membership-roles.js +3 -3
  100. package/dist/lib/mysql/schemas/org/organization-memberships.cjs +2 -1
  101. package/dist/lib/mysql/schemas/org/organization-memberships.cjs.map +1 -1
  102. package/dist/lib/mysql/schemas/org/organization-memberships.js +3 -3
  103. package/dist/lib/mysql/schemas/org/organizations.cjs +2 -1
  104. package/dist/lib/mysql/schemas/org/organizations.cjs.map +1 -1
  105. package/dist/lib/mysql/schemas/org/organizations.js +3 -3
  106. package/dist/lib/utils.d.cts +3 -1
  107. package/dist/lib/utils.d.ts +3 -1
  108. package/package.json +9 -8
  109. package/src/authjs.ts +34 -0
  110. package/src/index.ts +2 -2
  111. package/src/lib/mysql/index.ts +224 -50
  112. package/src/lib/mysql/schemas/auth/accounts.ts +3 -2
  113. package/src/lib/mysql/schemas/auth/personal-access-token.ts +107 -0
  114. package/src/lib/mysql/schemas/commerce/purchase.ts +3 -0
  115. package/src/lib/mysql/schemas/entitlements/entitlement.ts +3 -0
package/src/index.ts CHANGED
@@ -5,9 +5,9 @@ import {
5
5
  type MySqlTableFn,
6
6
  } from 'drizzle-orm/mysql-core'
7
7
 
8
- import { type CourseBuilderAdapter } from '@coursebuilder/core/adapters'
9
8
  import { PaymentsProviderConfig } from '@coursebuilder/core/types'
10
9
 
10
+ import type { AuthJsCourseBuilderAdapter } from './authjs.js'
11
11
  import {
12
12
  createTables as createMySqlTables,
13
13
  mySqlDrizzleAdapter,
@@ -18,7 +18,7 @@ export function DrizzleAdapter<SqlFlavor extends SqlFlavorOptions>(
18
18
  db: SqlFlavor,
19
19
  table: TableFn<SqlFlavor>,
20
20
  paymentProvider?: PaymentsProviderConfig,
21
- ): CourseBuilderAdapter {
21
+ ): AuthJsCourseBuilderAdapter {
22
22
  if (is(db, MySqlDatabase)) {
23
23
  return mySqlDrizzleAdapter(
24
24
  db,
@@ -1,4 +1,3 @@
1
- import type { AdapterSession, AdapterUser } from '@auth/core/adapters'
2
1
  import slugify from '@sindresorhus/slugify'
3
2
  import { addSeconds, isAfter } from 'date-fns'
4
3
  import {
@@ -24,7 +23,14 @@ import { customAlphabet } from 'nanoid'
24
23
  import { v4 } from 'uuid'
25
24
  import { z } from 'zod'
26
25
 
27
- import { type CourseBuilderAdapter } from '@coursebuilder/core/adapters'
26
+ import type {
27
+ CourseBuilderAuthSession,
28
+ CourseBuilderUser,
29
+ } from '@coursebuilder/core/auth'
30
+ import {
31
+ parseAndExpandPurchaseBenefitTransport,
32
+ PURCHASE_BENEFIT_METADATA_KEY,
33
+ } from '@coursebuilder/core/pricing/purchase-benefit-transport'
28
34
  import {
29
35
  Coupon,
30
36
  couponSchema,
@@ -79,6 +85,7 @@ import { PaymentsProviderConfig } from '@coursebuilder/core/types'
79
85
  import { logger as baseLogger } from '@coursebuilder/core/utils/logger'
80
86
  import { validateCoupon } from '@coursebuilder/core/utils/validate-coupon'
81
87
 
88
+ import type { AuthJsCourseBuilderAdapter } from '../../authjs.js'
82
89
  import {
83
90
  getAccountsRelationsSchema,
84
91
  getAccountsSchema,
@@ -95,6 +102,10 @@ import {
95
102
  getPermissionsRelationsSchema,
96
103
  getPermissionsSchema,
97
104
  } from './schemas/auth/permissions.js'
105
+ import {
106
+ getPersonalAccessTokenRelationsSchema,
107
+ getPersonalAccessTokenSchema,
108
+ } from './schemas/auth/personal-access-token.js'
98
109
  import {
99
110
  getProfilesRelationsSchema,
100
111
  getProfilesSchema,
@@ -270,6 +281,24 @@ const logger = baseLogger as unknown as {
270
281
  info: (message: string, attrs?: Record<string, unknown>) => void
271
282
  }
272
283
 
284
+ type PurchaseBenefitExpansion = ReturnType<
285
+ typeof parseAndExpandPurchaseBenefitTransport
286
+ > | null
287
+
288
+ function mergePurchaseBenefitFields(
289
+ fields: Record<string, any>,
290
+ expansion: PurchaseBenefitExpansion,
291
+ ) {
292
+ if (!expansion) return fields
293
+
294
+ return {
295
+ ...fields,
296
+ ...(expansion.success
297
+ ? { purchaseBenefits: expansion.benefits }
298
+ : { purchaseBenefitsReview: expansion.review }),
299
+ }
300
+ }
301
+
273
302
  export type CourseBuilderSchemaOverrides<
274
303
  TUsers extends ReturnType<typeof getUsersSchema> = ReturnType<
275
304
  typeof getUsersSchema
@@ -404,6 +433,11 @@ export function getCourseBuilderSchema<
404
433
  mysqlTable,
405
434
  userRelationsOverrides,
406
435
  ),
436
+ personalAccessToken: getPersonalAccessTokenSchema(mysqlTable),
437
+ personalAccessTokenRelations: getPersonalAccessTokenRelationsSchema(
438
+ mysqlTable,
439
+ userRelationsOverrides,
440
+ ),
407
441
  tag: getTagSchema(mysqlTable),
408
442
  tagRelations: getTagRelationsSchema(mysqlTable),
409
443
  tagTag: getTagTagSchema(mysqlTable),
@@ -449,11 +483,58 @@ export function createTables(mySqlTable: MySqlTableFn) {
449
483
 
450
484
  export type DefaultSchema = ReturnType<typeof createTables>
451
485
 
486
+ /**
487
+ * Provisions the personal organization every Course Builder user owns:
488
+ * creates the organization, adds the user as a member, and grants the owner
489
+ * role. Exposed on the adapter as the UserProvisioningPort contract from
490
+ * @coursebuilder/core (ADR-0012 section 3).
491
+ *
492
+ * MIGRATION BRIDGE (ADR-0012 section 6): createUser still invokes this from
493
+ * inside the Auth.js storage hook, and 7 apps carry duplicate logic in
494
+ * src/lib/personal-organization-service.ts. Phase 3 must move the trigger to
495
+ * the app auth boundary (NextAuth createUser event / signIn callback) and
496
+ * delete the app copies. Removal criteria: no call sites inside createUser
497
+ * and zero personal-organization-service.ts files under apps/.
498
+ * Do not add new callers of the inline-hook path.
499
+ */
500
+ async function provisionPersonalOrganization(
501
+ adapter: AuthJsCourseBuilderAdapter<typeof MySqlDatabase>,
502
+ params: { userId: string; email: string },
503
+ ): Promise<void> {
504
+ const { userId, email } = params
505
+
506
+ // creating a personal organization for the user
507
+ // every user gets an organization of their very own
508
+ const personalOrganization = await adapter.createOrganization({
509
+ name: `Personal (${email})`,
510
+ })
511
+
512
+ if (!personalOrganization) {
513
+ throw new Error('Failed to create personal organization')
514
+ }
515
+
516
+ const membership = await adapter.addMemberToOrganization({
517
+ organizationId: personalOrganization.id,
518
+ userId: userId,
519
+ invitedById: userId,
520
+ })
521
+
522
+ if (!membership) {
523
+ throw new Error('Failed to add user to personal organization')
524
+ }
525
+
526
+ await adapter.addRoleForMember({
527
+ organizationId: personalOrganization.id,
528
+ memberId: membership.id,
529
+ role: 'owner',
530
+ })
531
+ }
532
+
452
533
  export function mySqlDrizzleAdapter(
453
534
  client: InstanceType<typeof MySqlDatabase>,
454
535
  tableFn = defaultMySqlTableFn,
455
536
  paymentProvider?: PaymentsProviderConfig,
456
- ): CourseBuilderAdapter<typeof MySqlDatabase> {
537
+ ): AuthJsCourseBuilderAdapter<typeof MySqlDatabase> {
457
538
  const {
458
539
  users,
459
540
  accounts,
@@ -489,8 +570,14 @@ export function mySqlDrizzleAdapter(
489
570
  entitlementTypes,
490
571
  } = createTables(tableFn)
491
572
 
492
- const adapter: CourseBuilderAdapter = {
573
+ const normalizeLearnerEmail = (email: string) =>
574
+ email.trim().toLowerCase().replace(/\s+/g, '+')
575
+
576
+ const adapter: AuthJsCourseBuilderAdapter<typeof MySqlDatabase> = {
493
577
  client,
578
+ async provisionPersonalOrganization(params) {
579
+ await provisionPersonalOrganization(adapter, params)
580
+ },
494
581
  async redeemFullPriceCoupon(options) {
495
582
  const {
496
583
  email: baseEmail,
@@ -499,7 +586,7 @@ export function mySqlDrizzleAdapter(
499
586
  currentUserId,
500
587
  redeemingProductId,
501
588
  } = options
502
- const email = String(baseEmail).replace(' ', '+')
589
+ const email = normalizeLearnerEmail(String(baseEmail))
503
590
 
504
591
  const coupon = await adapter.getCouponWithBulkPurchases(couponId)
505
592
 
@@ -526,6 +613,35 @@ export function mySqlDrizzleAdapter(
526
613
 
527
614
  const redeemingForCurrentUser = currentUser?.id === user.id
528
615
 
616
+ if (bulkCouponRedemption) {
617
+ const existingValidRedemption =
618
+ await client.query.purchases.findFirst({
619
+ where: and(
620
+ eq(purchaseTable.redeemedBulkCouponId, coupon.id),
621
+ eq(purchaseTable.userId, user.id),
622
+ eq(purchaseTable.status, 'Valid'),
623
+ ),
624
+ })
625
+
626
+ if (existingValidRedemption) {
627
+ logger.info('adapter.coupon.redeem.duplicate_team_seat', {
628
+ couponId: coupon.id,
629
+ currentUserId,
630
+ email,
631
+ productId,
632
+ purchaseId: existingValidRedemption.id,
633
+ userId: user.id,
634
+ })
635
+ return {
636
+ error: {
637
+ message: 'team-seat-redemption-already-claimed',
638
+ },
639
+ redeemingForCurrentUser,
640
+ purchase: null,
641
+ }
642
+ }
643
+ }
644
+
529
645
  // To prevent double-purchasing, check if this user already has a
530
646
  // Purchase record for this product that is valid and wasn't a bulk
531
647
  // coupon purchase.
@@ -756,7 +872,12 @@ export function mySqlDrizzleAdapter(
756
872
  completedAt: now,
757
873
  updatedAt: now,
758
874
  })
759
- .where(eq(resourceProgress.resourceId, options.lessonId))
875
+ .where(
876
+ and(
877
+ eq(resourceProgress.userId, options.userId),
878
+ eq(resourceProgress.resourceId, options.lessonId),
879
+ ),
880
+ )
760
881
  }
761
882
  } else {
762
883
  await client.insert(resourceProgress).values({
@@ -868,6 +989,7 @@ export function mySqlDrizzleAdapter(
868
989
  country,
869
990
  usedCouponId,
870
991
  organizationId,
992
+ attributionSnapshot,
871
993
  } = options
872
994
 
873
995
  const existingMerchantCharge = merchantChargeSchema.nullable().parse(
@@ -931,6 +1053,7 @@ export function mySqlDrizzleAdapter(
931
1053
  and(
932
1054
  eq(coupon.restrictedToProductId, productId),
933
1055
  eq(purchaseTable.userId, userId),
1056
+ eq(purchaseTable.status, 'Valid'),
934
1057
  ),
935
1058
  )
936
1059
  .then((res) => {
@@ -939,26 +1062,48 @@ export function mySqlDrizzleAdapter(
939
1062
  )
940
1063
 
941
1064
  const isBulkPurchase =
942
- quantity > 1 ||
943
- Boolean(existingBulkCoupon) ||
944
- options.bulk ||
945
- Boolean(existingPurchase?.status === 'Valid')
1065
+ quantity > 1 || Boolean(existingBulkCoupon) || Boolean(options.bulk)
946
1066
 
947
1067
  let bulkCouponId: string | null = null
948
1068
  let couponToUpdate = null
1069
+ const purchaseBenefitsTransport =
1070
+ options.purchaseBenefitsTransport?.trim() || undefined
1071
+ const purchaseBenefitExpansion = purchaseBenefitsTransport
1072
+ ? parseAndExpandPurchaseBenefitTransport({
1073
+ transport: purchaseBenefitsTransport,
1074
+ appliesTo: isBulkPurchase ? 'redeemed_team_seat' : 'buyer',
1075
+ })
1076
+ : null
1077
+
1078
+ if (purchaseBenefitExpansion && !purchaseBenefitExpansion.success) {
1079
+ logger.info('purchase_benefits.transport.needs_review', {
1080
+ checkoutSessionId,
1081
+ event: 'purchase_benefits.transport.needs_review',
1082
+ metadataKey: PURCHASE_BENEFIT_METADATA_KEY,
1083
+ productId,
1084
+ stripeChargeId,
1085
+ userId,
1086
+ errors: purchaseBenefitExpansion.review.errors,
1087
+ })
1088
+ }
949
1089
 
950
1090
  if (isBulkPurchase) {
951
1091
  bulkCouponId =
952
1092
  existingBulkCoupon !== null ? existingBulkCoupon.id : v4()
953
1093
 
954
1094
  if (existingBulkCoupon !== null) {
1095
+ const existingCouponFields = existingBulkCoupon.fields ?? {}
955
1096
  couponToUpdate = await client
956
1097
  .update(coupon)
957
1098
  .set({
958
1099
  maxUses: (existingBulkCoupon?.maxUses || 0) + quantity,
1100
+ fields: mergePurchaseBenefitFields(
1101
+ existingCouponFields,
1102
+ purchaseBenefitExpansion,
1103
+ ),
959
1104
  ...(organizationId ? { organizationId } : {}),
960
1105
  })
961
- .where(eq(coupon.id, bulkCouponId))
1106
+ .where(eq(coupon.id, bulkCouponId as string))
962
1107
  } else {
963
1108
  // Try to find merchant coupon by stripeCouponId first
964
1109
  let merchantCouponToUse = stripeCouponId
@@ -994,6 +1139,10 @@ export function mySqlDrizzleAdapter(
994
1139
  restrictedToProductId: productId,
995
1140
  maxUses: quantity,
996
1141
  status: 1,
1142
+ fields: mergePurchaseBenefitFields(
1143
+ {},
1144
+ purchaseBenefitExpansion,
1145
+ ),
997
1146
  ...(organizationId ? { organizationId } : {}),
998
1147
  ...(merchantCouponToUse
999
1148
  ? {
@@ -1047,6 +1196,14 @@ export function mySqlDrizzleAdapter(
1047
1196
  bulkCouponId,
1048
1197
  merchantSessionId,
1049
1198
  country,
1199
+ fields: {
1200
+ ...(!isBulkPurchase
1201
+ ? mergePurchaseBenefitFields({}, purchaseBenefitExpansion)
1202
+ : {}),
1203
+ ...(attributionSnapshot
1204
+ ? { attribution: attributionSnapshot }
1205
+ : {}),
1206
+ },
1050
1207
  upgradedFromId: upgradedFromPurchaseId || null,
1051
1208
  couponId: usedCouponId || null,
1052
1209
  ...(organizationId ? { organizationId } : {}),
@@ -2535,7 +2692,12 @@ export function mySqlDrizzleAdapter(
2535
2692
  completedAt: lessonProgress.completedAt ? null : now,
2536
2693
  updatedAt: now,
2537
2694
  })
2538
- .where(eq(resourceProgress.resourceId, options.lessonId))
2695
+ .where(
2696
+ and(
2697
+ eq(resourceProgress.userId, options.userId),
2698
+ eq(resourceProgress.resourceId, options.lessonId),
2699
+ ),
2700
+ )
2539
2701
  } else {
2540
2702
  await client.insert(resourceProgress).values({
2541
2703
  userId: options.userId,
@@ -2765,7 +2927,13 @@ export function mySqlDrizzleAdapter(
2765
2927
  },
2766
2928
  })
2767
2929
 
2768
- const parentResource = ContentResourceSchema.parse(parentResourceData)
2930
+ const parsedParentResource =
2931
+ ContentResourceSchema.safeParse(parentResourceData)
2932
+ if (!parsedParentResource.success) {
2933
+ return null
2934
+ }
2935
+
2936
+ const parentResource = parsedParentResource.data
2769
2937
 
2770
2938
  await client.insert(contentResourceResource).values({
2771
2939
  resourceOfId: parentResource.id,
@@ -2773,15 +2941,17 @@ export function mySqlDrizzleAdapter(
2773
2941
  position: parentResource.resources?.length || 0,
2774
2942
  })
2775
2943
 
2776
- const resourceJoin = client.query.contentResourceResource.findFirst({
2777
- where: and(
2778
- eq(contentResourceResource.resourceOfId, parentResourceId),
2779
- eq(contentResourceResource.resourceId, childResourceId),
2780
- ),
2781
- with: {
2782
- resource: true,
2944
+ const resourceJoin = await client.query.contentResourceResource.findFirst(
2945
+ {
2946
+ where: and(
2947
+ eq(contentResourceResource.resourceOfId, parentResource.id),
2948
+ eq(contentResourceResource.resourceId, childResourceId),
2949
+ ),
2950
+ with: {
2951
+ resource: true,
2952
+ },
2783
2953
  },
2784
- })
2954
+ )
2785
2955
 
2786
2956
  const parsedResourceJoin =
2787
2957
  ContentResourceResourceSchema.safeParse(resourceJoin)
@@ -2794,12 +2964,33 @@ export function mySqlDrizzleAdapter(
2794
2964
  async removeResourceFromResource(options) {
2795
2965
  const { childResourceId, parentResourceId } = options
2796
2966
 
2967
+ const parentResourceData = await client.query.contentResource.findFirst({
2968
+ where: or(
2969
+ eq(
2970
+ sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
2971
+ parentResourceId,
2972
+ ),
2973
+ eq(contentResource.id, parentResourceId),
2974
+ ),
2975
+ })
2976
+
2977
+ const parsedParentResource =
2978
+ ContentResourceSchema.safeParse(parentResourceData)
2979
+ if (!parsedParentResource.success) {
2980
+ return null
2981
+ }
2982
+
2983
+ const parentResource = parsedParentResource.data
2984
+
2797
2985
  const resourceJoin = await client.query.contentResourceResource.findFirst(
2798
2986
  {
2799
2987
  where: and(
2800
- eq(contentResourceResource.resourceOfId, parentResourceId),
2988
+ eq(contentResourceResource.resourceOfId, parentResource.id),
2801
2989
  eq(contentResourceResource.resourceId, childResourceId),
2802
2990
  ),
2991
+ with: {
2992
+ resource: true,
2993
+ },
2803
2994
  },
2804
2995
  )
2805
2996
 
@@ -2813,7 +3004,7 @@ export function mySqlDrizzleAdapter(
2813
3004
  .delete(contentResourceResource)
2814
3005
  .where(
2815
3006
  and(
2816
- eq(contentResourceResource.resourceOfId, parentResourceId),
3007
+ eq(contentResourceResource.resourceOfId, parentResource.id),
2817
3008
  eq(contentResourceResource.resourceId, childResourceId),
2818
3009
  ),
2819
3010
  )
@@ -2883,7 +3074,8 @@ export function mySqlDrizzleAdapter(
2883
3074
  JSON_EXTRACT (${contentResource.fields}, "$.duration") AS duration,
2884
3075
  JSON_EXTRACT (${contentResource.fields}, "$.muxPlaybackId") AS muxPlaybackId,
2885
3076
  JSON_EXTRACT (${contentResource.fields}, "$.muxAssetId") AS muxAssetId,
2886
- JSON_EXTRACT (${contentResource.fields}, "$.transcript") AS transcript
3077
+ JSON_EXTRACT (${contentResource.fields}, "$.transcript") AS transcript,
3078
+ JSON_EXTRACT (${contentResource.fields}, "$.chapters") AS chapters
2887
3079
  FROM
2888
3080
  ${contentResource}
2889
3081
  WHERE
@@ -4209,9 +4401,12 @@ export function mySqlDrizzleAdapter(
4209
4401
  }
4210
4402
 
4211
4403
  const codesArray: string[] = []
4404
+ const requestedQuantity = Number(input.quantity)
4405
+ const couponQuantity = requestedQuantity < 0 ? 1 : requestedQuantity
4406
+
4212
4407
  await client.transaction(async (trx) => {
4213
4408
  // insert coupon for CouponInput quantity
4214
- for (let i = 0; i < Number(input.quantity); i++) {
4409
+ for (let i = 0; i < couponQuantity; i++) {
4215
4410
  const id = `coupon_${guid()}`
4216
4411
  await trx.insert(coupon).values({
4217
4412
  ...input,
@@ -4229,30 +4424,9 @@ export function mySqlDrizzleAdapter(
4229
4424
  try {
4230
4425
  await client.insert(users).values({ ...data, id })
4231
4426
 
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,
4427
+ await provisionPersonalOrganization(adapter, {
4244
4428
  userId: id,
4245
- invitedById: id,
4246
- })
4247
-
4248
- if (!membership) {
4249
- throw new Error('Failed to add user to personal organization')
4250
- }
4251
-
4252
- await adapter.addRoleForMember({
4253
- organizationId: personalOrganization.id,
4254
- memberId: membership.id,
4255
- role: 'owner',
4429
+ email: data.email,
4256
4430
  })
4257
4431
 
4258
4432
  const user = await adapter.getUser?.(id)
@@ -4296,7 +4470,7 @@ export function mySqlDrizzleAdapter(
4296
4470
  .select()
4297
4471
  .from(sessions)
4298
4472
  .where(eq(sessions.sessionToken, data.sessionToken))
4299
- .then((res) => res[0] as AdapterSession)
4473
+ .then((res) => res[0] as CourseBuilderAuthSession)
4300
4474
  },
4301
4475
  async getSessionAndUser(data) {
4302
4476
  return (
@@ -4322,7 +4496,7 @@ export function mySqlDrizzleAdapter(
4322
4496
  .select()
4323
4497
  .from(users)
4324
4498
  .where(eq(users.id, data.id))
4325
- .then((res) => res[0] as AdapterUser)
4499
+ .then((res) => res[0] as CourseBuilderUser)
4326
4500
  },
4327
4501
  async updateSession(data) {
4328
4502
  await client
@@ -1,4 +1,3 @@
1
- import { AdapterAccount } from '@auth/core/adapters'
2
1
  import { relations } from 'drizzle-orm'
3
2
  import {
4
3
  index,
@@ -9,6 +8,8 @@ import {
9
8
  varchar,
10
9
  } from 'drizzle-orm/mysql-core'
11
10
 
11
+ import type { CourseBuilderAccountType } from '@coursebuilder/core/auth'
12
+
12
13
  import { getUsersSchema, type UsersSchemaOverrides } from './users.js'
13
14
 
14
15
  export function getAccountsSchema(mysqlTable: MySqlTableFn) {
@@ -17,7 +18,7 @@ export function getAccountsSchema(mysqlTable: MySqlTableFn) {
17
18
  {
18
19
  userId: varchar('userId', { length: 255 }).notNull(),
19
20
  type: varchar('type', { length: 255 })
20
- .$type<AdapterAccount['type']>()
21
+ .$type<CourseBuilderAccountType>()
21
22
  .notNull(),
22
23
  provider: varchar('provider', { length: 255 }).notNull(),
23
24
  providerAccountId: varchar('providerAccountId', {
@@ -0,0 +1,107 @@
1
+ import { relations, sql } from 'drizzle-orm'
2
+ import {
3
+ index,
4
+ json,
5
+ MySqlTableFn,
6
+ primaryKey,
7
+ timestamp,
8
+ uniqueIndex,
9
+ varchar,
10
+ } from 'drizzle-orm/mysql-core'
11
+
12
+ import { getUsersSchema, type UsersSchemaOverrides } from './users.js'
13
+
14
+ /**
15
+ * Creates the PersonalAccessToken table schema for operator API credentials.
16
+ *
17
+ * Stores safe token metadata and a derived token hash, never the raw token
18
+ * secret. Key fields are `publicId` and `tokenPrefix` for lookup/display,
19
+ * `tokenHash` for verification, `scopes` for coarse authorization, and
20
+ * `expiresAt`, `lastUsedAt`, and `revokedAt` for lifecycle control.
21
+ *
22
+ * @param mysqlTable - Drizzle MySQL table factory, usually app-prefixed.
23
+ * @returns The PersonalAccessToken MySQL table schema.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const personalAccessToken = getPersonalAccessTokenSchema(mysqlTable)
28
+ * ```
29
+ */
30
+ export function getPersonalAccessTokenSchema(mysqlTable: MySqlTableFn) {
31
+ return mysqlTable(
32
+ 'PersonalAccessToken',
33
+ {
34
+ id: varchar('id', { length: 191 }).notNull(),
35
+ userId: varchar('userId', { length: 191 }).notNull(),
36
+ name: varchar('name', { length: 255 }).notNull(),
37
+ publicId: varchar('publicId', { length: 64 }).notNull(),
38
+ tokenPrefix: varchar('tokenPrefix', { length: 128 }).notNull(),
39
+ tokenHash: varchar('tokenHash', { length: 128 }).notNull(),
40
+ scopes: json('scopes').$type<string[]>().notNull(),
41
+ expiresAt: timestamp('expiresAt', {
42
+ mode: 'date',
43
+ fsp: 3,
44
+ }),
45
+ lastUsedAt: timestamp('lastUsedAt', {
46
+ mode: 'date',
47
+ fsp: 3,
48
+ }),
49
+ revokedAt: timestamp('revokedAt', {
50
+ mode: 'date',
51
+ fsp: 3,
52
+ }),
53
+ createdAt: timestamp('createdAt', {
54
+ mode: 'date',
55
+ fsp: 3,
56
+ }).default(sql`CURRENT_TIMESTAMP(3)`),
57
+ updatedAt: timestamp('updatedAt', {
58
+ mode: 'date',
59
+ fsp: 3,
60
+ })
61
+ .default(sql`CURRENT_TIMESTAMP(3)`)
62
+ .$onUpdate(() => new Date()),
63
+ },
64
+ (pat) => ({
65
+ pk: primaryKey({ columns: [pat.id] }),
66
+ publicIdIdx: uniqueIndex('publicId_idx').on(pat.publicId),
67
+ tokenHashIdx: uniqueIndex('tokenHash_idx').on(pat.tokenHash),
68
+ userIdIdx: index('userId_idx').on(pat.userId),
69
+ expiresAtIdx: index('expiresAt_idx').on(pat.expiresAt),
70
+ revokedAtIdx: index('revokedAt_idx').on(pat.revokedAt),
71
+ }),
72
+ )
73
+ }
74
+
75
+ /**
76
+ * Creates PersonalAccessToken relations for the Users table.
77
+ *
78
+ * Links `personalAccessToken.userId` to `user.id` with relation name `user`.
79
+ * Uses `getPersonalAccessTokenSchema` and either the default `getUsersSchema`
80
+ * helper or `overrides.getUsersSchema` when an app needs a custom user schema.
81
+ *
82
+ * @param mysqlTable - Drizzle MySQL table factory, usually app-prefixed.
83
+ * @param overrides - Optional schema overrides, especially `getUsersSchema`.
84
+ * @returns A Drizzle relations schema for PersonalAccessToken.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const relations = getPersonalAccessTokenRelationsSchema(mysqlTable, {
89
+ * getUsersSchema: customGetUsersSchema,
90
+ * })
91
+ * ```
92
+ */
93
+ export function getPersonalAccessTokenRelationsSchema(
94
+ mysqlTable: MySqlTableFn,
95
+ overrides: UsersSchemaOverrides = {},
96
+ ) {
97
+ const personalAccessToken = getPersonalAccessTokenSchema(mysqlTable)
98
+ const resolveUsersSchema = overrides.getUsersSchema ?? getUsersSchema
99
+ const user = resolveUsersSchema(mysqlTable)
100
+ return relations(personalAccessToken, ({ one }) => ({
101
+ user: one(user, {
102
+ fields: [personalAccessToken.userId],
103
+ references: [user.id],
104
+ relationName: 'user',
105
+ }),
106
+ }))
107
+ }
@@ -62,6 +62,9 @@ export function getPurchaseSchema(mysqlTable: MySqlTableFn) {
62
62
  organizationMembershipIdIdx: index('organizationMembershipId_idx').on(
63
63
  table.purchasedByorganizationMembershipId,
64
64
  ),
65
+ userStatusProductIdx: index(
66
+ 'idx_Purchase_on_userId_status_productId',
67
+ ).on(table.userId, table.status, table.productId),
65
68
  }
66
69
  },
67
70
  )
@@ -39,6 +39,9 @@ export function getEntitlementsSchema(mysqlTable: MySqlTableFn) {
39
39
  orgIdIdx: index('orgId_idx').on(table.organizationId),
40
40
  sourceIdx: index('source_idx').on(table.sourceType, table.sourceId),
41
41
  typeIdx: index('type_idx').on(table.entitlementType),
42
+ userIdTypeDeletedAtIdx: index(
43
+ 'idx_Entitlement_on_userId_type_deletedAt',
44
+ ).on(table.userId, table.entitlementType, table.deletedAt),
42
45
  }),
43
46
  )
44
47
  }