@coursebuilder/adapter-drizzle 1.0.7 → 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.
- package/dist/authjs.d.ts +27 -0
- package/dist/authjs.js +1 -0
- package/dist/chunk-3XHKEHGF.js +88 -0
- package/dist/{chunk-ENIKVP6O.js → chunk-4SMVVOU5.js} +2 -2
- package/dist/{chunk-EJ7UDN4T.js → chunk-52A4W4YI.js} +2 -2
- package/dist/{chunk-3RQH7IPC.js → chunk-6VDGZ4KA.js} +2 -2
- package/dist/{chunk-JX53ECWI.js → chunk-DB473RZJ.js} +13 -12
- package/dist/{chunk-O4PMFTPJ.js → chunk-DHII2GRW.js} +2 -2
- package/dist/{chunk-P76GFFVH.js → chunk-Q5NZ5UFZ.js} +2 -2
- package/dist/{chunk-5WMUBNWJ.js → chunk-QDWLBCD3.js} +2 -2
- package/dist/{chunk-MQSLKVLJ.js → chunk-RDAVEULA.js} +2 -2
- package/dist/{chunk-DP6YPJZ4.js → chunk-ULJCXOOM.js} +634 -197
- package/dist/{chunk-U6XVAMCK.js → chunk-WPDVDHLH.js} +4 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +15 -14
- package/dist/lib/mysql/index.cjs +1068 -557
- package/dist/lib/mysql/index.cjs.map +1 -1
- package/dist/lib/mysql/index.d.cts +412 -6
- package/dist/lib/mysql/index.d.ts +412 -6
- package/dist/lib/mysql/index.js +15 -14
- package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/accounts.d.cts +2 -2
- package/dist/lib/mysql/schemas/auth/accounts.d.ts +2 -2
- package/dist/lib/mysql/schemas/auth/accounts.js +3 -3
- package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/device-access-token.js +4 -4
- package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/device-verification.js +4 -4
- package/dist/lib/mysql/schemas/auth/personal-access-token.cjs +288 -0
- package/dist/lib/mysql/schemas/auth/personal-access-token.cjs.map +1 -0
- package/dist/lib/mysql/schemas/auth/personal-access-token.d.cts +243 -0
- package/dist/lib/mysql/schemas/auth/personal-access-token.d.ts +243 -0
- package/dist/lib/mysql/schemas/auth/personal-access-token.js +21 -0
- package/dist/lib/mysql/schemas/auth/personal-access-token.js.map +1 -0
- package/dist/lib/mysql/schemas/auth/profiles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/profiles.js +3 -3
- package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/role-permissions.js +4 -4
- package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/roles.js +3 -3
- package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/sessions.js +4 -4
- package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-permissions.js +3 -3
- package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-prefs.js +3 -3
- package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-roles.js +3 -3
- package/dist/lib/mysql/schemas/auth/users.cjs +2 -1
- package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/users.js +3 -3
- package/dist/lib/mysql/schemas/commerce/coupon.cjs +2 -1
- package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/coupon.js +3 -3
- package/dist/lib/mysql/schemas/commerce/merchant-charge.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/merchant-charge.js +3 -3
- package/dist/lib/mysql/schemas/commerce/merchant-subscription.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/merchant-subscription.js +3 -3
- package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/price.js +3 -3
- package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/product.js +3 -3
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs +2 -1
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +4 -4
- package/dist/lib/mysql/schemas/commerce/purchase.cjs +2 -1
- package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/purchase.js +3 -3
- package/dist/lib/mysql/schemas/commerce/subscription.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/subscription.js +3 -3
- package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/upgradable-products.js +4 -4
- package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/comment.js +3 -3
- package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/communication-preferences.js +3 -3
- package/dist/lib/mysql/schemas/communication/question-response.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/question-response.js +4 -4
- package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-contributions.js +3 -3
- package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-product.js +3 -3
- package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-resource.js +3 -3
- package/dist/lib/mysql/schemas/content/content-resource-tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-tag.js +3 -3
- package/dist/lib/mysql/schemas/content/content-resource-version.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-version.js +3 -3
- package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource.js +3 -3
- package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/tag-tag.js +3 -3
- package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/tag.js +3 -3
- package/dist/lib/mysql/schemas/entitlements/entitlement.cjs +2 -1
- package/dist/lib/mysql/schemas/entitlements/entitlement.cjs.map +1 -1
- package/dist/lib/mysql/schemas/entitlements/entitlement.js +4 -4
- package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organization-membership-roles.js +3 -3
- package/dist/lib/mysql/schemas/org/organization-memberships.cjs +2 -1
- package/dist/lib/mysql/schemas/org/organization-memberships.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organization-memberships.js +3 -3
- package/dist/lib/mysql/schemas/org/organizations.cjs +2 -1
- package/dist/lib/mysql/schemas/org/organizations.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organizations.js +3 -3
- package/dist/lib/utils.d.cts +3 -1
- package/dist/lib/utils.d.ts +3 -1
- package/package.json +9 -8
- package/src/authjs.ts +34 -0
- package/src/index.ts +2 -2
- package/src/lib/mysql/index.ts +224 -50
- package/src/lib/mysql/schemas/auth/accounts.ts +3 -2
- package/src/lib/mysql/schemas/auth/personal-access-token.ts +107 -0
- package/src/lib/mysql/schemas/commerce/purchase.ts +3 -0
- package/src/lib/mysql/schemas/entitlements/entitlement.ts +3 -0
- package/dist/chunk-6XFVP2Q4.js +0 -101
- package/dist/chunk-7223HR2E.js +0 -112
- package/dist/chunk-HEQ5MII4.js +0 -12764
- package/dist/chunk-LV3LIBNU.js +0 -12764
- package/dist/chunk-MUFYDXA7.js +0 -2143
- package/dist/chunk-OK4NHV6Z.js +0 -58
- package/dist/chunk-SUOPB24V.js +0 -78
- package/dist/chunk-UIQCNODZ.js +0 -100
- package/dist/chunk-XKCPXM63.js +0 -79
- package/dist/chunk-XM5PWOCV.js +0 -63
- package/dist/chunk-YMVONZZW.js +0 -49
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
|
-
):
|
|
21
|
+
): AuthJsCourseBuilderAdapter {
|
|
22
22
|
if (is(db, MySqlDatabase)) {
|
|
23
23
|
return mySqlDrizzleAdapter(
|
|
24
24
|
db,
|
package/src/lib/mysql/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
):
|
|
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
|
|
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)
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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,
|
|
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,
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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<
|
|
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
|
}
|