feat: implement automation feature
This commit is contained in:
@@ -3,6 +3,8 @@ import * as z from 'zod';
|
||||
import { ZCourseMetadata } from '../course/course';
|
||||
|
||||
const ZSupportedLocale = z.enum(['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']);
|
||||
const LESSON_BODY_HTML_DESCRIPTION =
|
||||
'Lesson body HTML only. Do not include the lesson title. Do not use h1 or h2 anywhere in lessonLanguages[].content. Start headings at h3 because that is the highest heading level allowed in lesson content.';
|
||||
|
||||
export const ZCourseImportWarning = z.object({
|
||||
code: z.string().min(1),
|
||||
@@ -12,7 +14,7 @@ export const ZCourseImportWarning = z.object({
|
||||
export type TCourseImportWarning = z.infer<typeof ZCourseImportWarning>;
|
||||
|
||||
export const ZCourseImportSourceReference = z.object({
|
||||
type: z.enum(['prompt', 'pdf']),
|
||||
type: z.enum(['prompt', 'pdf', 'course']),
|
||||
label: z.string().min(1),
|
||||
pageStart: z.number().int().min(1).optional(),
|
||||
pageEnd: z.number().int().min(1).optional()
|
||||
@@ -48,13 +50,14 @@ export type TCourseImportDraftLesson = z.infer<typeof ZCourseImportDraftLesson>;
|
||||
export const ZCourseImportDraftLessonLanguage = z.object({
|
||||
lessonExternalId: z.string().min(1),
|
||||
locale: ZSupportedLocale,
|
||||
content: z.string().min(1)
|
||||
content: z.string().min(1).describe(LESSON_BODY_HTML_DESCRIPTION)
|
||||
});
|
||||
export type TCourseImportDraftLessonLanguage = z.infer<typeof ZCourseImportDraftLessonLanguage>;
|
||||
|
||||
export const ZCourseImportDraftPayload = z
|
||||
.object({
|
||||
course: ZCourseImportDraftCourse,
|
||||
tags: z.array(z.string().trim().min(1).max(80)).max(100).default([]),
|
||||
sections: z.array(ZCourseImportDraftSection).min(1),
|
||||
lessons: z.array(ZCourseImportDraftLesson).min(1),
|
||||
lessonLanguages: z.array(ZCourseImportDraftLessonLanguage).min(1),
|
||||
@@ -63,6 +66,19 @@ export const ZCourseImportDraftPayload = z
|
||||
warnings: z.array(ZCourseImportWarning).default([])
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
const normalizedTags = new Set<string>();
|
||||
value.tags.forEach((tag, index) => {
|
||||
const tagKey = tag.trim().toLowerCase();
|
||||
if (normalizedTags.has(tagKey)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['tags', index],
|
||||
message: 'Draft tags must be unique'
|
||||
});
|
||||
}
|
||||
normalizedTags.add(tagKey);
|
||||
});
|
||||
|
||||
const sectionIds = new Set<string>();
|
||||
value.sections.forEach((section, index) => {
|
||||
if (sectionIds.has(section.externalId)) {
|
||||
@@ -108,7 +124,7 @@ export const ZCourseImportDraftPayload = z
|
||||
export type TCourseImportDraftPayload = z.infer<typeof ZCourseImportDraftPayload>;
|
||||
|
||||
export const ZCourseImportDraftCreate = z.object({
|
||||
sourceType: z.enum(['prompt', 'pdf']),
|
||||
sourceType: z.enum(['prompt', 'pdf', 'course']),
|
||||
idempotencyKey: z.string().min(1).optional(),
|
||||
summary: z.record(z.string(), z.unknown()).optional(),
|
||||
sourceArtifacts: z.array(z.record(z.string(), z.unknown())).optional(),
|
||||
@@ -116,6 +132,19 @@ export const ZCourseImportDraftCreate = z.object({
|
||||
});
|
||||
export type TCourseImportDraftCreate = z.infer<typeof ZCourseImportDraftCreate>;
|
||||
|
||||
export const ZCourseImportCourseParam = z.object({
|
||||
courseId: z.string().min(1)
|
||||
});
|
||||
export type TCourseImportCourseParam = z.infer<typeof ZCourseImportCourseParam>;
|
||||
|
||||
export const ZCourseImportDraftCreateFromCourse = z.object({
|
||||
courseId: z.string().min(1),
|
||||
idempotencyKey: z.string().min(1).optional(),
|
||||
summary: z.record(z.string(), z.unknown()).optional(),
|
||||
sourceArtifacts: z.array(z.record(z.string(), z.unknown())).optional()
|
||||
});
|
||||
export type TCourseImportDraftCreateFromCourse = z.infer<typeof ZCourseImportDraftCreateFromCourse>;
|
||||
|
||||
export const ZCourseImportDraftGetParam = z.object({
|
||||
draftId: z.string().uuid()
|
||||
});
|
||||
@@ -133,6 +162,14 @@ export const ZCourseImportDraftPublish = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().min(1).optional(),
|
||||
type: z.enum(['LIVE_CLASS', 'SELF_PACED']).optional(),
|
||||
metadata: ZCourseMetadata.optional()
|
||||
metadata: ZCourseMetadata.optional(),
|
||||
bannerImageUrl: z.string().url().optional(),
|
||||
bannerImageQuery: z.string().min(1).max(120).optional(),
|
||||
generateBannerImage: z.boolean().optional()
|
||||
});
|
||||
export type TCourseImportDraftPublish = z.infer<typeof ZCourseImportDraftPublish>;
|
||||
|
||||
export const ZCourseImportDraftPublishToCourse = ZCourseImportDraftPublish.extend({
|
||||
courseId: z.string().min(1)
|
||||
});
|
||||
export type TCourseImportDraftPublishToCourse = z.infer<typeof ZCourseImportDraftPublishToCourse>;
|
||||
|
||||
@@ -114,6 +114,69 @@ export const ZCourseContentUpdate = z.object({
|
||||
});
|
||||
export type TCourseContentUpdate = z.infer<typeof ZCourseContentUpdate>;
|
||||
|
||||
export const ZCourseContentReorderSection = z.object({
|
||||
id: z.string().min(1),
|
||||
order: z.number().int().min(0)
|
||||
});
|
||||
export type TCourseContentReorderSection = z.infer<typeof ZCourseContentReorderSection>;
|
||||
|
||||
export const ZCourseContentReorderItem = ZCourseContentUpdateItem.omit({
|
||||
isUnlocked: true
|
||||
});
|
||||
export type TCourseContentReorderItem = z.infer<typeof ZCourseContentReorderItem>;
|
||||
|
||||
export const ZCourseContentReorderBase = z.object({
|
||||
sections: z.array(ZCourseContentReorderSection).min(1).optional(),
|
||||
items: z.array(ZCourseContentReorderItem).min(1).optional()
|
||||
});
|
||||
export type TCourseContentReorderBase = z.infer<typeof ZCourseContentReorderBase>;
|
||||
|
||||
export const ZCourseContentReorder = ZCourseContentReorderBase.superRefine((data, ctx) => {
|
||||
if (!data.sections?.length && !data.items?.length) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['sections'],
|
||||
message: 'Provide sections or items to reorder'
|
||||
});
|
||||
}
|
||||
|
||||
const sectionIds = new Set<string>();
|
||||
data.sections?.forEach((section, index) => {
|
||||
if (sectionIds.has(section.id)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['sections', index, 'id'],
|
||||
message: 'Section IDs must be unique'
|
||||
});
|
||||
}
|
||||
|
||||
sectionIds.add(section.id);
|
||||
});
|
||||
|
||||
const itemKeys = new Set<string>();
|
||||
data.items?.forEach((item, index) => {
|
||||
if (item.order === undefined && item.sectionId === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['items', index],
|
||||
message: 'Each item must provide order or sectionId'
|
||||
});
|
||||
}
|
||||
|
||||
const itemKey = `${item.type}:${item.id}`;
|
||||
if (itemKeys.has(itemKey)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['items', index, 'id'],
|
||||
message: 'Item IDs must be unique per content type'
|
||||
});
|
||||
}
|
||||
|
||||
itemKeys.add(itemKey);
|
||||
});
|
||||
});
|
||||
export type TCourseContentReorder = z.infer<typeof ZCourseContentReorder>;
|
||||
|
||||
export const ZCourseContentDeleteItem = z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum(['LESSON', 'EXERCISE'])
|
||||
@@ -139,6 +202,40 @@ export const ZCourseCreate = z.object({
|
||||
});
|
||||
export type TCourseCreate = z.infer<typeof ZCourseCreate>;
|
||||
|
||||
export const ZCourseReward = z.object({
|
||||
show: z.boolean(),
|
||||
description: z.string()
|
||||
});
|
||||
|
||||
export const ZCourseInstructor = z.object({
|
||||
name: z.string(),
|
||||
role: z.string(),
|
||||
coursesNo: z.number(),
|
||||
description: z.string(),
|
||||
imgUrl: z.string()
|
||||
});
|
||||
|
||||
export const ZCourseCertificate = z.object({
|
||||
templateUrl: z.string()
|
||||
});
|
||||
|
||||
export const ZCourseReview = z.object({
|
||||
id: z.number(),
|
||||
hide: z.boolean(),
|
||||
name: z.string(),
|
||||
avatar_url: z.string(),
|
||||
rating: z.number(),
|
||||
created_at: z.number(),
|
||||
description: z.string()
|
||||
});
|
||||
|
||||
export const ZCourseLessonTabsOrder = z.array(
|
||||
z.object({
|
||||
id: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
|
||||
name: z.string()
|
||||
})
|
||||
);
|
||||
|
||||
// Course metadata schema matching the database structure
|
||||
export const ZCourseMetadata = z.object({
|
||||
requirements: z.string().optional(),
|
||||
@@ -148,47 +245,11 @@ export const ZCourseMetadata = z.object({
|
||||
showDiscount: z.boolean().optional(),
|
||||
discount: z.number().optional(),
|
||||
paymentLink: z.string().optional(),
|
||||
reward: z
|
||||
.object({
|
||||
show: z.boolean(),
|
||||
description: z.string()
|
||||
})
|
||||
.optional(),
|
||||
instructor: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
role: z.string(),
|
||||
coursesNo: z.number(),
|
||||
description: z.string(),
|
||||
imgUrl: z.string()
|
||||
})
|
||||
.optional(),
|
||||
certificate: z
|
||||
.object({
|
||||
templateUrl: z.string()
|
||||
})
|
||||
.optional(),
|
||||
reviews: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
hide: z.boolean(),
|
||||
name: z.string(),
|
||||
avatar_url: z.string(),
|
||||
rating: z.number(),
|
||||
created_at: z.number(),
|
||||
description: z.string()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
lessonTabsOrder: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
reward: ZCourseReward.optional(),
|
||||
instructor: ZCourseInstructor.optional(),
|
||||
certificate: ZCourseCertificate.optional(),
|
||||
reviews: z.array(ZCourseReview).optional(),
|
||||
lessonTabsOrder: ZCourseLessonTabsOrder.optional(),
|
||||
grading: z.boolean().optional(),
|
||||
lessonDownload: z.boolean().optional(),
|
||||
allowNewStudent: z.boolean(),
|
||||
@@ -197,6 +258,27 @@ export const ZCourseMetadata = z.object({
|
||||
});
|
||||
export type TCourseMetadata = z.infer<typeof ZCourseMetadata>;
|
||||
|
||||
export const ZCourseLandingPageMetadataUpdate = z.object({
|
||||
requirements: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
goals: z.string().optional(),
|
||||
videoUrl: z.string().optional(),
|
||||
showDiscount: z.boolean().optional(),
|
||||
discount: z.number().optional(),
|
||||
paymentLink: z.string().optional(),
|
||||
reward: ZCourseReward.partial().optional(),
|
||||
instructor: ZCourseInstructor.partial().optional(),
|
||||
certificate: ZCourseCertificate.partial().optional(),
|
||||
reviews: z.array(ZCourseReview).optional(),
|
||||
lessonTabsOrder: ZCourseLessonTabsOrder.optional(),
|
||||
grading: z.boolean().optional(),
|
||||
lessonDownload: z.boolean().optional(),
|
||||
allowNewStudent: z.boolean().optional(),
|
||||
sectionDisplay: z.record(z.string(), z.boolean()).optional(),
|
||||
isContentGroupingEnabled: z.boolean().optional()
|
||||
});
|
||||
export type TCourseLandingPageMetadataUpdate = z.infer<typeof ZCourseLandingPageMetadataUpdate>;
|
||||
|
||||
export const ZCourseUpdate = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().min(1).optional(),
|
||||
@@ -206,6 +288,8 @@ export const ZCourseUpdate = z.object({
|
||||
isPublished: z.boolean().optional(),
|
||||
overview: z.string().optional(),
|
||||
metadata: ZCourseMetadata.optional(),
|
||||
cost: z.number().int().min(0).optional(),
|
||||
currency: z.enum(['NGN', 'USD']).optional(),
|
||||
isCertificateDownloadable: z.boolean().optional(),
|
||||
certificateTheme: z.string().optional(),
|
||||
tagIds: z.array(z.uuid()).max(100).optional()
|
||||
@@ -217,6 +301,19 @@ export const ZCourseUpdateParam = z.object({
|
||||
});
|
||||
export type TCourseUpdateParam = z.infer<typeof ZCourseUpdateParam>;
|
||||
|
||||
export const ZCourseLandingPageUpdate = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().min(1).optional(),
|
||||
overview: z.string().optional(),
|
||||
cost: z.number().int().min(0).optional(),
|
||||
currency: z.enum(['NGN', 'USD']).optional(),
|
||||
imageUrl: z.url().optional(),
|
||||
generateImage: z.boolean().optional(),
|
||||
imageQuery: z.string().min(1).max(120).optional(),
|
||||
metadata: ZCourseLandingPageMetadataUpdate.optional()
|
||||
});
|
||||
export type TCourseLandingPageUpdate = z.infer<typeof ZCourseLandingPageUpdate>;
|
||||
|
||||
export const ZCourseDeleteParam = z.object({
|
||||
courseId: z.string().min(1)
|
||||
});
|
||||
|
||||
@@ -8,6 +8,11 @@ export const ZOrganizationApiKeyScope = z.enum([
|
||||
'course_import:draft:read',
|
||||
'course_import:draft:update',
|
||||
'course_import:draft:publish',
|
||||
'course:read',
|
||||
'course:write',
|
||||
'course:tag:write',
|
||||
'course:exercise:read',
|
||||
'course:exercise:write',
|
||||
'public_api:*'
|
||||
]);
|
||||
export type TOrganizationApiKeyScope = z.infer<typeof ZOrganizationApiKeyScope>;
|
||||
@@ -17,6 +22,11 @@ export const ZListOrganizationApiKeysQuery = z.object({
|
||||
});
|
||||
export type TListOrganizationApiKeysQuery = z.infer<typeof ZListOrganizationApiKeysQuery>;
|
||||
|
||||
export const ZOrganizationAutomationUsageQuery = z.object({
|
||||
type: ZOrganizationApiKeyType.default('mcp')
|
||||
});
|
||||
export type TOrganizationAutomationUsageQuery = z.infer<typeof ZOrganizationAutomationUsageQuery>;
|
||||
|
||||
export const ZCreateOrganizationApiKey = z.object({
|
||||
type: ZOrganizationApiKeyType,
|
||||
label: z.string().min(1).max(120).optional(),
|
||||
|
||||
@@ -67,3 +67,26 @@ export const ZCourseTagAssignment = z.object({
|
||||
tagIds: z.array(z.uuid()).max(100).default([])
|
||||
});
|
||||
export type TCourseTagAssignment = z.infer<typeof ZCourseTagAssignment>;
|
||||
|
||||
export const ZAutomationTagMode = z.enum(['merge', 'replace']).default('merge');
|
||||
export type TAutomationTagMode = z.infer<typeof ZAutomationTagMode>;
|
||||
|
||||
export const ZAutomationTagName = z.string().trim().min(1).max(80);
|
||||
export type TAutomationTagName = z.infer<typeof ZAutomationTagName>;
|
||||
|
||||
export const ZAutomationDraftTagParam = z.object({
|
||||
draftId: z.string().uuid()
|
||||
});
|
||||
export type TAutomationDraftTagParam = z.infer<typeof ZAutomationDraftTagParam>;
|
||||
|
||||
export const ZAutomationDraftTagAssignment = z.object({
|
||||
tagNames: z.array(ZAutomationTagName).min(1).max(100),
|
||||
mode: ZAutomationTagMode,
|
||||
groupName: z.string().trim().min(1).max(80).optional()
|
||||
});
|
||||
export type TAutomationDraftTagAssignment = z.infer<typeof ZAutomationDraftTagAssignment>;
|
||||
|
||||
export const ZAutomationCourseTagAssignment = ZAutomationDraftTagAssignment.extend({
|
||||
courseIds: z.array(z.string().uuid()).min(1).max(100)
|
||||
});
|
||||
export type TAutomationCourseTagAssignment = z.infer<typeof ZAutomationCourseTagAssignment>;
|
||||
|
||||
Reference in New Issue
Block a user