-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(egh): Course Creation Sync Flow #350
Changes from 5 commits
0d24f7e
b18c2ff
047a461
d4ea629
4e20a70
9bf6603
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { z } from 'zod' | ||
|
||
export const EGGHEAD_COURSE_CREATED_EVENT = 'egghead/course-created' | ||
|
||
export type EggheadCourseCreated = { | ||
name: typeof EGGHEAD_COURSE_CREATED_EVENT | ||
data: EggheadCourseCreatedEvent | ||
} | ||
export const EggheadCourseCreatedEventSchema = z.object({ | ||
id: z.number(), | ||
}) | ||
|
||
export type EggheadCourseCreatedEvent = z.infer< | ||
typeof EggheadCourseCreatedEventSchema | ||
> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { db } from '@/db' | ||
import { contentResource } from '@/db/schema' | ||
import { EGGHEAD_COURSE_CREATED_EVENT } from '@/inngest/events/egghead/course-created' | ||
import { POST_CREATED_EVENT } from '@/inngest/events/post-created' | ||
import { inngest } from '@/inngest/inngest.server' | ||
import { createEggheadCourse } from '@/lib/egghead' | ||
import { loadEggheadInstructorForUser } from '@/lib/users' | ||
import { eq } from 'drizzle-orm' | ||
import { NonRetriableError } from 'inngest' | ||
|
||
export const createCourseInEgghead = inngest.createFunction( | ||
{ | ||
id: 'create-course-in-egghead', | ||
name: 'Create Course in Egghead', | ||
}, | ||
{ | ||
event: POST_CREATED_EVENT, | ||
}, | ||
async ({ event, step }) => { | ||
const post = await step.run('verify-post', async () => { | ||
const { post } = event.data | ||
if (!post) { | ||
throw new NonRetriableError('Post not found') | ||
} | ||
|
||
if (post.fields?.postType !== 'course') { | ||
throw new NonRetriableError('Post is not a course') | ||
} | ||
|
||
return post | ||
}) | ||
|
||
const instructor = await step.run('get-instructor', async () => { | ||
const instructor = await loadEggheadInstructorForUser(post.createdById) | ||
|
||
if (!instructor) { | ||
return null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could throw the error instead of returning null and probably remove the need for the guard below |
||
} | ||
|
||
return instructor | ||
}) | ||
|
||
if (!instructor) { | ||
throw new NonRetriableError('Instructor not found', { | ||
cause: { | ||
postId: post.id, | ||
instructorId: post.createdById, | ||
}, | ||
}) | ||
} | ||
|
||
const course = await step.run('create-course', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. insider. among this eggheadCourse instead of just course because so many courses |
||
const courseGuid = post.fields?.slug.split('~').pop() | ||
|
||
const course = await createEggheadCourse({ | ||
title: post.fields?.title, | ||
guid: courseGuid, | ||
ownerId: instructor.user_id, | ||
}) | ||
|
||
return course | ||
}) | ||
|
||
step.sendEvent(EGGHEAD_COURSE_CREATED_EVENT, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kind of weird to go through all of the above and then send this different event 🤔 |
||
name: EGGHEAD_COURSE_CREATED_EVENT, | ||
data: { | ||
id: course.id, | ||
}, | ||
}) | ||
|
||
await step.run('sync-course-id-to-builder', async () => { | ||
await db | ||
.update(contentResource) | ||
.set({ | ||
fields: { | ||
...post.fields, | ||
eggheadCourseId: course.id, | ||
}, | ||
}) | ||
.where(eq(contentResource.id, post.id)) | ||
.catch((error) => { | ||
console.error('🚨 Error creating post', error) | ||
throw error | ||
}) | ||
}) | ||
}, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import { EGGHEAD_COURSE_CREATED_EVENT } from '@/inngest/events/egghead/course-created' | ||
import { POST_CREATED_EVENT } from '@/inngest/events/post-created' | ||
import { inngest } from '@/inngest/inngest.server' | ||
import { SanityCourseSchema } from '@/lib/sanity-content' | ||
|
@@ -39,22 +40,38 @@ export const createCourseInSanity = inngest.createFunction( | |
return contributor | ||
}) | ||
|
||
const courseCreatedEvent = await step.waitForEvent( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not super stoked about the way these are connected together and think we might consider tightening it up or making the flow more obvious this is the danger of event driven entropy |
||
`wait for course created in egghead-rails`, | ||
{ | ||
event: EGGHEAD_COURSE_CREATED_EVENT, | ||
timeout: '3 mins', | ||
}, | ||
) | ||
|
||
if (!courseCreatedEvent) { | ||
throw new NonRetriableError('Course not created in egghead') | ||
} | ||
|
||
const courseId = courseCreatedEvent.data.id | ||
|
||
const sanityCourse = await step.run('create-course', async () => { | ||
const { fields } = post | ||
|
||
const courseGuid = fields?.slug.split('~').pop() | ||
|
||
const coursePayload = SanityCourseSchema.safeParse({ | ||
title: fields?.title, | ||
slug: { | ||
current: fields?.slug, | ||
_type: 'slug', | ||
}, | ||
description: fields?.body, | ||
railsCourseId: fields?.railsCourseId, | ||
collaborators: [contributor], | ||
searchIndexingState: 'hidden', | ||
accessLevel: 'pro', | ||
productionProcessState: 'new', | ||
sharedId: post.id, | ||
sharedId: courseGuid, | ||
railsCourseId: courseId, | ||
}) | ||
|
||
if (!coursePayload.success) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,36 @@ export const getCachedEggheadInstructorForUser = unstable_cache( | |
{ revalidate: 3600, tags: ['users'] }, | ||
) | ||
|
||
export const InstructorDbSchema = z.object({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is the db here egghead? let's label it if so |
||
id: z.number(), | ||
user_id: z.number(), | ||
email: z.string(), | ||
avatar_content_type: z.string().nullish(), | ||
avatar_file_name: z.string().nullish(), | ||
avatar_file_size: z.number().nullish(), | ||
avatar_processing: z.boolean().nullish(), | ||
avatar_updated_at: z.coerce.date().nullish(), | ||
bio_short: z.string().nullish(), | ||
contract_id: z.string().nullish(), | ||
created_at: z.coerce.date().nullish(), | ||
first_name: z.string().nullish(), | ||
gear_tracking_number: z.string().nullish(), | ||
internal_note: z.string().nullish(), | ||
last_name: z.string().nullish(), | ||
percentage: z.string().nullish(), | ||
profile_picture_url: z.string().nullish(), | ||
skip_onboarding: z.boolean().nullish(), | ||
slack_group_id: z.string().nullish(), | ||
slack_id: z.string().nullish(), | ||
slug: z.string().nullish(), | ||
state: z.string().nullish(), | ||
trained_by_instructor_id: z.number().nullish(), | ||
twitter: z.string().nullish(), | ||
updated_at: z.coerce.date().nullish(), | ||
website: z.string().nullish(), | ||
}) | ||
export type InstructorDb = z.infer<typeof InstructorDbSchema> | ||
|
||
export const loadEggheadInstructorForUser = async (userId: string) => { | ||
const user = await db.query.users.findFirst({ | ||
where: eq(users.id, userId), | ||
|
@@ -60,11 +90,18 @@ export const loadEggheadInstructorForUser = async (userId: string) => { | |
return null | ||
} | ||
|
||
const instructor = await eggheadPgQuery( | ||
const instructorResult = await eggheadPgQuery( | ||
`SELECT * FROM instructors WHERE user_id = ${eggheadAccount.providerAccountId}`, | ||
) | ||
|
||
return instructor.rows[0] | ||
const instructor = InstructorDbSchema.safeParse(instructorResult.rows[0]) | ||
|
||
if (!instructor.success) { | ||
console.log('instructor.error', instructor.error) | ||
throw new Error('Failed to load instructor from egghead') | ||
} | ||
|
||
return instructor.data | ||
} | ||
|
||
export const loadUsersForRole = async (role: string) => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could be part of the trigger instead of here which imo makes more sense than throwing errors when any other kind of post is created