feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Wire `code` values returned in API JSON error bodies (`success: false`).
|
||||
* Shared by `@cio/api` and clients (e.g. dashboard snackbar mapping).
|
||||
*/
|
||||
export const ErrorCodes = {
|
||||
// Common errors
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||
|
||||
// Dashboard Analytics errors
|
||||
ORG_ANALYTICS_FETCH_FAILED: 'ORG_ANALYTICS_FETCH_FAILED',
|
||||
NO_ORG_ID_PROVIDED: 'NO_ORG_ID_PROVIDED',
|
||||
|
||||
// Community errors
|
||||
UPDATE_COMMUNITY_QUESTION_FAILED: 'UPDATE_COMMUNITY_QUESTION_FAILED',
|
||||
ADD_COMMUNITY_QUESTION_FAILED: 'ADD_COMMUNITY_QUESTION_FAILED',
|
||||
COMMUNITY_QUESTIONS_FETCH_FAILED: 'COMMUNITY_QUESTIONS_FETCH_FAILED',
|
||||
COMMUNITY_QUESTION_NOT_FOUND: 'COMMUNITY_QUESTION_NOT_FOUND',
|
||||
COMMENT_SUBMISSION_FAILED: 'COMMENT_SUBMISSION_FAILED',
|
||||
UPVOTE_FAILED: 'UPVOTE_FAILED',
|
||||
DELETE_COMMENT_FAILED: 'DELETE_COMMENT_FAILED',
|
||||
DELETE_QUESTION_FAILED: 'DELETE_QUESTION_FAILED',
|
||||
|
||||
// Organization errors
|
||||
SITENAME_EXISTS: 'SITENAME_EXISTS',
|
||||
ORG_CREATE_FAILED: 'ORG_CREATE_FAILED',
|
||||
ORG_NOT_FOUND: 'ORG_NOT_FOUND',
|
||||
ORGANIZATION_NOT_FOUND: 'ORGANIZATION_NOT_FOUND',
|
||||
ORG_TEAM_FETCH_FAILED: 'ORG_TEAM_FETCH_FAILED',
|
||||
ORG_AUDIENCE_FETCH_FAILED: 'ORG_AUDIENCE_FETCH_FAILED',
|
||||
COURSES_FETCH_FAILED: 'COURSES_FETCH_FAILED',
|
||||
ORG_PLAN_CREATE_FAILED: 'ORG_PLAN_CREATE_FAILED',
|
||||
ORG_PLAN_UPDATE_FAILED: 'ORG_PLAN_UPDATE_FAILED',
|
||||
ORG_PLAN_CANCEL_FAILED: 'ORG_PLAN_CANCEL_FAILED',
|
||||
ORG_PLAN_NOT_FOUND: 'ORG_PLAN_NOT_FOUND',
|
||||
ORG_TEAM_INVITE_FAILED: 'ORG_TEAM_INVITE_FAILED',
|
||||
ORG_TEAM_REMOVE_FAILED: 'ORG_TEAM_REMOVE_FAILED',
|
||||
ORG_AUDIENCE_REMOVE_FAILED: 'ORG_AUDIENCE_REMOVE_FAILED',
|
||||
ORG_TEAM_EMAIL_EXISTS: 'ORG_TEAM_EMAIL_EXISTS',
|
||||
ORG_TEAM_NOT_AUTHORIZED: 'ORG_TEAM_NOT_AUTHORIZED',
|
||||
ORGANIZATION_API_KEY_NOT_FOUND: 'ORGANIZATION_API_KEY_NOT_FOUND',
|
||||
ORGANIZATION_API_KEY_CREATE_FAILED: 'ORGANIZATION_API_KEY_CREATE_FAILED',
|
||||
ORGANIZATION_API_KEY_FETCH_FAILED: 'ORGANIZATION_API_KEY_FETCH_FAILED',
|
||||
ORGANIZATION_API_KEY_REVOKE_FAILED: 'ORGANIZATION_API_KEY_REVOKE_FAILED',
|
||||
ORGANIZATION_API_KEY_ROTATE_FAILED: 'ORGANIZATION_API_KEY_ROTATE_FAILED',
|
||||
AUTOMATION_KEY_LIMIT_EXCEEDED: 'AUTOMATION_KEY_LIMIT_EXCEEDED',
|
||||
AUTOMATION_CREDIT_LIMIT_EXCEEDED: 'AUTOMATION_CREDIT_LIMIT_EXCEEDED',
|
||||
AUTOMATION_RATE_LIMIT_EXCEEDED: 'AUTOMATION_RATE_LIMIT_EXCEEDED',
|
||||
TAG_GROUP_NOT_FOUND: 'TAG_GROUP_NOT_FOUND',
|
||||
TAG_NOT_FOUND: 'TAG_NOT_FOUND',
|
||||
TAG_FETCH_FAILED: 'TAG_FETCH_FAILED',
|
||||
TAG_CREATE_FAILED: 'TAG_CREATE_FAILED',
|
||||
TAG_UPDATE_FAILED: 'TAG_UPDATE_FAILED',
|
||||
TAG_ASSIGNMENT_FAILED: 'TAG_ASSIGNMENT_FAILED',
|
||||
|
||||
// Asset errors
|
||||
ASSET_NOT_FOUND: 'ASSET_NOT_FOUND',
|
||||
ASSET_IN_USE: 'ASSET_IN_USE',
|
||||
ASSET_USAGE_NOT_FOUND: 'ASSET_USAGE_NOT_FOUND',
|
||||
ASSET_CREATE_FAILED: 'ASSET_CREATE_FAILED',
|
||||
ASSET_FETCH_FAILED: 'ASSET_FETCH_FAILED',
|
||||
ASSET_LIST_FAILED: 'ASSET_LIST_FAILED',
|
||||
ASSET_UPDATE_FAILED: 'ASSET_UPDATE_FAILED',
|
||||
ASSET_DELETE_FAILED: 'ASSET_DELETE_FAILED',
|
||||
ASSET_ATTACH_FAILED: 'ASSET_ATTACH_FAILED',
|
||||
ASSET_DETACH_FAILED: 'ASSET_DETACH_FAILED',
|
||||
ASSET_USAGE_FETCH_FAILED: 'ASSET_USAGE_FETCH_FAILED',
|
||||
ASSET_EXPORT_FAILED: 'ASSET_EXPORT_FAILED',
|
||||
ASSET_STORAGE_FETCH_FAILED: 'ASSET_STORAGE_FETCH_FAILED',
|
||||
|
||||
// Profile errors
|
||||
PROFILE_UPDATE_FAILED: 'PROFILE_UPDATE_FAILED',
|
||||
PROFILE_NOT_FOUND: 'PROFILE_NOT_FOUND',
|
||||
|
||||
// Account errors
|
||||
ACCOUNT_NOT_FOUND: 'ACCOUNT_NOT_FOUND',
|
||||
|
||||
// Auth errors
|
||||
FORBIDDEN: 'FORBIDDEN',
|
||||
|
||||
// Course errors
|
||||
COURSE_NOT_FOUND: 'COURSE_NOT_FOUND',
|
||||
COURSE_FETCH_FAILED: 'COURSE_FETCH_FAILED',
|
||||
COURSE_IMPORT_DRAFT_NOT_FOUND: 'COURSE_IMPORT_DRAFT_NOT_FOUND',
|
||||
COURSE_IMPORT_CREATE_FAILED: 'COURSE_IMPORT_CREATE_FAILED',
|
||||
COURSE_IMPORT_FETCH_FAILED: 'COURSE_IMPORT_FETCH_FAILED',
|
||||
COURSE_IMPORT_UPDATE_FAILED: 'COURSE_IMPORT_UPDATE_FAILED',
|
||||
COURSE_IMPORT_PUBLISH_FAILED: 'COURSE_IMPORT_PUBLISH_FAILED',
|
||||
|
||||
// Lesson errors
|
||||
LESSON_NOT_FOUND: 'LESSON_NOT_FOUND',
|
||||
LESSON_FETCH_FAILED: 'LESSON_FETCH_FAILED',
|
||||
COURSE_SECTION_NOT_FOUND: 'COURSE_SECTION_NOT_FOUND',
|
||||
|
||||
// Exercise errors
|
||||
EXERCISE_NOT_FOUND: 'EXERCISE_NOT_FOUND',
|
||||
EXERCISE_FETCH_FAILED: 'EXERCISE_FETCH_FAILED',
|
||||
|
||||
// Submission errors
|
||||
SUBMISSION_NOT_FOUND: 'SUBMISSION_NOT_FOUND',
|
||||
SUBMISSION_FETCH_FAILED: 'SUBMISSION_FETCH_FAILED',
|
||||
|
||||
// Comment errors
|
||||
COMMENT_NOT_FOUND: 'COMMENT_NOT_FOUND',
|
||||
LESSON_COMMENT_UPDATE_FAILED: 'LESSON_COMMENT_UPDATE_FAILED',
|
||||
NEWSFEED_COMMENT_UPDATE_FAILED: 'NEWSFEED_COMMENT_UPDATE_FAILED',
|
||||
|
||||
// Quiz errors
|
||||
QUIZ_NOT_FOUND: 'QUIZ_NOT_FOUND',
|
||||
QUIZ_FETCH_FAILED: 'QUIZ_FETCH_FAILED',
|
||||
QUIZ_CREATE_FAILED: 'QUIZ_CREATE_FAILED',
|
||||
QUIZ_UPDATE_FAILED: 'QUIZ_UPDATE_FAILED',
|
||||
QUIZ_DELETE_FAILED: 'QUIZ_DELETE_FAILED',
|
||||
|
||||
// Lesson Language errors
|
||||
LESSON_LANGUAGE_NOT_FOUND: 'LESSON_LANGUAGE_NOT_FOUND',
|
||||
LESSON_LANGUAGE_FETCH_FAILED: 'LESSON_LANGUAGE_FETCH_FAILED',
|
||||
LESSON_LANGUAGE_CREATE_FAILED: 'LESSON_LANGUAGE_CREATE_FAILED',
|
||||
LESSON_LANGUAGE_UPDATE_FAILED: 'LESSON_LANGUAGE_UPDATE_FAILED',
|
||||
LESSON_LANGUAGE_DELETE_FAILED: 'LESSON_LANGUAGE_DELETE_FAILED',
|
||||
|
||||
// SSO errors
|
||||
SSO_REGISTRATION_FAILED: 'SSO_REGISTRATION_FAILED',
|
||||
SSO_NOT_FOUND: 'SSO_NOT_FOUND',
|
||||
SSO_NOT_ENABLED: 'SSO_NOT_ENABLED',
|
||||
UPGRADE_REQUIRED: 'UPGRADE_REQUIRED',
|
||||
CONFLICT: 'CONFLICT',
|
||||
|
||||
// Token exchange errors
|
||||
TOKEN_EXCHANGE_INVALID_TOKEN: 'TOKEN_EXCHANGE_INVALID_TOKEN',
|
||||
TOKEN_EXCHANGE_EXPIRED: 'TOKEN_EXCHANGE_EXPIRED',
|
||||
TOKEN_EXCHANGE_ORG_NOT_FOUND: 'TOKEN_EXCHANGE_ORG_NOT_FOUND',
|
||||
TOKEN_EXCHANGE_NOT_ENABLED: 'TOKEN_EXCHANGE_NOT_ENABLED',
|
||||
|
||||
// Token auth admin errors
|
||||
TOKEN_AUTH_ALREADY_EXISTS: 'TOKEN_AUTH_ALREADY_EXISTS',
|
||||
|
||||
// Program errors
|
||||
PROGRAM_NOT_FOUND: 'PROGRAM_NOT_FOUND',
|
||||
PROGRAM_FORBIDDEN: 'PROGRAM_FORBIDDEN',
|
||||
PROGRAM_CREATE_FAILED: 'PROGRAM_CREATE_FAILED',
|
||||
PROGRAM_UPDATE_FAILED: 'PROGRAM_UPDATE_FAILED',
|
||||
PROGRAM_DELETE_FAILED: 'PROGRAM_DELETE_FAILED',
|
||||
COURSE_ALREADY_IN_PROGRAM: 'COURSE_ALREADY_IN_PROGRAM',
|
||||
MEMBER_ALREADY_IN_PROGRAM: 'MEMBER_ALREADY_IN_PROGRAM',
|
||||
PROGRAM_MEMBER_NOT_FOUND: 'PROGRAM_MEMBER_NOT_FOUND',
|
||||
PROGRAM_NEWSFEED_NOT_FOUND: 'PROGRAM_NEWSFEED_NOT_FOUND',
|
||||
PROGRAM_NEWSFEED_COMMENT_NOT_FOUND: 'PROGRAM_NEWSFEED_COMMENT_NOT_FOUND',
|
||||
PROGRAM_MEMBER_CHECK_FAILED: 'PROGRAM_MEMBER_CHECK_FAILED',
|
||||
|
||||
// Generic
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR'
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './roles';
|
||||
export * from './org';
|
||||
export * from './content';
|
||||
export * from './error-codes';
|
||||
|
||||
@@ -11,8 +11,9 @@ export default {
|
||||
FEATURES: [
|
||||
'Unlimited Courses',
|
||||
'Unlimited Q/A',
|
||||
'Unlimited Programs',
|
||||
'20 Students',
|
||||
'AI Course Builder (no video upload)',
|
||||
'Advanced Course Builder (no video upload)',
|
||||
'MCP access with 20 automation credits / month',
|
||||
'ClassroomIO branding'
|
||||
],
|
||||
@@ -37,10 +38,11 @@ export default {
|
||||
'Everything in Basic',
|
||||
'Unlimited Collaborators',
|
||||
'10K Students',
|
||||
'Advanced AI Course Builder (Video upload & Certificate)',
|
||||
'Course Builder with Video Upload & Certificate',
|
||||
'MCP access with 500 automation credits / month',
|
||||
'Custom Branding',
|
||||
'Slack Invite',
|
||||
'Custom Domain',
|
||||
'API with limits',
|
||||
'Includes all upcoming features'
|
||||
],
|
||||
CTA: {
|
||||
@@ -65,8 +67,8 @@ export default {
|
||||
FEATURES: [
|
||||
'Everything in Early Adopters plus:',
|
||||
'Unlimited students',
|
||||
'Custom Domain',
|
||||
'MCP access with 5,000 automation credits / month',
|
||||
'Unlimited API requests',
|
||||
'SSO',
|
||||
'24/7 Support'
|
||||
],
|
||||
CTA: {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
const AssetKind = z.enum(['video', 'document', 'image', 'audio', 'other']);
|
||||
const AssetProvider = z.enum(['upload', 'youtube', 'generic', 'external_url']);
|
||||
const AssetProvider = z.enum(['upload', 'youtube', 'generic', 'external_url', 'google_drive']);
|
||||
export type TAssetProvider = z.infer<typeof AssetProvider>;
|
||||
const AssetStatus = z.enum(['active', 'archived']);
|
||||
const AssetTargetType = z.enum(['lesson', 'exercise', 'question']);
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ export const ZCourseImportDraftPublish = z.object({
|
||||
export type TCourseImportDraftPublish = z.infer<typeof ZCourseImportDraftPublish>;
|
||||
|
||||
export const ZCourseImportDraftPublishToCourse = ZCourseImportDraftPublish.extend({
|
||||
courseId: z.string().min(1)
|
||||
courseId: z.string().min(1),
|
||||
syncMode: z.enum(['merge', 'replace']).default('merge')
|
||||
});
|
||||
export type TCourseImportDraftPublishToCourse = z.infer<typeof ZCourseImportDraftPublishToCourse>;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { getStarRatingMaxFromSettings, isValidStarRatingValue } from '@cio/question-types';
|
||||
|
||||
import { QUESTION_TYPE } from '../constants';
|
||||
|
||||
const EXERCISE_QUESTION_TYPE_ID_LITERALS = [
|
||||
@@ -14,16 +16,20 @@ const EXERCISE_QUESTION_TYPE_ID_LITERALS = [
|
||||
z.literal(QUESTION_TYPE.MATCHING),
|
||||
z.literal(QUESTION_TYPE.ORDERING),
|
||||
z.literal(QUESTION_TYPE.HOTSPOT),
|
||||
z.literal(QUESTION_TYPE.LINK)
|
||||
z.literal(QUESTION_TYPE.LINK),
|
||||
z.literal(QUESTION_TYPE.WORD_BANK),
|
||||
z.literal(QUESTION_TYPE.STAR)
|
||||
] as const;
|
||||
|
||||
export const ZExerciseQuestionTypeId = z.union(EXERCISE_QUESTION_TYPE_ID_LITERALS);
|
||||
export type TExerciseQuestionTypeId = z.infer<typeof ZExerciseQuestionTypeId>;
|
||||
|
||||
const QUESTION_VALIDATION_RULES: Record<
|
||||
number,
|
||||
Array<(question: { options?: Array<{ isCorrect: boolean }> }) => string | null>
|
||||
> = {
|
||||
type QuestionRuleInput = {
|
||||
options?: Array<{ isCorrect: boolean }>;
|
||||
settings?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const QUESTION_VALIDATION_RULES: Record<number, Array<(question: QuestionRuleInput) => string | null>> = {
|
||||
[QUESTION_TYPE.RADIO]: [
|
||||
(question) => {
|
||||
const options = question.options || [];
|
||||
@@ -72,7 +78,41 @@ const QUESTION_VALIDATION_RULES: Record<
|
||||
return null;
|
||||
}
|
||||
],
|
||||
[QUESTION_TYPE.TEXTAREA]: []
|
||||
[QUESTION_TYPE.TEXTAREA]: [],
|
||||
[QUESTION_TYPE.WORD_BANK]: [
|
||||
(question) => {
|
||||
const settings = question.settings;
|
||||
const template = String(settings?.template ?? '');
|
||||
const blankCount = Math.max(0, template.split('___').length - 1);
|
||||
if (blankCount < 1) {
|
||||
return 'Word bank questions need at least one ___ blank in the template';
|
||||
}
|
||||
const raw = settings?.correctAnswers;
|
||||
const answers = Array.isArray(raw) ? (raw as unknown[]) : [];
|
||||
if (answers.length < blankCount) {
|
||||
return 'Provide a correct answer for each blank';
|
||||
}
|
||||
for (let i = 0; i < blankCount; i++) {
|
||||
if (!String(answers[i] ?? '').trim()) {
|
||||
return `Correct answer for blank ${i + 1} is required`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
[QUESTION_TYPE.STAR]: [
|
||||
(question) => {
|
||||
const settings = question.settings ?? {};
|
||||
const maxStars = getStarRatingMaxFromSettings(settings);
|
||||
const rawCorrect = settings.correctValue;
|
||||
const correct = typeof rawCorrect === 'number' ? rawCorrect : rawCorrect != null ? Number(rawCorrect) : NaN;
|
||||
if (!isValidStarRatingValue(correct, maxStars)) {
|
||||
return 'Set a correct star value from 1 up to the maximum number of stars';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Base schema for exercise update question (without refinement)
|
||||
|
||||
@@ -12,6 +12,8 @@ export * from './media';
|
||||
export * from './newsfeed';
|
||||
export * from './onboarding';
|
||||
export * from './organization';
|
||||
export * from './public-api';
|
||||
export * from './submission';
|
||||
export * from './dash';
|
||||
export * from './tag';
|
||||
export * from './program';
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ZLessonUpdate = z.object({
|
||||
videos: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(['youtube', 'generic', 'upload']),
|
||||
type: z.enum(['youtube', 'generic', 'upload', 'google_drive']),
|
||||
link: z.string(),
|
||||
key: z.string().optional(),
|
||||
assetId: z.string().uuid().optional(),
|
||||
|
||||
@@ -18,17 +18,25 @@ export type TGetAudienceQuery = z.infer<typeof ZGetAudienceQuery>;
|
||||
export const ZImportAudienceMembers = z.object({
|
||||
recipientCsv: z.string().max(25000),
|
||||
courseIds: z.array(z.string().uuid()).optional(),
|
||||
programIds: z.array(z.string().uuid()).optional(),
|
||||
allCourses: z.boolean().optional().default(false),
|
||||
allPrograms: z.boolean().optional().default(false),
|
||||
sendEmail: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export type TImportAudienceMembers = z.infer<typeof ZImportAudienceMembers>;
|
||||
|
||||
export const ZAssignAudienceCourses = z.object({
|
||||
profileIds: z.array(z.uuid()).min(1).max(500),
|
||||
courseIds: z.array(z.uuid()).min(1),
|
||||
sendEmail: z.boolean().default(true)
|
||||
});
|
||||
export const ZAssignAudienceCourses = z
|
||||
.object({
|
||||
profileIds: z.array(z.uuid()).min(1).max(500),
|
||||
courseIds: z.array(z.uuid()).optional(),
|
||||
programIds: z.array(z.uuid()).optional(),
|
||||
sendEmail: z.boolean().default(true)
|
||||
})
|
||||
.refine((data) => (data.courseIds?.length ?? 0) > 0 || (data.programIds?.length ?? 0) > 0, {
|
||||
message: 'At least one course or program must be selected',
|
||||
path: ['courseIds']
|
||||
});
|
||||
|
||||
export type TAssignAudienceCourses = z.infer<typeof ZAssignAudienceCourses>;
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ export const ZGetCoursesBySiteName = z.object({
|
||||
export type TGetCoursesBySiteName = z.infer<typeof ZGetCoursesBySiteName>;
|
||||
|
||||
export const ZGetOrganizationCoursesQuery = z.object({
|
||||
page: z.coerce.number().int().min(1).default(1),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
search: z.string().trim().optional(),
|
||||
tags: z.string().optional()
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './program';
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as z from 'zod';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
|
||||
export const ZCreateProgram = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
description: z.string().max(2000).optional(),
|
||||
coverImage: z.string().url().optional()
|
||||
});
|
||||
export type TCreateProgram = z.infer<typeof ZCreateProgram>;
|
||||
|
||||
export const ZUpdateProgram = z.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
description: z.string().max(2000).nullable().optional(),
|
||||
coverImage: z.string().url().nullable().optional(),
|
||||
status: z.enum(['ACTIVE', 'INACTIVE', 'ARCHIVED']).optional()
|
||||
});
|
||||
export type TUpdateProgram = z.infer<typeof ZUpdateProgram>;
|
||||
|
||||
export const ZAddCourseToProgram = z.object({
|
||||
courseId: z.string().uuid()
|
||||
});
|
||||
export type TAddCourseToProgram = z.infer<typeof ZAddCourseToProgram>;
|
||||
|
||||
export const ZAddProgramMembers = z.object({
|
||||
members: z
|
||||
.array(
|
||||
z.object({
|
||||
profileId: z.string().uuid().optional(),
|
||||
email: z.string().email().optional(),
|
||||
name: z.string().optional(),
|
||||
roleId: z.union([z.literal(ROLE.TUTOR), z.literal(ROLE.STUDENT)])
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.refine((members) => members.every((member) => member.profileId || member.email), {
|
||||
message: 'Each member must include a profileId or email'
|
||||
})
|
||||
});
|
||||
export type TAddProgramMembers = z.infer<typeof ZAddProgramMembers>;
|
||||
|
||||
export const ZUpdateProgramMember = z.object({
|
||||
roleId: z.union([z.literal(ROLE.TUTOR), z.literal(ROLE.STUDENT)])
|
||||
});
|
||||
export type TUpdateProgramMember = z.infer<typeof ZUpdateProgramMember>;
|
||||
|
||||
export const ZCreateProgramNewsfeed = z.object({
|
||||
content: z.string().min(1),
|
||||
isPinned: z.boolean().optional()
|
||||
});
|
||||
export type TCreateProgramNewsfeed = z.infer<typeof ZCreateProgramNewsfeed>;
|
||||
|
||||
export const ZUpdateProgramNewsfeed = z.object({
|
||||
content: z.string().min(1).optional(),
|
||||
isPinned: z.boolean().optional()
|
||||
});
|
||||
export type TUpdateProgramNewsfeed = z.infer<typeof ZUpdateProgramNewsfeed>;
|
||||
|
||||
export const ZUpdateProgramReaction = z.object({
|
||||
reaction: z.object({
|
||||
clap: z.array(z.string()),
|
||||
smile: z.array(z.string()),
|
||||
thumbsup: z.array(z.string()),
|
||||
thumbsdown: z.array(z.string())
|
||||
})
|
||||
});
|
||||
export type TUpdateProgramReaction = z.infer<typeof ZUpdateProgramReaction>;
|
||||
|
||||
export const ZCreateProgramNewsfeedComment = z.object({
|
||||
content: z.string().min(1)
|
||||
});
|
||||
export type TCreateProgramNewsfeedComment = z.infer<typeof ZCreateProgramNewsfeedComment>;
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { ZAssignAudienceCourses, ZGetAudienceQuery } from '../organization';
|
||||
|
||||
export const ZPublicApiAudienceQuery = ZGetAudienceQuery;
|
||||
export type TPublicApiAudienceQuery = z.infer<typeof ZPublicApiAudienceQuery>;
|
||||
|
||||
export const ZPublicApiCreateAudience = z.object({
|
||||
email: z.string().email(),
|
||||
courseIds: z.array(z.string().uuid()).optional(),
|
||||
sendEmail: z.boolean().default(true)
|
||||
});
|
||||
export type TPublicApiCreateAudience = z.infer<typeof ZPublicApiCreateAudience>;
|
||||
|
||||
export const ZPublicApiUpdateAudience = z.object({
|
||||
email: z.string().email(),
|
||||
sendEmail: z.boolean().default(true)
|
||||
});
|
||||
export type TPublicApiUpdateAudience = z.infer<typeof ZPublicApiUpdateAudience>;
|
||||
|
||||
export const ZPublicApiAssignAudienceCourses = ZAssignAudienceCourses;
|
||||
export type TPublicApiAssignAudienceCourses = z.infer<typeof ZPublicApiAssignAudienceCourses>;
|
||||
|
||||
export const ZPublicApiAudienceMemberParam = z.object({
|
||||
memberId: z.coerce.number().int().positive()
|
||||
});
|
||||
export type TPublicApiAudienceMemberParam = z.infer<typeof ZPublicApiAudienceMemberParam>;
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { ZCourseCreate, ZCourseUpdate } from '../course/course';
|
||||
import { ZCourseImportDraftPayload } from '../course-import/course-import';
|
||||
|
||||
export const ZPublicApiCoursesQuery = z.object({
|
||||
tags: z.string().optional()
|
||||
});
|
||||
export type TPublicApiCoursesQuery = z.infer<typeof ZPublicApiCoursesQuery>;
|
||||
|
||||
export const ZPublicApiCourseParam = z.object({
|
||||
courseId: z.string().uuid()
|
||||
});
|
||||
export type TPublicApiCourseParam = z.infer<typeof ZPublicApiCourseParam>;
|
||||
|
||||
export const ZPublicApiCreateCourse = ZCourseCreate.omit({
|
||||
organizationId: true
|
||||
});
|
||||
export type TPublicApiCreateCourse = z.infer<typeof ZPublicApiCreateCourse>;
|
||||
|
||||
export const ZPublicApiUpdateCourse = ZCourseUpdate.omit({
|
||||
tagIds: true
|
||||
});
|
||||
export type TPublicApiUpdateCourse = z.infer<typeof ZPublicApiUpdateCourse>;
|
||||
|
||||
export const ZPublicApiUpdateCourseStructure = z.object({
|
||||
mode: z.enum(['merge', 'replace']).default('merge'),
|
||||
idempotencyKey: z.string().min(1).optional(),
|
||||
summary: z.record(z.string(), z.unknown()).optional(),
|
||||
sourceArtifacts: z.array(z.record(z.string(), z.unknown())).optional(),
|
||||
draft: ZCourseImportDraftPayload
|
||||
});
|
||||
export type TPublicApiUpdateCourseStructure = z.infer<typeof ZPublicApiUpdateCourseStructure>;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './audience';
|
||||
export * from './course';
|
||||
Reference in New Issue
Block a user