diff --git a/apps/egghead/src/inngest/functions/sanity/create-course-in-sanity.ts b/apps/egghead/src/inngest/functions/sanity/create-course-in-sanity.ts new file mode 100644 index 000000000..eebbe1b3c --- /dev/null +++ b/apps/egghead/src/inngest/functions/sanity/create-course-in-sanity.ts @@ -0,0 +1,71 @@ +import { POST_CREATED_EVENT } from '@/inngest/events/post-created' +import { inngest } from '@/inngest/inngest.server' +import { SanityCourseSchema } from '@/lib/sanity-content' +import { createCourse, getSanityCollaborator } from '@/lib/sanity-content-query' +import { loadEggheadInstructorForUser } from '@/lib/users' +import { NonRetriableError } from 'inngest' + +export const createCourseInSanity = inngest.createFunction( + { + id: 'create-course-in-sanity', + name: 'Create Course in Sanity', + }, + { + 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 contributor = await step.run('get-contributor', async () => { + const instructor = await loadEggheadInstructorForUser(post.createdById) + + if (!instructor) { + return null + } + + const contributor = await getSanityCollaborator(instructor.id) + + return contributor + }) + + const sanityCourse = await step.run('create-course', async () => { + const { fields } = post + + 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, + }) + + if (!coursePayload.success) { + throw new NonRetriableError('Failed to create course in sanity', { + cause: coursePayload.error.flatten().fieldErrors, + }) + } + + const course = await createCourse(coursePayload.data) + + return course + }) + }, +) diff --git a/apps/egghead/src/inngest/functions/sanity/sync-lesson-to-sanity.ts b/apps/egghead/src/inngest/functions/sanity/sync-lesson-to-sanity.ts index 35396df71..d42da6c27 100644 --- a/apps/egghead/src/inngest/functions/sanity/sync-lesson-to-sanity.ts +++ b/apps/egghead/src/inngest/functions/sanity/sync-lesson-to-sanity.ts @@ -3,13 +3,12 @@ import { inngest } from '@/inngest/inngest.server' import { eggheadLessonSchema, getEggheadLesson } from '@/lib/egghead' import type { EggheadLesson } from '@/lib/egghead' import { - sanityReferenceSchema, - sanityVersionedSoftwareLibraryObjectSchema, + SanityReferenceSchema, + SoftwareLibraryArrayObjectSchema, } from '@/lib/sanity-content' import type { - SanityCollaboratorDocument, SanityReference, - SanityVersionedSoftwareLibraryObject, + SoftwareLibraryArrayObject, } from '@/lib/sanity-content' import { createSanityLesson, @@ -36,7 +35,7 @@ export const syncLessonToSanity = inngest.createFunction( try { return await Promise.all( lesson.topic_list.map(async (library: string) => { - return sanityVersionedSoftwareLibraryObjectSchema.parse( + return SoftwareLibraryArrayObjectSchema.parse( await getSanitySoftwareLibrary(library), ) }), @@ -46,12 +45,12 @@ export const syncLessonToSanity = inngest.createFunction( return [] } }, - )) as SanityVersionedSoftwareLibraryObject[] + )) as SoftwareLibraryArrayObject[] const sanityCollaboratorReferenceObject = (await step.run( 'Get collaborator', async () => { - return sanityReferenceSchema?.parse( + return SanityReferenceSchema?.parse( await getSanityCollaborator(lesson.instructor.id), ) }, diff --git a/apps/egghead/src/inngest/inngest.config.ts b/apps/egghead/src/inngest/inngest.config.ts index 9b4fb683f..242325326 100644 --- a/apps/egghead/src/inngest/inngest.config.ts +++ b/apps/egghead/src/inngest/inngest.config.ts @@ -6,6 +6,7 @@ import { courseBuilderCoreFunctions } from '@coursebuilder/core/inngest' import { instructorInviteCreated } from './functions/instructor-invite-created' import { migrateTipsToPosts } from './functions/migrate-tips-to-posts' import { notifySlack } from './functions/notify-slack-for-post' +import { createCourseInSanity } from './functions/sanity/create-course-in-sanity' import { syncLessonToSanity } from './functions/sanity/sync-lesson-to-sanity' import { syncVideoResourceToSanity } from './functions/sanity/sync-video-resource-to-sanity' import { syncPostToEgghead } from './functions/sync-post-to-egghead' @@ -26,6 +27,7 @@ export const inngestConfig = { notifySlack, syncVideoResourceData, syncPostsToEggheadLessons, + createCourseInSanity, instructorInviteCreated, ], } diff --git a/apps/egghead/src/lib/posts-query.ts b/apps/egghead/src/lib/posts-query.ts index 2cc7d2b5d..32f02eb6c 100644 --- a/apps/egghead/src/lib/posts-query.ts +++ b/apps/egghead/src/lib/posts-query.ts @@ -51,7 +51,7 @@ import { updateEggheadLesson, writeLegacyTaggingsToEgghead, } from './egghead' -import { sanityLessonDocumentSchema } from './sanity-content' +import { SanityLessonDocumentSchema } from './sanity-content' import { replaceSanityLessonResources, updateSanityLesson, @@ -373,7 +373,7 @@ export async function writeNewPostToDatabase(input: { : null if (eggheadLessonId) { - const lesson = sanityLessonDocumentSchema.parse({ + const lesson = SanityLessonDocumentSchema.parse({ _id: `lesson-${eggheadLessonId}`, _type: 'lesson', title, diff --git a/apps/egghead/src/lib/sanity-content-query.ts b/apps/egghead/src/lib/sanity-content-query.ts index e0c97ac75..f55913520 100644 --- a/apps/egghead/src/lib/sanity-content-query.ts +++ b/apps/egghead/src/lib/sanity-content-query.ts @@ -15,18 +15,19 @@ import { import { createSanityReference, keyGenerator, - sanityCollaboratorDocumentSchema, - sanityLessonDocumentSchema, - sanityReferenceSchema, + SanityCollaboratorSchema, + SanityLessonDocumentSchema, + SanityReferenceSchema, sanitySoftwareLibraryDocumentSchema, - sanityVersionedSoftwareLibraryObjectSchema, - sanityVideoResourceDocumentSchema, + SanityVideoResourceDocumentSchema, + SoftwareLibraryArrayObjectSchema, } from '@/lib/sanity-content' import type { - SanityCollaboratorDocument, + SanityCollaborator, + SanityCourse, SanityReference, SanitySoftwareLibraryDocument, - SanityVersionedSoftwareLibraryObject, + SoftwareLibraryArrayObject, } from '@/lib/sanity-content' import { sanityWriteClient } from '@/server/sanity-write-client' import { asc, eq, sql } from 'drizzle-orm' @@ -42,7 +43,7 @@ export async function createSanityVideoResource(videoResource: VideoResource) { const streamUrl = muxPlaybackId && `https://stream.mux.com/${muxPlaybackId}.m3u8` - const body = sanityVideoResourceDocumentSchema.parse({ + const body = SanityVideoResourceDocumentSchema.parse({ _type: 'videoResource', filename: id, muxAsset: { @@ -224,9 +225,9 @@ export async function updateSanityLesson( const softwareLibraries = await Promise.all( eggheadLesson.topic_list.map(async (library: string) => { console.log('process topic library', library) - return sanityVersionedSoftwareLibraryObjectSchema - .nullable() - .parse(await getSanitySoftwareLibrary(library)) + return SoftwareLibraryArrayObjectSchema.nullable().parse( + await getSanitySoftwareLibrary(library), + ) }), ) @@ -234,7 +235,7 @@ export async function updateSanityLesson( eggheadLesson.instructor.id, ) - let collaborator = sanityReferenceSchema.nullable().parse(collaboratorData) + let collaborator = SanityReferenceSchema.nullable().parse(collaboratorData) if (!collaborator) { const user = await db.query.users.findFirst({ @@ -264,9 +265,9 @@ export async function updateSanityLesson( await syncInstructorToSanity(eggheadUserProfile) - collaborator = sanityReferenceSchema - .nullable() - .parse(await getSanityCollaborator(eggheadLesson.instructor.id)) + collaborator = SanityReferenceSchema.nullable().parse( + await getSanityCollaborator(eggheadLesson.instructor.id), + ) if (!collaborator) { throw new Error( @@ -299,7 +300,7 @@ export async function updateSanityLesson( export async function createSanityLesson( eggheadLesson: EggheadLesson, collaborator: SanityReference, - softwareLibraries: SanityVersionedSoftwareLibraryObject[], + softwareLibraries: SoftwareLibraryArrayObject[], ) { const post = PostSchema.nullable().parse( await db.query.contentResource.findFirst({ @@ -336,7 +337,7 @@ export async function createSanityLesson( return updateSanityLesson(eggheadLesson.id, post) } - const lesson = sanityLessonDocumentSchema.parse({ + const lesson = SanityLessonDocumentSchema.parse({ _id: `lesson-${eggheadLesson.id}`, _type: 'lesson', title: eggheadLesson.title, @@ -363,18 +364,17 @@ export async function createSanityLesson( export async function getSanityCollaborator( instructorId: number, - role: SanityCollaboratorDocument['role'] = 'instructor', + role: SanityCollaborator['role'] = 'instructor', returnReference = true, ) { const collaboratorData = await sanityWriteClient.fetch( `*[_type == "collaborator" && eggheadInstructorId == "${instructorId}" && role == "${role}"][0]`, ) - const collaborator = sanityCollaboratorDocumentSchema - .nullable() - .parse(collaboratorData) + const collaborator = + SanityCollaboratorSchema.nullable().parse(collaboratorData) - if (!collaborator) return null + if (!collaborator || !collaborator._id) return null return returnReference ? createSanityReference(collaborator._id) @@ -405,3 +405,10 @@ export async function getSanitySoftwareLibrary( }, } } + +export const createCourse = async (course: Partial) => { + return await sanityWriteClient.create({ + _type: 'course', + ...course, + }) +} diff --git a/apps/egghead/src/lib/sanity-content.ts b/apps/egghead/src/lib/sanity-content.ts index addedc92a..34a11eafe 100644 --- a/apps/egghead/src/lib/sanity-content.ts +++ b/apps/egghead/src/lib/sanity-content.ts @@ -1,5 +1,35 @@ import { z } from 'zod' +export const ImageSchema = z.object({ + _type: z.string().optional(), + label: z.string().optional(), + url: z.string().optional(), + _key: z.string().optional(), +}) +export type Image = z.infer + +export const SlugSchema = z.object({ + current: z.string().optional(), + _type: z.string().optional(), +}) +export type Slug = z.infer + +export const SystemFieldsSchema = z.object({ + _id: z.string().optional(), + _type: z.string().optional(), + _rev: z.string().optional(), + _createdAt: z.coerce.date().optional(), + _updatedAt: z.coerce.date().optional(), +}) +export type SystemFields = z.infer + +export const ReferenceSchema = z.object({ + _ref: z.string().optional(), + _type: z.string().optional(), + _key: z.string().optional(), +}) +export type Reference = z.infer + export const sanitySoftwareLibraryDocumentSchema = z.object({ _type: z.literal('software-library'), _id: z.string(), @@ -12,37 +42,22 @@ export type SanitySoftwareLibraryDocument = z.infer< typeof sanitySoftwareLibraryDocumentSchema > -export const sanityVersionedSoftwareLibraryObjectSchema = z.object({ - _type: z.literal('versioned-software-library'), +export const SanityReferenceSchema = z.object({ + _type: z.literal('reference'), _key: z.string(), - library: z.object({ - _type: z.literal('reference'), - _ref: z.string(), - }), + _ref: z.string(), }) -export type SanityVersionedSoftwareLibraryObject = z.infer< - typeof sanityVersionedSoftwareLibraryObjectSchema -> - -export const sanityCollaboratorDocumentSchema = z.object({ - _type: z.literal('collaborator'), - role: z.enum(['instructor', 'staff', 'illustrator']), - _id: z.string(), - eggheadInstructorId: z.string(), +export const SoftwareLibraryArrayObjectSchema = z.object({ + _type: z.string().optional(), + _key: z.string().optional(), + library: ReferenceSchema.optional(), }) - -export type SanityCollaboratorDocument = z.infer< - typeof sanityCollaboratorDocumentSchema +export type SoftwareLibraryArrayObject = z.infer< + typeof SoftwareLibraryArrayObjectSchema > -export const sanityReferenceSchema = z.object({ - _type: z.literal('reference'), - _key: z.string(), - _ref: z.string(), -}) - -export type SanityReference = z.infer +export type SanityReference = z.infer export function createSanityReference(documentId: string): SanityReference { return { @@ -52,7 +67,7 @@ export function createSanityReference(documentId: string): SanityReference { } } -export const sanityVideoResourceDocumentSchema = z.object({ +export const SanityVideoResourceDocumentSchema = z.object({ _createdAt: z.string().datetime().nullish(), _id: z.string().nullish(), _rev: z.string().nullish(), @@ -78,10 +93,10 @@ export const sanityVideoResourceDocumentSchema = z.object({ }) export type SanityVideoResourceDocument = z.infer< - typeof sanityVideoResourceDocumentSchema + typeof SanityVideoResourceDocumentSchema > -export const sanityLessonDocumentSchema = z.object({ +export const SanityLessonDocumentSchema = z.object({ _type: z.literal('lesson'), _id: z.string().nullish(), title: z.string(), @@ -91,18 +106,51 @@ export const sanityLessonDocumentSchema = z.object({ }), description: z.string().nullish(), railsLessonId: z.string().or(z.number()).nullish(), - softwareLibraries: z - .array(sanityVersionedSoftwareLibraryObjectSchema) - .nullish(), - collaborators: z.array(sanityReferenceSchema).nullish(), + softwareLibraries: z.array(SoftwareLibraryArrayObjectSchema).nullish(), + collaborators: z.array(SanityReferenceSchema).nullish(), status: z.string().nullish(), accessLevel: z.enum(['free', 'pro']).nullish(), }) -export type SanityLessonDocument = z.infer +export type SanityLessonDocument = z.infer export const keyGenerator = () => { return [...Array(12)] .map(() => Math.floor(Math.random() * 16).toString(16)) .join('') } + +export const SanityCollaboratorSchema = z.object({ + ...SystemFieldsSchema.shape, + person: ReferenceSchema.optional(), + title: z.string().optional(), + eggheadInstructorId: z.string().optional(), + role: z.string().optional(), + department: z.string().optional(), +}) +export type SanityCollaborator = z.infer + +export const SanityCourseSchema = z.object({ + ...SystemFieldsSchema.shape, + title: z.string().optional(), + slug: SlugSchema.optional(), + summary: z.string().optional(), + description: z.string().optional(), + image: z.string().optional(), + images: z.array(ImageSchema).optional(), + imageIllustrator: ReferenceSchema.optional(), + accessLevel: z.string().optional(), + searchIndexingState: z.string().optional(), + productionProcessState: z.string().optional(), + railsCourseId: z.number().optional(), + sharedId: z.string().optional(), + softwareLibraries: z.array(SoftwareLibraryArrayObjectSchema).optional(), + collaborators: z + .array(ReferenceSchema) + .optional() + .or(SanityCollaboratorSchema) + .optional(), + resources: z.array(ReferenceSchema).optional(), +}) + +export type SanityCourse = z.infer