Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"formatter": {
"indentStyle": "space",
"indentWidth": 2
"indentWidth": 2,
"lineWidth": 80
},
"linter": {
"rules": {
Expand Down
Binary file removed bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type {
RelationshipID,
Relationship,
CourseID,
InterestSubjectID,
Slot,
Course,
Enrollment,
Expand Down
24 changes: 13 additions & 11 deletions common/zod/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,6 @@ export const IntroLongSchema = z
// .min(2, { message: "自己紹介文は2文字以上です" })
.max(225, { message: "自己紹介文は225文字以下です" });

export const InterestSubjectSchema = z.object({
id: z.number(),
name: z.string(),
group: z.string(),
});

export const InterestSchema = z.object({
userId: UserIDSchema,
subjectId: z.number(),
});

export const UserSchema = z.object({
id: UserIDSchema,
guid: GUIDSchema,
Expand Down Expand Up @@ -76,6 +65,8 @@ export const RelationshipSchema = z.object({

export const CourseIDSchema = z.string();

export const InterestSubjectIDSchema = z.number();

export const DaySchema = z.enum([
"mon",
"tue",
Expand Down Expand Up @@ -108,6 +99,17 @@ export const EnrollmentSchema = z.object({
courseId: CourseIDSchema,
});

export const InterestSubjectSchema = z.object({
id: InterestSubjectIDSchema,
name: z.string(),
group: z.string(),
});

export const InterestSchema = z.object({
userId: UserIDSchema,
subjectId: InterestSubjectIDSchema,
});

export const UserWithCoursesAndSubjectsSchema = UserSchema.extend({
courses: CourseSchema.array(),
interestSubjects: InterestSubjectSchema.array(),
Expand Down
2 changes: 2 additions & 0 deletions common/zod/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
InitSharedRoomSchema,
InitUserSchema,
InterestSchema,
InterestSubjectIDSchema,
InterestSubjectSchema,
IntroLongSchema,
IntroShortSchema,
Expand Down Expand Up @@ -57,6 +58,7 @@ export type Step1User = z.infer<typeof Step1UserSchema>;
export type RelationshipID = z.infer<typeof RelationshipIDSchema>;
export type Relationship = z.infer<typeof RelationshipSchema>;
export type CourseID = z.infer<typeof CourseIDSchema>;
export type InterestSubjectID = z.infer<typeof InterestSubjectIDSchema>;
export type Slot = z.infer<typeof SlotSchema>;
export type Course = z.infer<typeof CourseSchema>;
export type Enrollment = z.infer<typeof EnrollmentSchema>;
Expand Down
2 changes: 2 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node";
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine";
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt";

export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib
'';
};
in
Expand Down
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
"license": "ISC",
"devDependencies": {
"@biomejs/biome": "^1.9.1",
"husky": "^9.1.4",
"lint-staged": "^15.2.10"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"react-hook-form": "^7.53.2",
"@types/bun": "^1.1.10",
"cspell": "^8.14.4",
"typescript": "^5.6.2",
"lefthook": "^1.8.2"
},
"dependencies": {
"lefthook": "^1.8.2",
"zod": "^3.23.8"
},
"trustedDependencies": ["@biomejs/biome", "lefthook"]
Expand Down
35 changes: 35 additions & 0 deletions server/src/database/interest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ export async function get(id: number): Promise<InterestSubject | null> {
return await prisma.interestSubject.findUnique({ where: { id } });
}

export async function create(name: string): Promise<InterestSubject> {
const existingTag = await prisma.interestSubject.findMany({
where: { name },
});
if (existingTag.length > 0) {
throw new Error("同名のタグがすでに存在します");
}
return await prisma.interestSubject.create({
data: {
name,
group: "", // TODO: 運用次第
},
});
}

export async function of(userId: UserID): Promise<InterestSubject[]> {
return await prisma.interest
.findMany({
Expand Down Expand Up @@ -37,3 +52,23 @@ export async function remove(userId: UserID, subjectId: number) {
},
});
}

export async function updateMultipleWithTransaction(
userId: UserID,
subjectIds: number[],
) {
return await prisma.$transaction(async (prisma) => {
await prisma.interest.deleteMany({
where: {
userId,
},
});

await prisma.interest.createMany({
data: subjectIds.map((subjectId) => ({
userId,
subjectId,
})),
});
});
}
2 changes: 2 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import coursesRoutes from "./router/courses";
import matchesRoutes from "./router/matches";
import pictureRoutes from "./router/picture";
import requestsRoutes from "./router/requests";
import subjectsRoutes from "./router/subjects";
import usersRoutes from "./router/users";

const app = express();
Expand Down Expand Up @@ -52,6 +53,7 @@ app.get("/", (_, res) => {
app.use("/picture", pictureRoutes);
app.use("/users", usersRoutes);
app.use("/courses", coursesRoutes);
app.use("/subjects", subjectsRoutes);
app.use("/requests", requestsRoutes);
app.use("/matches", matchesRoutes);
app.use("/chat", chatRoutes);
Expand Down
131 changes: 131 additions & 0 deletions server/src/router/subjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import express, { type Request, type Response } from "express";
import * as interest from "../database/interest";
import { safeGetUserId } from "../firebase/auth/db";

const router = express.Router();

router.get("/userId/:userId", async (req: Request, res: Response) => {
const userId = Number.parseInt(req.params.userId);
if (Number.isNaN(userId)) {
return res.status(400).json({ error: "Invalid userId" });
}
try {
const subjects = await interest.of(userId);
res.status(200).json(subjects);
} catch (error) {
console.error("Error fetching subjects by userId:", error);
res.status(500).json({ error: "Failed to fetch subjects by userId" });
}
});

router.get("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
try {
const subjects = await interest.of(userId.value);
res.status(200).json(subjects);
} catch (error) {
console.error("Error fetching subjects:", error);
res.status(500).json({ error: "Failed to fetch subjects" });
}
});

router.post("/", async (req: Request, res: Response) => {
const { name } = req.body;
if (typeof name !== "string") {
return res.status(400).json({ error: "name must be a string" });
}
try {
const newSubject = await interest.create(name);
res.status(201).json(newSubject);
} catch (error) {
console.error("Error creating subject:", error);
res.status(500).json({ error: "Failed to create subject" });
}
});

router.patch("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
const { subjectId } = req.body;
try {
const newSubject = await interest.get(subjectId);
if (!newSubject) {
return res.status(404).json({ error: "Subject not found" });
}
} catch (err) {
console.error("Error fetching subject:", err);
res.status(500).json({ error: "Failed to fetch subject" });
}
try {
const updatedSubjects = await interest.add(userId.value, subjectId);
res.status(200).json(updatedSubjects);
} catch (error) {
console.error("Error updating subjects:", error);
res.status(500).json({ error: "Failed to update subjects" });
}
});

router.delete("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
const { subjectId } = req.body;
try {
const newSubject = await interest.get(subjectId);
if (!newSubject) {
return res.status(404).json({ error: "Subject not found" });
}
} catch (err) {
console.error("Error fetching subject:", err);
res.status(500).json({ error: "Failed to fetch subject" });
}
try {
const updatedSubjects = await interest.remove(userId.value, subjectId);
res.status(200).json(updatedSubjects);
} catch (error) {
console.error("Error deleting subjects:", error);
res.status(500).json({ error: "Failed to delete subjects" });
}
});

router.put("/mine", async (req: Request, res: Response) => {
const userId = await safeGetUserId(req);
if (!userId.ok) return res.status(401).send("auth error");
const { subjectIds } = req.body;
if (!Array.isArray(subjectIds)) {
return res.status(400).json({ error: "subjectIds must be an array" });
}
try {
const newSubjects = await Promise.all(
subjectIds.map((id) => interest.get(id)),
);
if (newSubjects.some((s) => !s)) {
return res.status(404).json({ error: "Subject not found" });
}
} catch (err) {
console.error("Error fetching subjects:", err);
res.status(500).json({ error: "Failed to fetch subjects" });
}
try {
const updatedSubjects = await interest.updateMultipleWithTransaction(
userId.value,
subjectIds,
);
res.status(200).json(updatedSubjects);
} catch (error) {
console.error("Error updating subjects:", error);
res.status(500).json({ error: "Failed to update subjects" });
}
});

router.get("/all", async (req: Request, res: Response) => {
try {
const subjects = await interest.all();
res.status(200).json(subjects);
} catch (error) {
console.error("Error fetching subjects:", error);
res.status(500).json({ error: "Failed to fetch subjects" });
}
});

export default router;
71 changes: 71 additions & 0 deletions web/api/internal/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,73 @@ export const coursesDayPeriod = (day: Day, period: number) => {
return `${API_ENDPOINT}/courses/day-period?day=${day}&period=${period}`;
};

/**
* [v] 実装済み
* POST -> create a new subject.
* - statuses:
* - 200: ok.
* - body: InterestSubject
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjects = `${API_ENDPOINT}/subjects`;

/**
* [v] 実装済み
* GET -> get subjects the user is interested in.
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjectsUserId = (userId: UserID) => {
return `${API_ENDPOINT}/subjects/userId/${userId}`;
};

/**
* [v] 実装済み
* GET -> get my subjects.
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
* PATCH -> update my subjects.
* - request body: SubjectId
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
* DELETE -> delete my subjects.
* - request body: SubjectId
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
* PUT → replace my subjects.
* - request body: SubjectId[]
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjectsMine = `${API_ENDPOINT}/subjects/mine`;

/**
* [v] 実装済み
* GET -> get all subjects.
* - statuses:
* - 200: ok.
* - body: InterestSubject[]
* - 401: unauthorized.
* - 500: internal error.
*/
export const subjectsAll = `${API_ENDPOINT}/subjects/all`;

/**
* [v] 実装済み
* PUT -> create request.
Expand Down Expand Up @@ -400,6 +467,10 @@ export default {
cancelRequest,
coursesUserId,
coursesDayPeriod,
subjects,
subjectsUserId,
subjectsMine,
subjectsAll,
roomOverview,
dmTo,
dmWith,
Expand Down
Loading
Loading