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,330 @@
|
||||
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
|
||||
import type { Program, ProgramDetail, ProgramMember, ProgramCourse, ProgramNewsfeed } from '../utils/types';
|
||||
import type {
|
||||
TCreateProgram,
|
||||
TUpdateProgram,
|
||||
TAddProgramMembers,
|
||||
TUpdateProgramMember,
|
||||
TAddCourseToProgram
|
||||
} from '@cio/utils/validation/program';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { get } from 'svelte/store';
|
||||
import { isStudentExperience } from '$lib/utils/store/app';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
class ProgramApi extends BaseApiWithErrors {
|
||||
programs = $state<Program[]>([]);
|
||||
program = $state<ProgramDetail | null>(null);
|
||||
members = $state<ProgramMember[]>([]);
|
||||
courses = $state<ProgramCourse[]>([]);
|
||||
newsfeed = $state<ProgramNewsfeed | null>(null);
|
||||
isProgramShellLoading = $state(false);
|
||||
currentProgramId = $state<string | null>(null);
|
||||
|
||||
loadedProgramId = $state<string | null>(null);
|
||||
loadedMembersProgramId = $state<string | null>(null);
|
||||
loadedCoursesProgramId = $state<string | null>(null);
|
||||
loadedCoursesStudentExperience = $state<boolean | null>(null);
|
||||
loadedNewsfeedProgramId = $state<string | null>(null);
|
||||
|
||||
syncProgramCourseCount(programId: string, courseCount: number) {
|
||||
this.programs = this.programs.map((program) => (program.id === programId ? { ...program, courseCount } : program));
|
||||
}
|
||||
|
||||
async listPrograms() {
|
||||
const org = get(currentOrg);
|
||||
if (!org.id) return;
|
||||
|
||||
await this.execute<typeof classroomio.program.$get>({
|
||||
requestFn: () => classroomio.program.$get({ query: { organizationId: org.id } }),
|
||||
onSuccess: (data) => {
|
||||
this.programs = data.data;
|
||||
},
|
||||
logContext: 'listPrograms'
|
||||
});
|
||||
}
|
||||
|
||||
async getProgram(programId: string, force = false) {
|
||||
if (!force && this.loadedProgramId === programId) return;
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['$get']>({
|
||||
requestFn: () => classroomio.program[':programId'].$get({ param: { programId } }),
|
||||
onSuccess: (data) => {
|
||||
this.program = data.data;
|
||||
this.loadedProgramId = programId;
|
||||
},
|
||||
logContext: 'getProgram'
|
||||
});
|
||||
}
|
||||
|
||||
resetProgramShell(programId?: string) {
|
||||
this.currentProgramId = programId ?? null;
|
||||
this.program = null;
|
||||
this.members = [];
|
||||
this.courses = [];
|
||||
this.newsfeed = null;
|
||||
this.loadedProgramId = null;
|
||||
this.loadedMembersProgramId = null;
|
||||
this.loadedCoursesProgramId = null;
|
||||
this.loadedCoursesStudentExperience = null;
|
||||
this.loadedNewsfeedProgramId = null;
|
||||
}
|
||||
|
||||
async ensureProgramShell(programId: string, force = false) {
|
||||
const studentExperience = get(isStudentExperience);
|
||||
const isShellReady =
|
||||
this.loadedProgramId === programId &&
|
||||
this.loadedMembersProgramId === programId &&
|
||||
this.loadedCoursesProgramId === programId &&
|
||||
this.loadedCoursesStudentExperience === studentExperience;
|
||||
|
||||
if (!force && isShellReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isProgramChanged = this.currentProgramId !== programId;
|
||||
if (force || isProgramChanged) {
|
||||
this.resetProgramShell(programId);
|
||||
}
|
||||
|
||||
this.currentProgramId = programId;
|
||||
this.isProgramShellLoading = true;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.getProgram(programId, force),
|
||||
this.listMembers(programId, force),
|
||||
this.listCourses(programId, force)
|
||||
]);
|
||||
} finally {
|
||||
this.isProgramShellLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async createProgram(data: TCreateProgram) {
|
||||
const org = get(currentOrg);
|
||||
if (!org.id) return;
|
||||
|
||||
await this.execute<typeof classroomio.program.$post>({
|
||||
requestFn: () =>
|
||||
classroomio.program.$post({
|
||||
json: { ...data, organizationId: org.id }
|
||||
}),
|
||||
onSuccess: (res) => {
|
||||
this.programs = [res.data, ...this.programs];
|
||||
snackbar.success(t.get('programs.create_success') || 'Program created');
|
||||
},
|
||||
logContext: 'createProgram'
|
||||
});
|
||||
}
|
||||
|
||||
async updateProgram(programId: string, data: TUpdateProgram) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['$put']>({
|
||||
requestFn: () => classroomio.program[':programId'].$put({ param: { programId }, json: data }),
|
||||
onSuccess: (res) => {
|
||||
this.program = res.data;
|
||||
this.programs = this.programs.map((p) => (p.id === programId ? { ...p, ...res.data } : p));
|
||||
snackbar.success(t.get('programs.update_success') || 'Program updated');
|
||||
},
|
||||
onError: (result) => {
|
||||
const message = typeof result === 'string' ? result : result.error;
|
||||
snackbar.error(message || 'Failed to update program');
|
||||
},
|
||||
logContext: 'updateProgram'
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProgram(programId: string) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['$delete']>({
|
||||
requestFn: () => classroomio.program[':programId'].$delete({ param: { programId } }),
|
||||
onSuccess: () => {
|
||||
this.programs = this.programs.filter((p) => p.id !== programId);
|
||||
snackbar.success(t.get('programs.delete_success') || 'Program deleted');
|
||||
},
|
||||
logContext: 'deleteProgram'
|
||||
});
|
||||
}
|
||||
|
||||
// Members
|
||||
|
||||
async listMembers(programId: string, force = false) {
|
||||
if (!force && this.loadedMembersProgramId === programId) return;
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members']['$get']>({
|
||||
requestFn: () => classroomio.program[':programId'].members.$get({ param: { programId } }),
|
||||
onSuccess: (data) => {
|
||||
this.members = data.data;
|
||||
this.loadedMembersProgramId = programId;
|
||||
},
|
||||
logContext: 'listProgramMembers'
|
||||
});
|
||||
}
|
||||
|
||||
async addMembers(programId: string, data: TAddProgramMembers) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members']['$post']>({
|
||||
requestFn: () => classroomio.program[':programId'].members.$post({ param: { programId }, json: data }),
|
||||
onSuccess: async () => {
|
||||
await this.listMembers(programId, true);
|
||||
snackbar.success(t.get('programs.members_added') || 'Members added');
|
||||
},
|
||||
logContext: 'addProgramMembers'
|
||||
});
|
||||
}
|
||||
|
||||
async updateMember(programId: string, memberId: string, data: TUpdateProgramMember) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members'][':memberId']['$put']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].members[':memberId'].$put({
|
||||
param: { programId, memberId },
|
||||
json: data
|
||||
}),
|
||||
onSuccess: (res) => {
|
||||
this.members = this.members.map((m) => (m.id === memberId ? { ...m, ...res.data } : m));
|
||||
},
|
||||
logContext: 'updateProgramMember'
|
||||
});
|
||||
}
|
||||
|
||||
async removeMember(programId: string, memberId: string) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['members'][':memberId']['$delete']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].members[':memberId'].$delete({
|
||||
param: { programId, memberId }
|
||||
}),
|
||||
onSuccess: () => {
|
||||
this.members = this.members.filter((m) => m.id !== memberId);
|
||||
snackbar.success(t.get('programs.member_removed') || 'Member removed');
|
||||
},
|
||||
logContext: 'removeProgramMember'
|
||||
});
|
||||
}
|
||||
|
||||
// Courses
|
||||
|
||||
async listCourses(programId: string, force = false) {
|
||||
const studentExperience = get(isStudentExperience);
|
||||
|
||||
if (
|
||||
!force &&
|
||||
this.loadedCoursesProgramId === programId &&
|
||||
this.loadedCoursesStudentExperience === studentExperience
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['courses']['$get']>({
|
||||
requestFn: () => classroomio.program[':programId'].courses.$get({ param: { programId } }),
|
||||
onSuccess: (data) => {
|
||||
this.courses = data.data;
|
||||
this.syncProgramCourseCount(programId, data.data.length);
|
||||
this.loadedCoursesProgramId = programId;
|
||||
this.loadedCoursesStudentExperience = studentExperience;
|
||||
},
|
||||
logContext: 'listProgramCourses'
|
||||
});
|
||||
}
|
||||
|
||||
async addCourse(programId: string, data: TAddCourseToProgram) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['courses']['$post']>({
|
||||
requestFn: () => classroomio.program[':programId'].courses.$post({ param: { programId }, json: data }),
|
||||
onSuccess: async () => {
|
||||
await this.listCourses(programId, true);
|
||||
this.syncProgramCourseCount(programId, this.courses.length);
|
||||
snackbar.success(t.get('programs.course_added') || 'Course added');
|
||||
},
|
||||
logContext: 'addCourseToProgram'
|
||||
});
|
||||
}
|
||||
|
||||
async addCourses(programId: string, courseIds: string[]) {
|
||||
if (courseIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
this.success = false;
|
||||
|
||||
try {
|
||||
for (const courseId of courseIds) {
|
||||
const response = await classroomio.program[':programId'].courses.$post({
|
||||
param: { programId },
|
||||
json: { courseId }
|
||||
});
|
||||
const result = (await response.json()) as { success?: boolean; error?: string };
|
||||
|
||||
if (!result.success) {
|
||||
this.error = result.error ?? 'Failed to add course to program';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await this.listCourses(programId, true);
|
||||
this.syncProgramCourseCount(programId, this.courses.length);
|
||||
|
||||
const successMessage =
|
||||
courseIds.length === 1
|
||||
? t.get('programs.course_added') || 'Course added'
|
||||
: t.get('programs.multiple_added') || 'Courses added';
|
||||
|
||||
snackbar.success(successMessage);
|
||||
this.success = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error in addCoursesToProgram:', error);
|
||||
this.error = error instanceof Error ? error.message : 'Failed to add courses to program';
|
||||
return false;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async removeCourse(programId: string, courseId: string) {
|
||||
await this.execute<(typeof classroomio.program)[':programId']['courses'][':courseId']['$delete']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].courses[':courseId'].$delete({
|
||||
param: { programId, courseId }
|
||||
}),
|
||||
onSuccess: () => {
|
||||
this.courses = this.courses.filter((c) => c.courseId !== courseId);
|
||||
this.syncProgramCourseCount(programId, this.courses.length);
|
||||
snackbar.success(t.get('programs.course_removed') || 'Course removed');
|
||||
},
|
||||
logContext: 'removeCourseFromProgram'
|
||||
});
|
||||
}
|
||||
|
||||
// Newsfeed
|
||||
|
||||
async listNewsfeed(programId: string, force = false, cursor?: string) {
|
||||
if (!force && this.loadedNewsfeedProgramId === programId) return;
|
||||
|
||||
await this.execute<(typeof classroomio.program)[':programId']['newsfeed']['$get']>({
|
||||
requestFn: () =>
|
||||
classroomio.program[':programId'].newsfeed.$get({
|
||||
param: { programId },
|
||||
query: { limit: '10', ...(cursor ? { cursor } : {}) }
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
this.newsfeed = data.data;
|
||||
this.loadedNewsfeedProgramId = programId;
|
||||
},
|
||||
logContext: 'listProgramNewsfeed'
|
||||
});
|
||||
}
|
||||
|
||||
// LMS — enrolled programs
|
||||
|
||||
async listEnrolledPrograms() {
|
||||
await this.execute<typeof classroomio.program.enrolled.$get>({
|
||||
requestFn: () => classroomio.program.enrolled.$get({}),
|
||||
onSuccess: (data) => {
|
||||
this.programs = data.data;
|
||||
},
|
||||
logContext: 'listEnrolledPrograms'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const programApi = new ProgramApi();
|
||||
Reference in New Issue
Block a user