diff --git a/packages/api-hub/README.md b/packages/api-hub/README.md deleted file mode 100644 index b664dc16..00000000 --- a/packages/api-hub/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# API-HUB - -提供 api 专门给外部调用 - -目前只服务于编辑端 - -## 注意点 - -1. 没有检测用户 - - 因为目前只服务于编辑端 , 而用户的检测已经在编辑端做了, 所以这里先不做, 但是以后如果开放给多端的话 那么就需要做了 - -## 安全问题 - -- 目前只允许编辑端的 ip 访问接口,其他的 ip 访问一律不通过 diff --git a/packages/api-hub/ecosystem.config.js b/packages/api-hub/ecosystem.config.js deleted file mode 100644 index 6be65164..00000000 --- a/packages/api-hub/ecosystem.config.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - apps: [ - { - name: "api-hub", - script: "dist/index.js", - instances: "max", - exec_mode: "cluster", - env: { - NODE_ENV: "prod", - PORT: 3008, - }, - }, - ], -}; diff --git a/packages/api-hub/package.json b/packages/api-hub/package.json deleted file mode 100644 index 389914bd..00000000 --- a/packages/api-hub/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "api-hub", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "dev": "tsx watch src", - "test": "vitest", - "build": "rm -rf dist && tsc", - "start": "pnpm build && NODE_ENV=prod node ./dist/index.js", - "start:pm2": "pm2 start ecosystem.config.js", - "restart:pm2": "pm2 restart api-hub" - }, - "engines": { - "node": ">=20.15.0" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@earthworm/schema": "workspace:^", - "@fastify/autoload": "^5.8.3", - "dotenv": "^16.3.1", - "drizzle-orm": "^0.30.8", - "fastify": "^4.28.0", - "fastify-plugin": "^4.5.1", - "pino": "^9.2.0", - "postgres": "^3.4.4" - }, - "devDependencies": { - "@fastify/swagger": "^8.14.0", - "@types/node": "^20.14.2", - "fast-glob": "^3.3.2", - "json-schema-to-ts": "^3.1.0", - "pino-pretty": "^11.2.1", - "tsx": "^4.7.0", - "typescript": "^5.4.5", - "vitest": "^1.6.0" - } -} diff --git a/packages/api-hub/src/db/index.ts b/packages/api-hub/src/db/index.ts deleted file mode 100644 index 604c5875..00000000 --- a/packages/api-hub/src/db/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; - -import { DefaultLogger, LogWriter, sql } from "drizzle-orm"; -import { drizzle } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; - -import { schemas, SchemaType } from "@earthworm/schema"; -import { logger } from "../services/logger"; - -export type DbType = PostgresJsDatabase; - -export let db: DbType; -let connection: postgres.Sql; - -export const setupDb = async (databaseUrl?: string) => { - const dbUrl = databaseUrl || process.env.DATABASE_URL || ""; - connection = postgres(dbUrl); - logger.info(`DB_URL: ${process.env.DATABASE_URL}`); - - class CustomDbLogWriter implements LogWriter { - write(message: string) { - logger.debug(message); - } - } - - db = drizzle(connection, { - schema: schemas, - logger: new DefaultLogger({ writer: new CustomDbLogWriter() }), - }); - - return db; -}; - -export async function cleanDB(db: DbType) { - await db.execute( - sql`TRUNCATE TABLE courses, statements, "course_packs" , "user_course_progress", "course_history", "user_learn_record", "memberships" RESTART IDENTITY CASCADE;`, - ); -} - -export async function teardownDb() { - if (connection) { - await connection.end(); - logger.info("Database connection closed."); - } -} diff --git a/packages/api-hub/src/index.ts b/packages/api-hub/src/index.ts deleted file mode 100644 index abb261e4..00000000 --- a/packages/api-hub/src/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import "./services/env"; - -import { join } from "path"; - -import autoLoad from "@fastify/autoload"; -import Fastify from "fastify"; - -import { setupDb } from "./db"; - -const fastify = Fastify({ - logger: { - transport: { - target: "pino-pretty", - options: { - destination: 1, - colorize: true, - translateTime: "HH:MM:ss.l", - ignore: "pid,hostname", - }, - }, - }, -}); - -// fastify.register(autoLoad, { -// dir: join(__dirname, "plugins"), -// }); - -fastify.register(autoLoad, { - dir: join(__dirname, "routes"), -}); - -const start = async () => { - try { - await setupDb(); - const port = process.env.PORT || 3008; - await fastify.listen({ port: Number(port) }); - } catch (err) { - fastify.log.error(err); - process.exit(1); - } -}; -start(); diff --git a/packages/api-hub/src/routes/course-pack/handler.ts b/packages/api-hub/src/routes/course-pack/handler.ts deleted file mode 100644 index 142b83ee..00000000 --- a/packages/api-hub/src/routes/course-pack/handler.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { RouteHandler } from "fastify"; - -import type { - CreateCoursePack, - DeleteCoursePack, - UpdateCoursePackBody, - UpdateCoursePackParams, -} from "./schema"; -import { logger } from "../../services/logger"; -import { createCoursePack, deleteCoursePack, updateCoursePack } from "./service"; - -export const createCoursePackHandler: RouteHandler<{ - Body: CreateCoursePack; -}> = async function (req, reply) { - try { - const result = await createCoursePack(req.body); - reply.code(201).send({ - state: 1, - data: { - ...result, - }, - }); - } catch (error) { - logger.error(error); - reply.code(500).send({ - state: 0, - message: "Internal Server Error", - }); - } -}; - -export const deleteCoursePackHandler: RouteHandler<{ - Params: DeleteCoursePack; -}> = async function (req, reply) { - const coursePackId = req.params.id; - - try { - const result = await deleteCoursePack(coursePackId); - reply.code(200).send({ - state: 1, - data: result, - }); - } catch (error) { - logger.error(`Failed to delete course pack ${coursePackId}: ${error}`); - reply.code(500).send({ - state: 0, - message: "Internal Server Error", - }); - } -}; - -export const updateCoursePackHandler: RouteHandler<{ - Body: UpdateCoursePackBody; - Params: UpdateCoursePackParams; -}> = async function (req, reply) { - const coursePackId = req.params.id; - try { - const result = await updateCoursePack(coursePackId, req.body); - reply.code(200).send({ - state: 1, - data: { ...result }, - }); - } catch (error) { - logger.error(error); - reply.code(500).send({ - state: 0, - message: "Internal Server Error", - }); - } -}; diff --git a/packages/api-hub/src/routes/course-pack/index.ts b/packages/api-hub/src/routes/course-pack/index.ts deleted file mode 100644 index 4a4e11db..00000000 --- a/packages/api-hub/src/routes/course-pack/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { FastifyInstance } from "fastify"; - -import { - createCoursePackHandler, - deleteCoursePackHandler, - updateCoursePackHandler, -} from "./handler"; -import { - coursePackSchema, - deleteCoursePackSchema, - updateCoursePackParamsSchema, - updateCoursePackSchema, -} from "./schema"; - -export default async (fastify: FastifyInstance) => { - fastify.post("/", { schema: { body: coursePackSchema } }, createCoursePackHandler); - fastify.delete("/:id", { schema: { params: deleteCoursePackSchema } }, deleteCoursePackHandler); - fastify.put( - "/:id", - { schema: { body: updateCoursePackSchema, params: updateCoursePackParamsSchema } }, - updateCoursePackHandler, - ); -}; diff --git a/packages/api-hub/src/routes/course-pack/schema.ts b/packages/api-hub/src/routes/course-pack/schema.ts deleted file mode 100644 index 9e48a0fc..00000000 --- a/packages/api-hub/src/routes/course-pack/schema.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { FromSchema } from "json-schema-to-ts"; - -const statementSchema = { - type: "object", - required: ["english", "phonetic", "chinese"], - properties: { - english: { type: "string" }, - phonetic: { type: "string" }, - chinese: { type: "string" }, - }, -} as const; - -export const coursePackSchema = { - type: "object", - required: ["title", "description", "cover", "courses", "uId", "shareLevel"], - properties: { - title: { type: "string" }, - description: { type: "string" }, - cover: { type: "string" }, - uId: { type: "string" }, - shareLevel: { type: "string" }, - courses: { - type: "array", - items: { - type: "object", - required: ["title", "description", "statements"], - properties: { - title: { type: "string" }, - description: { type: "string" }, - statements: { - type: "array", - items: { - ...statementSchema, - }, - }, - }, - }, - }, - }, -} as const; - -export const updateCoursePackSchema = { - type: "object", - required: ["title", "description", "cover", "courses", "uId", "shareLevel"], - properties: { - title: { type: "string" }, - description: { type: "string" }, - cover: { type: "string" }, - uId: { type: "string" }, - shareLevel: { type: "string" }, - courses: { - type: "array", - items: { - type: "object", - required: ["title", "description", "statements", "publishCourseId"], - properties: { - title: { type: "string" }, - description: { type: "string" }, - publishCourseId: { type: "string" }, - statements: { - type: "array", - items: { - ...statementSchema, - }, - }, - }, - }, - }, - }, -} as const; - -export const updateCoursePackParamsSchema = { - type: "object", - properties: { - id: { type: "string" }, - }, - required: ["id"], -} as const; - -export type CreateCoursePack = FromSchema; - -export type UpdateCoursePackBody = FromSchema; - -export type UpdateCoursePackParams = FromSchema; -export type Statement = FromSchema; - -export const deleteCoursePackSchema = { - type: "object", - properties: { - id: { type: "string" }, - }, - required: ["id"], -} as const; - -export type DeleteCoursePack = FromSchema; diff --git a/packages/api-hub/src/routes/course-pack/service.ts b/packages/api-hub/src/routes/course-pack/service.ts deleted file mode 100644 index dd4d52f9..00000000 --- a/packages/api-hub/src/routes/course-pack/service.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { and, asc, eq } from "drizzle-orm"; - -import { - courseHistory as courseHistorySchema, - coursePack as coursePackSchema, - course as courseSchema, - statement as statementSchema, - userCourseProgress as userCourseProgressSchema, -} from "@earthworm/schema"; -import type { CreateCoursePack, Statement, UpdateCoursePackBody } from "./schema"; -import { db } from "../../db"; -import { logger } from "../../services/logger"; - -export async function createCoursePack(coursePackInfo: CreateCoursePack) { - const result = await db.transaction(async (tx) => { - const coursePackOrder = await calculateCoursePackOrder(coursePackInfo.uId); - - const [coursePackEntity] = await tx - .insert(coursePackSchema) - .values({ - order: coursePackOrder, - creatorId: coursePackInfo.uId, - shareLevel: coursePackInfo.shareLevel, - title: coursePackInfo.title, - description: coursePackInfo.description, - cover: coursePackInfo.cover, - isFree: true, - }) - .returning(); - - const courseIds: string[] = []; - for (const [cIndex, course] of coursePackInfo.courses.entries()) { - const [courseEntity] = await tx - .insert(courseSchema) - .values({ - coursePackId: coursePackEntity.id, - order: cIndex + 1, - title: course.title, - description: course.description, - }) - .returning({ id: courseSchema.id, order: courseSchema.order, title: courseSchema.title }); - - // coursePackIds.courses.push(courseEntity.id.toString()); - courseIds.push(courseEntity.id.toString()); - - logger.debug( - `创建: id-${courseEntity.id} order-${courseEntity.order} title-${courseEntity.title}`, - ); - - const createStatementTasks = course.statements.map( - ({ chinese, english, phonetic }, sIndex) => { - return tx.insert(statementSchema).values({ - chinese, - english, - soundmark: phonetic, - order: sIndex + 1, - courseId: courseEntity.id, - }); - }, - ); - - logger.debug("开始创建 statements"); - await Promise.all(createStatementTasks); - logger.debug("创建 statements 完成"); - } - - return { - coursePackId: coursePackEntity.id, - courseIds, - }; - - async function calculateCoursePackOrder(userId: string) { - const entity = await tx.query.coursePack.findFirst({ - orderBy: (table, { desc }) => [desc(table.order)], - where: (table, { eq }) => eq(table.creatorId, userId), - }); - - if (entity) { - return entity.order + 1; - } - - return 1; - } - }); - - return result; -} - -export async function deleteCoursePack(coursePackId: string) { - const result = await db.transaction(async (tx) => { - const coursePack = await tx.query.coursePack.findFirst({ - where: eq(coursePackSchema.id, coursePackId), - }); - - if (!coursePack) { - throw new Error("not found course pack"); - } - - const courses = await tx.query.course.findMany({ - where: eq(courseSchema.coursePackId, coursePackId), - }); - - const deleteStatementTasks = courses.map((course) => { - return tx.delete(statementSchema).where(eq(statementSchema.courseId, course.id)); - }); - - await Promise.all(deleteStatementTasks); - await tx.delete(courseSchema).where(eq(courseSchema.coursePackId, coursePackId)); - await tx.delete(coursePackSchema).where(eq(coursePackSchema.id, coursePackId)); - - // 还需要删除 course_history - // 和 user_course_progress 里面的记录 - await tx.delete(courseHistorySchema).where(eq(courseHistorySchema.coursePackId, coursePackId)); - - await tx - .delete(userCourseProgressSchema) - .where(eq(userCourseProgressSchema.coursePackId, coursePackId)); - - return true; - }); - - return result; -} - -export async function updateCoursePack(coursePackId: string, coursePackInfo: UpdateCoursePackBody) { - const result = await db.transaction(async (tx) => { - async function _updateCoursePack() { - await tx - .update(coursePackSchema) - .set({ - title: coursePackInfo.title, - description: coursePackInfo.description, - cover: coursePackInfo.cover, - shareLevel: coursePackInfo.shareLevel, - }) - .where(eq(coursePackSchema.id, coursePackId)); - } - - async function _updateCourses() { - const courseIds: string[] = []; - const oldCourses = await tx.query.course.findMany({ - where: eq(courseSchema.coursePackId, coursePackId), - orderBy: [asc(courseSchema.order)], - }); - - const oldCourseMap = new Map(oldCourses.map((course) => [course.id, course])); - const newCourseMap = new Map( - coursePackInfo.courses.map((course) => [course.publishCourseId, course]), - ); - - // 新的在老的里面存在 那么更新 - for (const [newCourseIndex, newCourseInfo] of coursePackInfo.courses.entries()) { - if (oldCourseMap.has(newCourseInfo.publishCourseId)) { - // Update existing course - await tx - .update(courseSchema) - .set({ - title: newCourseInfo.title, - description: newCourseInfo.description, - }) - .where(eq(courseSchema.id, newCourseInfo.publishCourseId)); - - courseIds.push(newCourseInfo.publishCourseId); - - await _updateStatements(newCourseInfo.publishCourseId, newCourseInfo.statements); - } else { - // Create new course - // 新的在老的里面不存在 那么创建 - const [courseEntity] = await tx - .insert(courseSchema) - .values({ - title: newCourseInfo.title, - description: newCourseInfo.description, - order: newCourseIndex + 1, - coursePackId: coursePackId, - }) - .returning({ - id: courseSchema.id, - order: courseSchema.order, - title: courseSchema.title, - }); - - courseIds.push(courseEntity.id.toString()); - - const createStatementTasks = newCourseInfo.statements.map( - async ({ chinese, english, phonetic }, sIndex) => { - return tx.insert(statementSchema).values({ - chinese, - english, - soundmark: phonetic, - order: sIndex + 1, - courseId: courseEntity.id, - }); - }, - ); - - logger.debug("开始创建 statements"); - await Promise.all(createStatementTasks); - logger.debug("创建 statements 完成"); - } - } - - // 老的在新的里面不存在 那么删除 - for (const oldCourse of oldCourses) { - if (!newCourseMap.has(oldCourse.id)) { - // Delete course if it is not in the new course pack info - await tx.delete(statementSchema).where(eq(statementSchema.courseId, oldCourse.id)); - await tx.delete(courseSchema).where(eq(courseSchema.id, oldCourse.id)); - - // Delete related records in course_history and user_course_progress - await tx - .delete(courseHistorySchema) - .where( - and( - eq(courseHistorySchema.coursePackId, coursePackId), - eq(courseHistorySchema.courseId, oldCourse.id), - ), - ); - - await tx - .delete(userCourseProgressSchema) - .where( - and( - eq(userCourseProgressSchema.coursePackId, coursePackId), - eq(userCourseProgressSchema.courseId, oldCourse.id), - ), - ); - } - } - - return courseIds; - } - - async function _updateStatements(courseId: string, newStatements: Statement[]) { - const oldStatements = await tx.query.statement.findMany({ - where: eq(statementSchema.courseId, courseId), - orderBy: [asc(statementSchema.order)], - }); - - let oldIndex = 0; - let newIndex = 0; - - while (oldIndex < oldStatements.length && newIndex < newStatements.length) { - const newStatementInfo = newStatements[newIndex]; - const oldStatement = oldStatements[oldIndex]; - - await tx - .update(statementSchema) - .set({ - english: newStatementInfo.english, - chinese: newStatementInfo.chinese, - soundmark: newStatementInfo.phonetic, - }) - .where(eq(statementSchema.id, oldStatement.id)); - - oldIndex++; - newIndex++; - } - - // 如果新的课程statements更多,创建剩余的新课程 - while (newIndex < newStatements.length) { - const newStatementInfo = newStatements[newIndex]; - await tx.insert(statementSchema).values({ - english: newStatementInfo.english, - chinese: newStatementInfo.chinese, - soundmark: newStatementInfo.phonetic, - order: newIndex + 1, - courseId, - }); - newIndex++; - } - - // 如果旧的课程信息更多,删除剩余的旧课程 - while (oldIndex < oldStatements.length) { - const oldStatement = oldStatements[oldIndex]; - await tx.delete(statementSchema).where(and(eq(statementSchema.id, oldStatement.id))); - oldIndex++; - } - } - - const coursePack = await tx.query.coursePack.findFirst({ - where: and( - eq(coursePackSchema.id, coursePackId), - eq(coursePackSchema.creatorId, coursePackInfo.uId), - ), - }); - - if (!coursePack) { - throw new Error("not found course pack"); - } - - await _updateCoursePack(); - const courseIds = await _updateCourses(); - - return { courseIds }; - }); - - return result; -} diff --git a/packages/api-hub/src/routes/membership/handler.ts b/packages/api-hub/src/routes/membership/handler.ts deleted file mode 100644 index ac7b3a50..00000000 --- a/packages/api-hub/src/routes/membership/handler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FastifyReply, FastifyRequest } from "fastify"; - -import { logger } from "../../services/logger"; -import { checkMembership } from "./service"; - -export async function checkMembershipHandler( - request: FastifyRequest<{ Params: { userId: string } }>, - reply: FastifyReply, -) { - const { userId } = request.params; - try { - const membershipStatus = await checkMembership(userId); - reply.send(membershipStatus); - } catch (error) { - logger.error(error); - reply.status(500).send({ error: "Internal Server Error" }); - } -} diff --git a/packages/api-hub/src/routes/membership/index.ts b/packages/api-hub/src/routes/membership/index.ts deleted file mode 100644 index aa99d669..00000000 --- a/packages/api-hub/src/routes/membership/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FastifyInstance } from "fastify"; - -import { checkMembershipHandler } from "./handler"; -import { checkMembershipParamsSchema } from "./schema"; - -export default async (fastify: FastifyInstance) => { - fastify.get( - "/:userId", - { schema: { params: checkMembershipParamsSchema } }, - checkMembershipHandler, - ); -}; diff --git a/packages/api-hub/src/routes/membership/schema.ts b/packages/api-hub/src/routes/membership/schema.ts deleted file mode 100644 index 745e18da..00000000 --- a/packages/api-hub/src/routes/membership/schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { FromSchema } from "json-schema-to-ts"; - -export const checkMembershipParamsSchema = { - type: "object", - properties: { - userId: { type: "string" }, - }, - required: ["userId"], -} as const; - -export type CheckMembershipParamsSchema = FromSchema; diff --git a/packages/api-hub/src/routes/membership/service.ts b/packages/api-hub/src/routes/membership/service.ts deleted file mode 100644 index 2028fbe3..00000000 --- a/packages/api-hub/src/routes/membership/service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { eq } from "drizzle-orm"; - -import { membership as membershipSchema } from "@earthworm/schema"; -import { db } from "../../db"; - -export async function checkMembership( - userId: string, -): Promise<{ isActive: boolean; endDate: Date | null }> { - const membershipEntity = await db.query.membership.findFirst({ - where: eq(membershipSchema.userId, userId), - }); - - const isActive = membershipEntity ? !!membershipEntity.isActive : false; - return { - isActive, - endDate: membershipEntity ? membershipEntity.end_date : null, - }; -} diff --git a/packages/api-hub/src/routes/root.ts b/packages/api-hub/src/routes/root.ts deleted file mode 100644 index a200497b..00000000 --- a/packages/api-hub/src/routes/root.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FastifyPluginAsync } from "fastify"; - -const root: FastifyPluginAsync = async (fastify) => { - fastify.get("/", async function () { - return { root: "hi this is api-hub" }; - }); -}; - -export default root; diff --git a/packages/api-hub/src/services/env.ts b/packages/api-hub/src/services/env.ts deleted file mode 100644 index bb932723..00000000 --- a/packages/api-hub/src/services/env.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { join } from "path"; - -import dotenv from "dotenv"; - -const envName = - process.env.NODE_ENV === "prod" - ? ".env.prod" - : process.env.NODE_ENV === "test" - ? ".env.test" - : ".env"; - -console.log(envName); -dotenv.config({ - path: join(__dirname, `../../${envName}`), -}); diff --git a/packages/api-hub/src/services/logger.ts b/packages/api-hub/src/services/logger.ts deleted file mode 100644 index be45bfad..00000000 --- a/packages/api-hub/src/services/logger.ts +++ /dev/null @@ -1,9 +0,0 @@ -import pino from "pino"; - -const isTestEnv = process.env.NODE_ENV === "test"; -export const logger = pino({ - level: isTestEnv ? "silent" : "info", - transport: { - target: "pino-pretty", - }, -}); diff --git a/packages/api-hub/tests/service.spec.ts b/packages/api-hub/tests/service.spec.ts deleted file mode 100644 index 9cf7b959..00000000 --- a/packages/api-hub/tests/service.spec.ts +++ /dev/null @@ -1,781 +0,0 @@ -import { and, asc, eq, or } from "drizzle-orm"; -import { afterAll, beforeEach, describe, expect, it } from "vitest"; - -import { - courseHistory as courseHistorySchema, - coursePack as coursePackSchema, - course as courseSchema, - statement as statementSchema, - userCourseProgress as userCourseProgressSchema, -} from "@earthworm/schema"; -import { cleanDB, db } from "../src/db"; -import { - createCoursePack, - deleteCoursePack, - updateCoursePack, -} from "../src/routes/course-pack/service"; - -describe("course pack service", () => { - beforeEach(async () => { - // 清空数据库 - await cleanDB(db); - }); - - afterAll(async () => { - await cleanDB(db); - }); - - describe("create course pack", () => { - it("should create a new course pack with all fields correctly provided", async () => { - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - expect(result).toBeDefined(); - expect(result.coursePackId).toBeDefined(); - expect(result.courseIds.length).toBe(mockData.courses.length); - }); - - it("should return the correct result with course pack ID, title, and course ID list", async () => { - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - expect(result).toBeDefined(); - expect(result.coursePackId).toBeDefined(); - expect(result.courseIds.length).toBe(mockData.courses.length); - }); - - it("should correctly calculate the course pack order", async () => { - const mockData = createCoursePackMockData(); - - // Create the first course pack - const firstResult = await createCoursePack(mockData); - expect(firstResult.coursePackId).toBeDefined(); - - // Query the order of the first course pack - const firstCoursePackOrder = await db.query.coursePack.findFirst({ - where: (coursePacks, { eq }) => eq(coursePacks.id, firstResult.coursePackId), - columns: { order: true }, - }); - expect(firstCoursePackOrder!.order).toBe(1); - - // Create the second course pack - const secondResult = await createCoursePack(mockData); - expect(secondResult.coursePackId).toBeDefined(); - - // Query the order of the second course pack - const secondCoursePackOrder = await db.query.coursePack.findFirst({ - where: (coursePacks, { eq }) => eq(coursePacks.id, secondResult.coursePackId), - columns: { order: true }, - }); - expect(secondCoursePackOrder!.order).toBe(2); - }); - }); - - describe("delete course pack", () => { - it("should delete a course pack and related entities", async () => { - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - const deleteResult = await deleteCoursePack(result.coursePackId); - expect(deleteResult).toBe(true); - - // Check if the course pack is deleted - const coursePack = await db.query.coursePack.findFirst({ - where: eq(coursePackSchema.id, result.coursePackId), - }); - expect(coursePack).toBeUndefined(); - - // Check if the related courses are deleted - const courses = await db.query.course.findMany({ - where: eq(courseSchema.coursePackId, result.coursePackId), - }); - expect(courses.length).toBe(0); - - // Check if the related statements are deleted - const statements = await db.query.statement.findMany({ - where: or(...result.courseIds.map((courseId) => eq(statementSchema.courseId, courseId))), - }); - expect(statements.length).toBe(0); - }); - - it("should delete course history related to the course pack", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - expect(result).toBeDefined(); - expect(result.coursePackId).toBeDefined(); - - // Insert course history data - await db.insert(courseHistorySchema).values({ - coursePackId: result.coursePackId, - courseId: "", - completionCount: 1, - userId: "user123", - }); - - // Delete the course pack - const deleteResult = await deleteCoursePack(result.coursePackId); - expect(deleteResult).toBe(true); - - // Check if the course history is deleted - const courseHistory = await db.query.courseHistory.findMany({ - where: eq(courseHistorySchema.coursePackId, result.coursePackId), - }); - expect(courseHistory.length).toBe(0); - }); - - it("should delete user course progress related to the course pack", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - expect(result).toBeDefined(); - expect(result.coursePackId).toBeDefined(); - - // Insert user course progress data - await db.insert(userCourseProgressSchema).values({ - coursePackId: result.coursePackId, - userId: "user123", - courseId: "", - statementIndex: 0, - }); - - // Delete the course pack - const deleteResult = await deleteCoursePack(result.coursePackId); - expect(deleteResult).toBe(true); - - // Check if the user course progress is deleted - const userCourseProgress = await db.query.userCourseProgress.findMany({ - where: eq(userCourseProgressSchema.coursePackId, result.coursePackId), - }); - expect(userCourseProgress.length).toBe(0); - }); - - it("should not delete course history unrelated to the course pack", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - expect(result).toBeDefined(); - expect(result.coursePackId).toBeDefined(); - - // Insert unrelated course history data - await db.insert(courseHistorySchema).values({ - coursePackId: "unrelated-course-pack-id", - userId: "user123", - courseId: "", - completionCount: 50, - }); - - // Delete the course pack - const deleteResult = await deleteCoursePack(result.coursePackId); - expect(deleteResult).toBe(true); - - // Check if the unrelated course history is still present - const courseHistory = await db.query.courseHistory.findMany({ - where: eq(courseHistorySchema.coursePackId, "unrelated-course-pack-id"), - }); - expect(courseHistory.length).toBe(1); - }); - - it("should not delete user course progress unrelated to the course pack", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - expect(result).toBeDefined(); - expect(result.coursePackId).toBeDefined(); - - // Insert unrelated user course progress data - await db.insert(userCourseProgressSchema).values({ - coursePackId: "unrelated-course-pack-id", - userId: "user123", - courseId: "", - statementIndex: 0, - }); - - // Delete the course pack - const deleteResult = await deleteCoursePack(result.coursePackId); - expect(deleteResult).toBe(true); - - // Check if the unrelated user course progress is still present - const userCourseProgress = await db.query.userCourseProgress.findMany({ - where: eq(userCourseProgressSchema.coursePackId, "unrelated-course-pack-id"), - }); - expect(userCourseProgress.length).toBe(1); - }); - - it("should return false if the course pack does not exist", async () => { - await expect(async () => { - await deleteCoursePack("non-existent-id"); - }).rejects.toThrow("not found course pack"); - }); - }); - - describe("update course pack", () => { - it("should update basic information of the course pack", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Update course pack information - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - courses: [], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toEqual({ - courseIds: [], - }); - - // Check if the course pack information is updated - const updatedCoursePack = await db.query.coursePack.findFirst({ - where: eq(coursePackSchema.id, result.coursePackId), - }); - - expect(updatedCoursePack).toBeDefined(); - expect(updatedCoursePack!.title).toBe(updateInfo.title); - expect(updatedCoursePack!.description).toBe(updateInfo.description); - expect(updatedCoursePack!.cover).toBe(updateInfo.cover); - }); - - it("should only allow the creator to update the course pack information", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Attempt to update with a different user ID - const updateInfo = { - uId: "different-user-id", - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - courses: [], - }; - - await expect(async () => { - await updateCoursePack(result.coursePackId, updateInfo); - }).rejects.toThrow("not found course pack"); - }); - - it("should return false if the course pack does not exist", async () => { - // Attempt to update a non-existent course pack - const updateInfo = { - uId: "user123", - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - courses: [], - }; - - await expect(async () => { - await updateCoursePack("non-existent-course-pack-id", updateInfo); - }).rejects.toThrow("not found course pack"); - }); - - describe("update course", () => { - it("should update existing course information", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Update course information - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], - statements: mockData.courses[0].statements, - }, - ], - }; - - await updateCoursePack(result.coursePackId, updateInfo); - // Check if the course information is updated - const updatedCourse = await db.query.course.findFirst({ - where: eq(courseSchema.id, updateInfo.courses[0].publishCourseId), - }); - - expect(updatedCourse).toBeDefined(); - expect(updatedCourse!.title).toBe(updateInfo.courses[0].title); - expect(updatedCourse!.description).toBe(updateInfo.courses[0].description); - }); - - it("should create new courses and add them to the course pack", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - mockData.courses[0].publishCourseId = result.courseIds[0]; - mockData.courses[1].publishCourseId = result.courseIds[1]; - // Update course pack with new courses - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - ...mockData.courses, - { - title: "New Course Title", - description: "New Course Description", - publishCourseId: "", - statements: [ - { - english: "New English Statement", - chinese: "New Chinese Statement", - phonetic: "New Phonetic", - }, - ], - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the new course is created - const newCourseId = updateResult.courseIds[2] || ""; - const newCourse = await db.query.course.findFirst({ - where: eq(courseSchema.id, newCourseId), - }); - - expect(newCourse).toBeDefined(); - expect(newCourse!.title).toBe(updateInfo.courses[2].title); - expect(newCourse!.description).toBe(updateInfo.courses[2].description); - }); - - it("should delete existing courses that are no longer needed", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Update course pack with fewer courses - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], - statements: mockData.courses[0].statements, - }, - ], - }; - await updateCoursePack(result.coursePackId, updateInfo); - // 删除了一个 只剩下一个 - const courses = await db.query.course.findMany(); - expect(courses.length).toBe(1); - }); - - it("should delete course history records when a course is deleted", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Insert course history records - await db.insert(courseHistorySchema).values({ - coursePackId: result.coursePackId, - courseId: result.courseIds[1], // Use the publishCourseId from the result - userId: "user123", - completionCount: 50, - }); - - // Update course pack with fewer courses - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: mockData.courses[0].statements, - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the course history records are deleted - const courseHistory = await db.query.courseHistory.findFirst({ - where: and( - eq(courseHistorySchema.coursePackId, result.coursePackId), - eq(courseHistorySchema.courseId, result.courseIds[1]), // Use the publishCourseId from the result - ), - }); - - expect(courseHistory).toBeUndefined(); - }); - - it("should delete user course progress records when a course is deleted", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Insert user course progress records - await db.insert(userCourseProgressSchema).values({ - coursePackId: result.coursePackId, - courseId: result.courseIds[1], // Use the publishCourseId from the result - userId: "user123", - statementIndex: 1, - }); - - // Update course pack with fewer courses - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: mockData.courses[0].statements, - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the user course progress records are deleted - const userCourseProgress = await db.query.userCourseProgress.findFirst({ - where: and( - eq(userCourseProgressSchema.coursePackId, result.coursePackId), - eq(userCourseProgressSchema.courseId, result.courseIds[1]), // Use the publishCourseId from the result - ), - }); - - expect(userCourseProgress).toBeUndefined(); - }); - - it("should not affect publishCourseId in courseHistorySchema after deleting a course", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Insert course history records - await db.insert(courseHistorySchema).values({ - coursePackId: result.coursePackId, - courseId: result.courseIds[0], // Use the publishCourseId from the result - userId: "user123", - completionCount: 50, - }); - - // Update course pack with fewer courses - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: mockData.courses[0].statements, - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the course history records are intact - const courseHistory = await db.query.courseHistory.findFirst({ - where: and( - eq(courseHistorySchema.coursePackId, result.coursePackId), - eq(courseHistorySchema.courseId, result.courseIds[0]), // Use the publishCourseId from the result - ), - }); - - expect(courseHistory).toBeDefined(); - }); - - it("should not affect publishCourseId in userCourseProgressSchema after deleting a course", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - expect(result).toBeDefined(); - expect(result.coursePackId).toBeDefined(); - - // Insert user course progress records - await db.insert(userCourseProgressSchema).values({ - coursePackId: result.coursePackId, - courseId: result.courseIds[0], // Use the publishCourseId from the result - userId: "user123", - statementIndex: 1, - }); - - // Update course pack with fewer courses - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: mockData.courses[0].statements, - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the user course progress records are intact - const userCourseProgress = await db.query.userCourseProgress.findFirst({ - where: and( - eq(userCourseProgressSchema.coursePackId, result.coursePackId), - eq(userCourseProgressSchema.courseId, result.courseIds[0]), // Use the publishCourseId from the result - ), - }); - - expect(userCourseProgress).toBeDefined(); - }); - }); - - describe("update statements", () => { - it("should update existing statement information", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Update course pack with updated statements - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: [ - { - english: "Updated English Statement", - chinese: "Updated Chinese Statement", - phonetic: "Updated Phonetic", - }, - ], - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the statement information is updated - const updatedStatement = await db.query.statement.findFirst({ - where: eq(statementSchema.courseId, result.courseIds[0]), // Use the publishCourseId from the result - }); - - expect(updatedStatement).toBeDefined(); - expect(updatedStatement!.english).toBe(updateInfo.courses[0].statements[0].english); - expect(updatedStatement!.chinese).toBe(updateInfo.courses[0].statements[0].chinese); - expect(updatedStatement!.soundmark).toBe(updateInfo.courses[0].statements[0].phonetic); - }); - - it("should create new statements and add them to the course", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Update course pack with new statements - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: [ - ...mockData.courses[0].statements, - { - english: "New English Statement", - chinese: "New Chinese Statement", - phonetic: "New Phonetic", - }, - ], - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the new statement is created - const newStatement = await db.query.statement.findFirst({ - where: and( - eq(statementSchema.courseId, result.courseIds[0]), // Use the publishCourseId from the result - eq(statementSchema.english, "New English Statement"), - ), - }); - - expect(newStatement).toBeDefined(); - expect(newStatement!.chinese).toBe("New Chinese Statement"); - expect(newStatement!.soundmark).toBe("New Phonetic"); - }); - - it("should delete existing statements that are no longer needed", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Update course pack with fewer statements - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: [ - { - english: "Updated English Statement", - chinese: "Updated Chinese Statement", - phonetic: "Updated Phonetic", - }, - ], - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the statement is deleted - const deletedStatement = await db.query.statement.findFirst({ - where: and( - eq(statementSchema.courseId, result.courseIds[0]), // Use the publishCourseId from the result - eq(statementSchema.english, mockData.courses[0].statements[1].english), - ), - }); - - expect(deletedStatement).toBeUndefined(); - }); - - it("should update statement order", async () => { - // Create a course pack - const mockData = createCoursePackMockData(); - const result = await createCoursePack(mockData); - - // Update course pack with reordered statements - const updateInfo = { - uId: mockData.uId, - title: "Updated Title", - description: "Updated Description", - cover: "https://example.com/updated-cover.jpg", - shareLevel: "public", - courses: [ - { - title: "Updated Course Title 1", - description: "Updated Course Description 1", - publishCourseId: result.courseIds[0], // Use the publishCourseId from the result - statements: [mockData.courses[0].statements[1], mockData.courses[0].statements[0]], - }, - ], - }; - - const updateResult = await updateCoursePack(result.coursePackId, updateInfo); - expect(updateResult).toBeDefined(); - - // Check if the statement order is updated - const statements = await db.query.statement.findMany({ - where: eq(statementSchema.courseId, result.courseIds[0]), // Use the publishCourseId from the result - orderBy: [asc(statementSchema.order)], - }); - - expect(statements).toBeDefined(); - expect(statements.length).toBe(2); - expect(statements[0].english).toBe(mockData.courses[0].statements[1].english); - expect(statements[1].english).toBe(mockData.courses[0].statements[0].english); - }); - }); - }); -}); - -function createCoursePackMockData() { - return { - title: "Advanced English Course Pack", - description: "This course pack is designed for advanced English learners.", - cover: "https://example.com/cover.jpg", - uId: "user123", - shareLevel: "public", - courses: [ - { - title: "Advanced Grammar", - description: "Deep dive into advanced English grammar concepts.", - publishCourseId: "", - statements: [ - { - english: "The quick brown fox jumps over the lazy dog.", - phonetic: "/ðə kwɪk braʊn fɑks dʒʌmps oʊvər ðə leɪzi dɔɡ/", - chinese: "快速的棕色狐狸跳过懒狗。", - }, - { - english: "She sells seashells by the seashore.", - phonetic: "/ʃi sɛlz siːʃɛlz baɪ ðə siːʃɔːr/", - chinese: "她在海边卖贝壳。", - }, - ], - }, - { - title: "Advanced Vocabulary", - description: "Expand your vocabulary with advanced English words.", - publishCourseId: "", - statements: [ - { - english: "He is an erudite scholar with extensive knowledge.", - phonetic: "/hiː ɪz ən ˈɜːr.daɪt ˈskɒl.ər wɪð ɪkˈstɛn.sɪv ˈnɒl.ɪdʒ/", - chinese: "他是一位博学的学者,拥有广泛的知识。", - }, - { - english: "The symposium encompassed a wide range of topics.", - phonetic: "/ðə ˈsɪm.pə.zi.əm ɛn.kʌmp.əsəd ə waɪd reɪndʒ ʌv ˈtɑ.pɪks/", - chinese: "研讨会涵盖了广泛的主题。", - }, - ], - }, - ], - }; -} diff --git a/packages/api-hub/tsconfig.json b/packages/api-hub/tsconfig.json deleted file mode 100644 index 0dda7b18..00000000 --- a/packages/api-hub/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "CommonJS", - "outDir": "dist", - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "baseUrl": ".", - "paths": { - "~/*": ["src/*"] - }, - "skipLibCheck": true - }, - "exclude": ["node_modules", "**/*.spec.ts"], - "include": ["./src/**/*.ts"] -} diff --git a/packages/api-hub/vitest.config.ts b/packages/api-hub/vitest.config.ts deleted file mode 100644 index 1da16d5e..00000000 --- a/packages/api-hub/vitest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import path from "path"; - -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(__dirname, "src"), - }, - }, - - test: { - globals: true, - environment: "happy-dom", - setupFiles: ["./vitest.setup.ts"], - }, -}); diff --git a/packages/api-hub/vitest.setup.ts b/packages/api-hub/vitest.setup.ts deleted file mode 100644 index ce5501c1..00000000 --- a/packages/api-hub/vitest.setup.ts +++ /dev/null @@ -1,14 +0,0 @@ -import "./src/services/env"; - -import { afterAll, beforeAll } from "vitest"; - -import { setupDb, teardownDb } from "./src/db"; - -beforeAll(async () => { - // 创建连接数据库 - await setupDb(); -}); - -afterAll(async () => { - await teardownDb(); -}); diff --git a/packages/schema/package.json b/packages/schema/package.json index ee834dd0..030b38a7 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,6 +1,6 @@ { "name": "@earthworm/schema", - "version": "1.0.0", + "version": "1.0.1", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts",