From ac20027a072b41f1bfa87b0c5cc82908f3d86120 Mon Sep 17 00:00:00 2001 From: Karthik Kanithi Date: Tue, 21 Jul 2026 00:17:54 +0530 Subject: [PATCH 1/2] feat: extend API to support all 4 college years MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What's New ### Student Group Endpoint - Extended /students/getGroup to support all batches (2022–2026) - Batch auto-detected from roll number prefix (e.g. 24xxxxxx → batch2024) - Removed first-year-only enum constraint; now returns generic group/subGroup strings ### Timetable Endpoints - Added GET /timetable/year/:year for 2nd, 3rd, and 4th year students - New lib/higherYears.ts maps branch abbreviations to Xceed department names - Automatically resolves odd/even semester from live Xceed session API - Supports 12 branch abbreviations (CSE, IT, ECE, EE, ME, CE, BT, CH, ICE, IPE, TT, MCE) ### Database Schema - Added batch2022, batch2023, batch2024, batch2026 tables to schema.ts - Added seed.ts to initialise and populate all batch tables ### Testing - Added test.spec.ts with 9 integration tests covering all new endpoints - Tests use global fetch mocking for fast, network-isolated runs (79ms) ### Security Hardening - Added CORS (GET-only), secureHeaders, and global error handler middleware - Roll numbers validated: 8–10 digits, year prefix 20–35 - All external Xceed fetches wrapped with 8-second AbortController timeout - Input parameters constrained via Zod: branch 2–5 alpha chars, group max 3 alphanumeric - Error messages scrubbed — internal details logged server-side only - Fixed uniqueDept[0] assumption: now searches by name ('Basic Sciences') - Validated Xceed code response shape before using in URL construction - All parseInt calls use explicit radix 10 ### Documentation - Fully rewrote README.md with setup instructions, all endpoint docs, branch abbreviation table, database schema section, and security notes --- README.md | 260 +++- app.ts | 18 + lib/db/schema.ts | 36 + lib/firstYear.ts | 72 +- lib/higherYears.ts | 147 +++ package-lock.json | 1800 ++++++++++++++++++++++++++ routes/students/getGroup.ts | 24 +- routes/timetable/index.ts | 2 + routes/timetable/year/1.ts | 42 +- routes/timetable/year/higherYears.ts | 234 ++++ seed.ts | 64 + test.spec.ts | 216 ++++ 12 files changed, 2821 insertions(+), 94 deletions(-) create mode 100644 lib/higherYears.ts create mode 100644 package-lock.json create mode 100644 routes/timetable/year/higherYears.ts create mode 100644 seed.ts create mode 100644 test.spec.ts diff --git a/README.md b/README.md index 839aa56..a4e480d 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,77 @@

-### Features: +### Features -- Get student details using roll number -- Get simplified view of timetable to embed into your applications +- Get student group or section using roll number — supports **all 4 years** +- Get timetables for **1st, 2nd, 3rd, and 4th year** students, fetched live from Xceed - Get real-time library seat availability +### Tech Stack + +- **Runtime**: [Bun](https://bun.sh) +- **Framework**: [Hono](https://hono.dev) with `@hono/zod-openapi` +- **Database**: SQLite via [Drizzle ORM](https://orm.drizzle.team) +- **Validation**: [Zod](https://zod.dev) + +--- + +## Getting Started + +### Prerequisites + +- [Bun](https://bun.sh) v1.0+ + +### Installation + +```bash +bun install +``` + +### Database Setup + +Before starting the server, seed the database with student data: + +```bash +bun seed.ts +``` + +> **Note:** `seed.ts` creates all batch tables (`batch2022`–`batch2026`) and inserts sample records. Replace these with your real data before deploying. + +### Running the Server + +```bash +bun server.ts +``` + +Server starts on port **6969** by default. + +### Running Tests + +```bash +bun test test.spec.ts +``` + +### OpenAPI Docs + +The full interactive OpenAPI spec is served at: + +``` +http://localhost:6969/openapi +``` + +--- + ## Routes +### Base URL + +``` +https://api.opensourcenitj.com +``` + +--- + ### Health Check ```http @@ -88,7 +151,7 @@ Returns real-time library seat availability information across all floors. | totalCapacity | number | Total seating capacity of the library | | capacity | object[] | Floor-wise seating statistics | -#### capacity Object +#### `capacity` Object | Field | Type | Description | | ---------------- | -------- | -------------------------------------------- | @@ -101,30 +164,24 @@ Returns real-time library seat availability information across all floors. #### Error Response (404) ```json -{ - "error": "Library information not found" -} +{ "error": "Library information not found" } ``` -| Field | Type | Description | -| ----- | ------ | ------------- | -| error | string | Error message | - --- -### Get Student Group (First Year Only) +### Get Student Group ```http GET /students/getGroup ``` -Returns the group and subgroup (e.g., `B6`, `a`) of a first-year student based on their roll number. +Returns the group/section and subgroup of a student based on their roll number. Supports students from **all years** (batches 2022–2026). The batch is automatically inferred from the first two digits of the roll number. #### Query Parameters -| Parameter | Type | Description | -| ---------- | ------ | -------------------------------- | -| rollNumber | string | Required. Roll number of student | +| Parameter | Type | Required | Description | +| ---------- | ------ | -------- | -------------------- | +| rollNumber | string | ✅ | Roll number of student (8–10 digits) | #### Response (200) @@ -135,14 +192,18 @@ Returns the group and subgroup (e.g., `B6`, `a`) of a first-year student based o } ``` -| Field | Type | Description | -| -------- | ------ | -------------------------------- | -| group | string | Student group (`A1`-`B6`) | -| subGroup | string | Student subgroup (`a`, `b`, `c`) | +For higher-year students, `group` will reflect their branch section (e.g. `CSE-A`) and `subGroup` their lab batch identifier (e.g. `1`). + +| Field | Type | Description | +| -------- | --------------- | -------------------------------------------------- | +| group | string | Student group or section (e.g. `B6`, `CSE-A`) | +| subGroup | string \| null | Student subgroup or lab batch (e.g. `a`, `1`, `2`) | #### Error Response (404) -Group not found for the provided roll number. +```json +{ "message": "Group not found for the provided roll number." } +``` --- @@ -152,13 +213,13 @@ Group not found for the provided roll number. GET /timetable/year/1 ``` -Returns the timetable for first-year students. +Returns the live timetable for first-year students, fetched directly from Xceed. #### Query Parameters -| Parameter | Type | Description | -| --------- | ------ | --------------------------- | -| group | string | Required. Group (`A1`–`B6`) | +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | ---------------------------- | +| group | string | ✅ | Common group (`A1`–`B6`) | #### Response (200) @@ -181,13 +242,98 @@ Returns the timetable for first-year students. } ``` -#### timetableData structure +#### Error Responses + +| Status | Description | +| ------ | -------------------------------- | +| 400 | Invalid `group` parameter | +| 502 | Failed to fetch data from Xceed | +| 500 | Internal server error | + +--- + +### Get Higher Year Timetable + +```http +GET /timetable/year/:year +``` + +Returns the live timetable for **2nd, 3rd, or 4th year** students for a specific branch and section, fetched directly from Xceed. The correct semester is automatically resolved based on the current academic session (Odd/Even). + +#### Path Parameters + +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | ------------------------ | +| year | number | ✅ | Academic year: `2`, `3`, or `4` | + +#### Query Parameters + +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | ------------------------------------------------------- | +| branch | string | ✅ | Department code (see table below). Use the **parent department**, not the specific programme name. | +| group | string | ❌ | Section letter or identifier (e.g. `A`, `B`). Default: `A` | + +#### Department Codes + +> **Note:** The `branch` parameter maps to the **department** on Xceed, not your specific programme name. +> Programmes that were introduced under a parent department share its timetable infrastructure: +> +> - **DSE** (Data Science & Engineering, from ~2023) → use `CSE` +> - **AI** (Artificial Intelligence, from 2026) → use `CSE` +> - **VLSI** specialisation → use `ECE` +> - **DS** (Data Science) elective streams → use `IT` +> +> When in doubt, check which department conducts your core theory classes on Xceed. + +| Code | Department on Xceed | Programmes covered | +| ---- | ------------------- | ------------------- | +| `CSE` / `CS` | Computer Science and Engineering | B.Tech CSE, DSE (2023+), AI (2026+) | +| `IT` | Information Technology | B.Tech IT, Data Science streams | +| `ECE` / `EC` | Electronics and Communication Engineering | B.Tech ECE, VLSI specialisation | +| `EE` | Electrical Engineering | B.Tech EE | +| `ME` | Mechanical Engineering | B.Tech ME | +| `CE` | Civil Engineering | B.Tech CE | +| `BT` | Biotechnology | B.Tech BT | +| `CH` / `CHE` | Chemical Engineering | B.Tech CH | +| `ICE` | Instrumentation and Control Engineering | B.Tech ICE | +| `IPE` | Industrial and Production Engineering | B.Tech IPE | +| `TT` | Textile Technology | B.Tech TT | +| `MNC` | Mathematics & Computing | B.Tech MNC | + +#### Example Request + +```http +GET /timetable/year/2?branch=CSE&group=A +``` + +#### Response (200) + +```json +{ + "timetableData": { + "Monday": { + "period1": [ + [ + { + "subject": "DSA", + "faculty": "Lalatendu Behera", + "room": "LT-403" + } + ] + ] + } + }, + "notes": [] +} +``` + +#### `timetableData` Structure ```ts Record< - string, + string, // Day (e.g. "Monday") Record< - string, + string, // Period (e.g. "period1", "lunch") Array< Array<{ subject: string; @@ -199,32 +345,48 @@ Record< >; ``` -| Field | Type | Description | -| ------- | ------ | ------------ | -| subject | string | Subject name | -| faculty | string | Faculty name | -| room | string | Room or lab | +Each period contains an outer array (one entry per concurrent group/lab batch) and an inner array of classes happening simultaneously. -| Field | Type | Description | -| ----- | -------- | -------------------------- | -| notes | string[] | Additional timetable notes | +#### Error Responses -#### Error Response (400) +| Status | Description | +| ------ | --------------------------------------------------------- | +| 400 | Invalid `branch` or `group` parameter | +| 404 | No timetable section found for the given parameters | +| 502 | Failed to fetch data from Xceed | +| 500 | Internal server error (including upstream timeout) | -```json -{ - "error": "Invalid group parameter" -} -``` +--- -| Field | Type | Description | -| ----- | ------ | ------------- | -| error | string | Error message | +## Database Schema + +Student data is stored in per-batch SQLite tables (`batch2022` through `batch2026`). Each table has the same schema: + +| Column | Type | Description | +| ----------- | ------- | ---------------------------------- | +| rollnumber | integer | Primary key — student roll number | +| name | text | Full name | +| branchabbr | text | Branch abbreviation (e.g. `CSE`) | +| branch | text | Full branch name | +| group | text | Group or section | +| subgroup | text | Sub-group or lab batch | + +The correct batch table is automatically selected based on the first two digits of the roll number (e.g. `24xxxxxx` → `batch2024`). --- -### Base URL +## Security -``` -https://api.opensourcenitj.com -``` +- Input validated via Zod schemas on all endpoints +- Roll numbers validated to be 8–10 numeric digits with a valid year prefix +- Branch abbreviations constrained to a strict allowlist — no raw user input reaches Xceed URLs +- All Xceed fetches have an 8-second timeout +- Secure HTTP headers via `hono/secure-headers` +- CORS restricted to `GET` requests +- Generic error messages in responses — internal details logged server-side only + +--- + +## Contributing + +Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/opensourcenitj/api). diff --git a/app.ts b/app.ts index 916ce23..9f1dd59 100644 --- a/app.ts +++ b/app.ts @@ -1,12 +1,30 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { secureHeaders } from "hono/secure-headers"; import { prettyJSON } from "hono/pretty-json"; import { routes } from "./routes/_routes"; export const app = new OpenAPIHono(); +// Security middleware +app.use(secureHeaders()); +app.use(cors({ + origin: "*", + allowMethods: ["GET"], + allowHeaders: ["Content-Type"], +})); app.use(prettyJSON({ force: true })); +// Global error handler — prevents raw stack traces leaking to clients +app.onError((err, c) => { + console.error(err); + return c.json({ error: "Internal server error" }, 500); +}); + +// Global 404 handler +app.notFound((c) => c.json({ error: "Route not found" }, 404)); + for (const [path, route] of Object.entries(routes)) { app.route(path, route as Hono); } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index a4f34ed..d24e9c6 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -1,5 +1,32 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +export const batch2022Table = sqliteTable("batch2022", { + rollnumber: integer("rollnumber").primaryKey(), + name: text("name"), + branchabbr: text("branchabbr"), + branch: text("branch"), + group: text("group"), + subgroup: text("subgroup"), +}); + +export const batch2023Table = sqliteTable("batch2023", { + rollnumber: integer("rollnumber").primaryKey(), + name: text("name"), + branchabbr: text("branchabbr"), + branch: text("branch"), + group: text("group"), + subgroup: text("subgroup"), +}); + +export const batch2024Table = sqliteTable("batch2024", { + rollnumber: integer("rollnumber").primaryKey(), + name: text("name"), + branchabbr: text("branchabbr"), + branch: text("branch"), + group: text("group"), + subgroup: text("subgroup"), +}); + export const batch2025Table = sqliteTable("batch2025", { rollnumber: integer("rollnumber").primaryKey(), name: text("name"), @@ -8,3 +35,12 @@ export const batch2025Table = sqliteTable("batch2025", { group: text("group"), subgroup: text("subgroup"), }); + +export const batch2026Table = sqliteTable("batch2026", { + rollnumber: integer("rollnumber").primaryKey(), + name: text("name"), + branchabbr: text("branchabbr"), + branch: text("branch"), + group: text("group"), + subgroup: text("subgroup"), +}); diff --git a/lib/firstYear.ts b/lib/firstYear.ts index 1d109e4..590d72c 100644 --- a/lib/firstYear.ts +++ b/lib/firstYear.ts @@ -1,4 +1,3 @@ -import { fetch } from "bun"; import { eq } from "drizzle-orm"; import config from "../config.json" assert { type: "json" }; import type { studentInfo } from "../types/db"; @@ -16,14 +15,26 @@ export async function getFirstYearGroups(): Promise<{ [key: string]: semArrDataT } const currentSession = currentSessionObj.session; - const dept = sessionDeptData.uniqueDept[0]; + const dept = sessionDeptData.uniqueDept.find(d => d === "Basic Sciences"); + if (!dept) { + throw new Error("Basic Sciences department not found in Xceed session data"); + } const codeFetch = await fetch( `${config.url.deptCode}/${encodeURIComponent(`${currentSession}`)}/${encodeURIComponent(`${dept}`)}` ); + if (!codeFetch.ok) { + throw new Error("Failed to fetch department code from Xceed"); + } const code = JSON.parse(await codeFetch.text()); + if (typeof code !== "string" || code.trim() === "") { + throw new Error("Unexpected code format from Xceed API"); + } const semFetch = await fetch(`${config.url.sem}${encodeURIComponent(code)}`); + if (!semFetch.ok) { + throw new Error("Failed to fetch semester data from Xceed"); + } const semData = (await semFetch.json()) as semArrDataType; const usableData = semData.filter(semEntry => semEntry.code === code); @@ -40,16 +51,38 @@ export async function getFirstYearGroups(): Promise<{ [key: string]: semArrDataT } export async function getStudentDetails(rollNumber: string, batch: string): Promise { - const table = batch === "2025" ? dbSchema.batch2025Table : null; + let table; + switch (batch) { + case "2022": + table = dbSchema.batch2022Table; + break; + case "2023": + table = dbSchema.batch2023Table; + break; + case "2024": + table = dbSchema.batch2024Table; + break; + case "2025": + table = dbSchema.batch2025Table; + break; + case "2026": + table = dbSchema.batch2026Table; + break; + default: + return null; + } - if (!table) { + if (!/^\d+$/.test(rollNumber)) { + return null; + } + const rollInt = parseInt(rollNumber, 10); + if (isNaN(rollInt)) { return null; } - const data = await db .select() .from(table) - .where(eq(table.rollnumber, parseInt(rollNumber))); + .where(eq(table.rollnumber, rollInt)); if (data.length === 0) { return null; @@ -69,20 +102,21 @@ export async function getStudentDetails(rollNumber: string, batch: string): Prom } export async function getGroup(rollNumber: string): Promise<{ group: string; subGroup: string | null } | null> { - const groups = ["A1", "A2", "A3", "A4", "A5", "A6", "B1", "B2", "B3", "B4", "B5", "B6"]; - const subGroups = ["a", "b", "c"]; - - const studentDetails = await getStudentDetails(rollNumber, "2025"); + const rollStr = rollNumber.trim(); + // Validate: must be numeric digits only + if (!/^\d{8,10}$/.test(rollStr)) return null; + const yearDigits = rollStr.substring(0, 2); + // Validate: year prefix must be a valid recent batch (e.g. 20–30) + const yearNum = parseInt(yearDigits, 10); + if (isNaN(yearNum) || yearNum < 20 || yearNum > 35) return null; + const batch = "20" + yearDigits; + + const studentDetails = await getStudentDetails(rollStr, batch); if (studentDetails) { - const studentGroup = studentDetails.group.toUpperCase(); - const subGroup = studentDetails.subgroup.toLowerCase().slice(-1); - if (groups.includes(studentGroup)) { - if (subGroups.includes(subGroup)) { - return { group: studentGroup, subGroup: subGroup }; - } else { - return { group: studentGroup, subGroup: subGroup[0] as string }; - } - } + const studentGroup = studentDetails.group ? studentDetails.group.toUpperCase() : ""; + const subgroupStr = studentDetails.subgroup ? studentDetails.subgroup.trim() : ""; + const subGroup = subgroupStr ? subgroupStr.toLowerCase().slice(-1) : null; + return { group: studentGroup, subGroup: subGroup }; } return null; diff --git a/lib/higherYears.ts b/lib/higherYears.ts new file mode 100644 index 0000000..6ed9bdb --- /dev/null +++ b/lib/higherYears.ts @@ -0,0 +1,147 @@ +import config from "../config.json" assert { type: "json" }; +import type { + semArrDataType, + semDataType, + sessionDeptDataType, +} from "../types/xceed"; + +export const branchToDeptMap: Record< + string, + { deptName: string; semPrefix: string } +> = { + CSE: { + deptName: "Computer Science and Engineering", + semPrefix: "B.Tech-CSE", + }, + CS: { deptName: "Computer Science and Engineering", semPrefix: "B.Tech-CSE" }, + IT: { deptName: "Information Technology", semPrefix: "B.Tech-IT" }, + ECE: { + deptName: "Electronics and Communication Engineering", + semPrefix: "B.Tech-ECE", + }, + EC: { + deptName: "Electronics and Communication Engineering", + semPrefix: "B.Tech-ECE", + }, + EE: { deptName: "Electrical Engineering", semPrefix: "B.Tech-EE" }, + ME: { deptName: "Mechanical Engineering", semPrefix: "B.Tech-ME" }, + CE: { deptName: "Civil Engineering", semPrefix: "B.Tech-CE" }, + BT: { deptName: "Biotechnology", semPrefix: "B.Tech-BT" }, + CH: { deptName: "Chemical Engineering", semPrefix: "B.Tech-CH" }, + CHE: { deptName: "Chemical Engineering", semPrefix: "B.Tech-CH" }, + ICE: { + deptName: "Instrumentation and Control Engineering", + semPrefix: "B.Tech-ICE", + }, + IPE: { + deptName: "Industrial and Production Engineering", + semPrefix: "B.Tech-IPE", + }, + TT: { deptName: "Textile Technology", semPrefix: "B.Tech-TT" }, + MNC: { deptName: "Mathematics & Computing", semPrefix: "B.Tech-MCE" }, + MC: { deptName: "Mathematics & Computing", semPrefix: "B.Tech-MCE" }, +}; + +export async function getHigherYearGroup( + year: number, + branch: string, + group: string, + subSection?: string, +): Promise { + const branchUpper = branch.trim().toUpperCase(); + const branchInfo = branchToDeptMap[branchUpper]; + if (!branchInfo) { + throw new Error(`Invalid branch abbreviation: ${branch}`); + } + + const sessions = await fetch(config.url.sessionAndDept); + const sessionDeptData = (await sessions.json()) as sessionDeptDataType; + + const currentSessionObj = sessionDeptData.uniqueSessions.find( + (sess) => sess.currentSession, + ); + if (!currentSessionObj || !currentSessionObj.session) { + throw new Error( + "Current session not found in sessionDeptData.uniqueSessions", + ); + } + const currentSession = currentSessionObj.session; + const isEven = currentSession.toLowerCase().includes("even"); + + // Calculate semester number based on college year and odd/even session + const semNumber = (year - 1) * 2 + (isEven ? 2 : 1); + + // Get the department code + const codeFetch = await fetch( + `${config.url.deptCode}/${encodeURIComponent(`${currentSession}`)}/${encodeURIComponent(`${branchInfo.deptName}`)}`, + ); + if (!codeFetch.ok) { + throw new Error("Failed to fetch department code from Xceed"); + } + const code = JSON.parse(await codeFetch.text()); + if (typeof code !== "string" || code.trim() === "") { + throw new Error("Unexpected code format from Xceed API"); + } + + // Fetch the semesters list for this department code + const semFetch = await fetch(`${config.url.sem}${encodeURIComponent(code)}`); + if (!semFetch.ok) { + throw new Error("Failed to fetch semester data from Xceed"); + } + const semData = (await semFetch.json()) as semArrDataType; + + const grpUpper = group.trim().toUpperCase(); + const subUpper = subSection ? subSection.trim().toUpperCase() : ""; + + // Attempt pattern match with subsection first: e.g. B.Tech-CSE-4A1 or B.Tech-CSE-4A-1 + const targetPatternWithSub1 = `${branchInfo.semPrefix}-${semNumber}${grpUpper}${subUpper}`; + const targetPatternWithSub2 = `${branchInfo.semPrefix}-${semNumber}${grpUpper}-${subUpper}`; + + const targetPattern = `${branchInfo.semPrefix}-${semNumber}${grpUpper}`; // e.g. B.Tech-CSE-4A + const fallbackPattern2 = `${branchInfo.semPrefix}-${semNumber}-${grpUpper}`; + const fallbackPattern3 = `${branchInfo.semPrefix}-${semNumber}`; // e.g. if single section like B.Tech-CSE-8 + + let semEntry: semDataType | undefined; + + if (subUpper) { + semEntry = semData.find( + (entry) => + entry.code === code && + (entry.sem.toUpperCase() === targetPatternWithSub1.toUpperCase() || + entry.sem.toUpperCase().includes(targetPatternWithSub1.toUpperCase()) || + entry.sem.toUpperCase() === targetPatternWithSub2.toUpperCase() || + entry.sem.toUpperCase().includes(targetPatternWithSub2.toUpperCase())), + ); + } + + if (!semEntry) { + semEntry = semData.find( + (entry) => + entry.code === code && + (entry.sem.toUpperCase() === targetPattern.toUpperCase() || + entry.sem.toUpperCase().includes(targetPattern.toUpperCase()) || + entry.sem.toUpperCase().includes(fallbackPattern2.toUpperCase())), + ); + } + + if (!semEntry && grpUpper === "") { + semEntry = semData.find( + (entry) => + entry.code === code && + entry.sem.toUpperCase() === fallbackPattern3.toUpperCase(), + ); + } + + if (!semEntry) { + semEntry = semData.find( + (entry) => + entry.code === code && + entry.sem.toUpperCase().includes(branchInfo.semPrefix.toUpperCase()) && + entry.sem.includes(`${semNumber}`) && + entry.sem.toUpperCase().includes(grpUpper) && + (!subUpper || entry.sem.toUpperCase().includes(subUpper)), + ); + } + + return semEntry || null; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0885b07 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1800 @@ +{ + "name": "api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api", + "version": "0.1.0", + "dependencies": { + "@hono/zod-openapi": "^1.2.0", + "api": ".", + "drizzle-orm": "^0.45.1", + "hono": "^4.11.4", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/bun": "latest", + "drizzle-kit": "^0.31.8" + }, + "peerDependencies": { + "typescript": "^5" + } + }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz", + "integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==", + "license": "MIT", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/zod-openapi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@hono/zod-openapi/-/zod-openapi-1.5.1.tgz", + "integrity": "sha512-ZaDdEIkn6PEGjIYHXJeyByg6yhvLzI+UXn968pWtahpwcQ6HMjQYXI3zNffTl0Wl9QZ79nUnGqiN1erEIu23fA==", + "license": "MIT", + "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.5.0", + "@hono/zod-validator": "^0.9.0", + "openapi3-ts": "^4.5.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "hono": ">=4.10.0", + "zod": "^4.0.0" + } + }, + "node_modules/@hono/zod-validator": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.9.0.tgz", + "integrity": "sha512-n0ZSXmCiHVIp4Y5wlOOyZCeTd/rsawA/qW1cipB8QOYKZ9N8Tk0nZUZCXho9cu374AN4JpDNKioNKBJ/W+LBug==", + "license": "MIT", + "peerDependencies": { + "hono": ">=4.11.2", + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@types/bun": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz", + "integrity": "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.14" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/api": { + "resolved": "", + "link": true + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bun-types": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", + "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/hono": { + "version": "4.12.31", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", + "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/openapi3-ts": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.6.0.tgz", + "integrity": "sha512-a4sfn6L2sIShhtzJqmjGrARvxAW/3F2BJDdyRVvNF9VhAsZSh5hSyI3a9TNvmzBxXmq66nY5LNT5bQcBxYAZZg==", + "license": "MIT", + "dependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/routes/students/getGroup.ts b/routes/students/getGroup.ts index 1517cdb..25e6c2d 100644 --- a/routes/students/getGroup.ts +++ b/routes/students/getGroup.ts @@ -8,8 +8,8 @@ const querySchema = z.object({ }); const responseSchema = z.object({ - group: z.enum(["A1", "A2", "A3", "A4", "A5", "A6", "B1", "B2", "B3", "B4", "B5", "B6"]), - subGroup: z.enum(["a", "b", "c"]), + group: z.string().openapi({ example: "B6" }), + subGroup: z.string().nullable().openapi({ example: "a" }), }); getGroupRoute.openapi( @@ -17,8 +17,8 @@ getGroupRoute.openapi( method: "get", path: "/", tags: ["Students"], - summary: "Get Student Group [ FIRST YEAR STUDENTS ONLY ]", - description: "Returns the group (e.g., B6) of a student based on their roll number.", + summary: "Get Student Group", + description: "Returns the group (e.g., B6) or section of a student based on their roll number.", request: { query: querySchema, }, @@ -41,20 +41,8 @@ getGroupRoute.openapi( const studentGroup = await getGroup(rollNumber); if (studentGroup) { return c.json({ - group: studentGroup.group as - | "A1" - | "A2" - | "A3" - | "A4" - | "A5" - | "A6" - | "B1" - | "B2" - | "B3" - | "B4" - | "B5" - | "B6", - subGroup: studentGroup.subGroup as "a" | "b" | "c", + group: studentGroup.group, + subGroup: studentGroup.subGroup, }); } return c.json({ message: "Group not found for the provided roll number." }, 404); diff --git a/routes/timetable/index.ts b/routes/timetable/index.ts index 2aac84f..8412f21 100644 --- a/routes/timetable/index.ts +++ b/routes/timetable/index.ts @@ -1,6 +1,8 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import { year1 } from "./year/1"; +import { higherYears } from "./year/higherYears"; export const timeTableRoute = new OpenAPIHono(); timeTableRoute.route("/year/1", year1); +timeTableRoute.route("/year", higherYears); diff --git a/routes/timetable/year/1.ts b/routes/timetable/year/1.ts index 87daa72..a04ea6c 100644 --- a/routes/timetable/year/1.ts +++ b/routes/timetable/year/1.ts @@ -1,5 +1,4 @@ import { OpenAPIHono, z } from "@hono/zod-openapi"; -import { fetch } from "bun"; import config from "../../../config.json" assert { type: "json" }; import { getFirstYearGroups } from "../../../lib/firstYear"; import type { semDataType, timetableDataType } from "../../../types/xceed"; @@ -60,6 +59,22 @@ year1.openapi( }, }, }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: errorSchema, + }, + }, + }, + 502: { + description: "Failed to fetch timetable data", + content: { + "application/json": { + schema: errorSchema, + }, + }, + }, }, }, async c => { @@ -73,11 +88,19 @@ year1.openapi( } try { - const fetchedData = await fetch( - `${config.url.timeTable}/${encodeURIComponent(groupData.code.toString())}/${encodeURIComponent( - groupData.sem.toString() - )}` - ); + const controller = new AbortController(); + const fetchTimeout = setTimeout(() => controller.abort(), 8000); + let fetchedData: Response; + try { + fetchedData = await fetch( + `${config.url.timeTable}/${encodeURIComponent(groupData.code.toString())}/${encodeURIComponent( + groupData.sem.toString() + )}`, + { signal: controller.signal } + ); + } finally { + clearTimeout(fetchTimeout); + } if (!fetchedData.ok) { return c.json({ error: "Failed to fetch timetable data" }, 502); @@ -86,8 +109,11 @@ year1.openapi( const data = (await fetchedData.json()) as timetableDataType; return c.json(data, 200); - } catch (error) { - return c.json({ error: "An error occurred while fetching timetable data" }, 500); + } catch (error: unknown) { + const message = error instanceof Error && error.name === "AbortError" + ? "Timetable fetch timed out" + : "An error occurred while fetching timetable data"; + return c.json({ error: message }, 500); } } ); diff --git a/routes/timetable/year/higherYears.ts b/routes/timetable/year/higherYears.ts new file mode 100644 index 0000000..35d0316 --- /dev/null +++ b/routes/timetable/year/higherYears.ts @@ -0,0 +1,234 @@ +import { OpenAPIHono, z } from "@hono/zod-openapi"; +import config from "../../../config.json" assert { type: "json" }; +import { getHigherYearGroup } from "../../../lib/higherYears"; +import type { timetableDataType } from "../../../types/xceed"; + +export const higherYears = new OpenAPIHono(); + +const pathSchema = z.object({ + year: z.enum(["2", "3", "4"]), +}); + +const querySchema = z.object({ + branch: z + .string() + .min(2) + .max(5) + .regex(/^[A-Za-z]+$/, "Branch must contain only letters") + .openapi({ example: "CSE" }), + group: z + .string() + .max(3) + .regex(/^[A-Za-z0-9]*$/, "Group must be alphanumeric") + .default("A") + .openapi({ example: "A" }), + subSection: z + .string() + .max(3) + .regex(/^[A-Za-z0-9]*$/, "SubSection must be alphanumeric") + .optional() + .openapi({ example: "1" }), +}); + +const responseSchema = z.object({ + timetableData: z.record( + z.string(), + z.record( + z.string(), + z.array( + z.array( + z.object({ + subject: z.string(), + subjectCode: z.string().nullable(), + faculty: z.string(), + room: z.string(), + day: z.string(), + startTime: z.string().nullable(), + endTime: z.string().nullable(), + duration: z.string().nullable(), + branch: z.string(), + year: z.string(), + section: z.string(), + subSection: z.string().nullable(), + courseType: z.enum(["Lecture", "Tutorial", "Practical", "Unknown"]), + }), + ), + ), + ), + ), + notes: z.array(z.string()), +}); + +const errorSchema = z.object({ + error: z.string(), +}); + +higherYears.openapi( + { + method: "get", + path: "/:year", + tags: ["Timetable"], + summary: "Get Higher Year Timetable", + description: + "Returns the timetable for second, third, and fourth year students.", + request: { + params: pathSchema, + query: querySchema, + }, + responses: { + 200: { + description: "Timetable retrieved successfully", + content: { + "application/json": { + schema: responseSchema, + }, + }, + }, + 400: { + description: "Invalid parameters", + content: { + "application/json": { + schema: errorSchema, + }, + }, + }, + 404: { + description: "Timetable not found for the specified parameters", + content: { + "application/json": { + schema: errorSchema, + }, + }, + }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: errorSchema, + }, + }, + }, + 502: { + description: "Failed to fetch timetable data from Xceed", + content: { + "application/json": { + schema: errorSchema, + }, + }, + }, + }, + }, + async (c) => { + const { year } = c.req.valid("param"); + const { branch, group, subSection } = c.req.valid("query"); + + try { + const yearNum = parseInt(year, 10); + const groupData = await getHigherYearGroup(yearNum, branch, group, subSection); + + if (!groupData) { + return c.json( + { error: "Timetable section not found for the specified parameters" }, + 404, + ); + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 8000); + let fetchedData: Response; + try { + fetchedData = await fetch( + `${config.url.timeTable}/${encodeURIComponent(groupData.code.toString())}/${encodeURIComponent( + groupData.sem.toString(), + )}`, + { signal: controller.signal }, + ); + } finally { + clearTimeout(timeout); + } + + if (!fetchedData.ok) { + return c.json( + { error: "Failed to fetch timetable data from Xceed" }, + 502, + ); + } + + const rawData = (await fetchedData.json()) as timetableDataType; + + // Define approximate NITJ slot times for standard periods + const timeSlots: Record = { + period1: { start: "08:30 AM", end: "09:30 AM", duration: "1 hour" }, + period2: { start: "09:30 AM", end: "10:30 AM", duration: "1 hour" }, + period3: { start: "10:30 AM", end: "11:30 AM", duration: "1 hour" }, + period4: { start: "11:30 AM", end: "12:30 PM", duration: "1 hour" }, + period5: { start: "01:30 PM", end: "02:30 PM", duration: "1 hour" }, + period6: { start: "02:30 PM", end: "03:30 PM", duration: "1 hour" }, + period7: { start: "03:30 PM", end: "04:30 PM", duration: "1 hour" }, + period8: { start: "04:30 PM", end: "05:30 PM", duration: "1 hour" }, + }; + + const structuredTimetable: Record>>> = {}; + + for (const [day, periods] of Object.entries(rawData.timetableData)) { + structuredTimetable[day] = {}; + for (const [periodName, slots] of Object.entries(periods)) { + structuredTimetable[day][periodName] = slots.map((concurrentSlotList) => { + return concurrentSlotList.map((slot) => { + const subjectText = slot.subject.trim(); + + // Attempt to extract subject code if present in subjectText (e.g. "CSX-201 Data Structures" or "CSX 201") + const codeMatch = subjectText.match(/^([A-Z]{2,4}[- ]?\d{3})/i); + const subjectCode = codeMatch ? codeMatch[1] : null; + + // Determine Course Type: Lab/Practical, Tutorial, or Lecture + let courseType: "Lecture" | "Tutorial" | "Practical" | "Unknown" = "Lecture"; + const lowerSubj = subjectText.toLowerCase(); + if (lowerSubj.includes("lab") || lowerSubj.includes("practical") || lowerSubj.includes("project")) { + courseType = "Practical"; + } else if (lowerSubj.includes("tutorial") || lowerSubj.includes("tut")) { + courseType = "Tutorial"; + } + + // Retrieve period timings + const timings = timeSlots[periodName.toLowerCase()] || { start: null, end: null, duration: null }; + + return { + subject: subjectText, + subjectCode, + faculty: slot.faculty.trim(), + room: slot.room.trim(), + day, + startTime: timings.start, + endTime: timings.end, + duration: timings.duration, + branch, + year, + section: group, + subSection: subSection || null, + courseType, + }; + }); + }); + } + } + + return c.json({ + timetableData: structuredTimetable, + notes: rawData.notes, + }, 200); + } catch (error: unknown) { + if ( + error instanceof Error && + error.message.startsWith("Invalid branch abbreviation") + ) { + return c.json({ error: "Invalid branch parameter" }, 400); + } + const message = + error instanceof Error && error.name === "AbortError" + ? "Timetable fetch timed out" + : "An error occurred while fetching timetable data"; + return c.json({ error: message }, 500); + } + }, +); diff --git a/seed.ts b/seed.ts new file mode 100644 index 0000000..a72c648 --- /dev/null +++ b/seed.ts @@ -0,0 +1,64 @@ +import { Database } from "bun:sqlite"; +import { db } from "./lib/db"; +import * as schema from "./lib/db/schema"; + +async function main() { + console.log("Initializing database.sqlite..."); + const sqlite = new Database("database.sqlite"); + + const ALLOWED_TABLES = ["batch2022", "batch2023", "batch2024", "batch2025", "batch2026"]; + const createTableQuery = (tableName: string) => { + if (!ALLOWED_TABLES.includes(tableName)) throw new Error(`Invalid table name: ${tableName}`); + return ` + CREATE TABLE IF NOT EXISTS ${tableName} ( + rollnumber INTEGER PRIMARY KEY, + name TEXT, + branchabbr TEXT, + branch TEXT, + "group" TEXT, + subgroup TEXT + ); + `; + }; + + sqlite.run(createTableQuery("batch2022")); + sqlite.run(createTableQuery("batch2023")); + sqlite.run(createTableQuery("batch2024")); + sqlite.run(createTableQuery("batch2025")); + sqlite.run(createTableQuery("batch2026")); + sqlite.close(); + + console.log("Seeding test data..."); + + // Seed dummy students across different years + await db.insert(schema.batch2025Table).values({ + rollnumber: 25103001, + name: "First Year CSE Student", + branchabbr: "CS", + branch: "Computer Science & Engineering", + group: "B6", + subgroup: "a", + }).onConflictDoNothing(); + + await db.insert(schema.batch2024Table).values({ + rollnumber: 24103002, + name: "Second Year CSE Student", + branchabbr: "CS", + branch: "Computer Science & Engineering", + group: "CSE-A", + subgroup: "C1", + }).onConflictDoNothing(); + + await db.insert(schema.batch2023Table).values({ + rollnumber: 23104003, + name: "Third Year ECE Student", + branchabbr: "EC", + branch: "Electronics & Communication Engineering", + group: "ECE-B", + subgroup: "D2", + }).onConflictDoNothing(); + + console.log("Database seeded successfully!"); +} + +main().catch(console.error); diff --git a/test.spec.ts b/test.spec.ts new file mode 100644 index 0000000..de6ebb2 --- /dev/null +++ b/test.spec.ts @@ -0,0 +1,216 @@ +import { beforeAll, afterAll, describe, expect, it } from "bun:test"; +import { Database } from "bun:sqlite"; +import { app } from "./app"; +import { db } from "./lib/db"; +import * as schema from "./lib/db/schema"; + +const originalFetch = globalThis.fetch; + +beforeAll(async () => { + // Ensure all tables are created in the SQLite database + const sqlite = new Database("database.sqlite"); + + const createTableQuery = (tableName: string) => ` + CREATE TABLE IF NOT EXISTS ${tableName} ( + rollnumber INTEGER PRIMARY KEY, + name TEXT, + branchabbr TEXT, + branch TEXT, + "group" TEXT, + subgroup TEXT + ); + `; + + sqlite.run(createTableQuery("batch2022")); + sqlite.run(createTableQuery("batch2023")); + sqlite.run(createTableQuery("batch2024")); + sqlite.run(createTableQuery("batch2025")); + sqlite.run(createTableQuery("batch2026")); + sqlite.close(); + + // Seed dummy students across different years + await db.insert(schema.batch2025Table).values({ + rollnumber: 25103001, + name: "First Year CSE Student", + branchabbr: "CS", + branch: "Computer Science & Engineering", + group: "B6", + subgroup: "a", + }).onConflictDoNothing(); + + await db.insert(schema.batch2024Table).values({ + rollnumber: 24103002, + name: "Second Year CSE Student", + branchabbr: "CS", + branch: "Computer Science & Engineering", + group: "CSE-A", + subgroup: "C1", + }).onConflictDoNothing(); + + await db.insert(schema.batch2023Table).values({ + rollnumber: 23104003, + name: "Third Year ECE Student", + branchabbr: "EC", + branch: "Electronics & Communication Engineering", + group: "ECE-B", + subgroup: "D2", + }).onConflictDoNothing(); + + // Mock global fetch to isolate tests from network dependency + globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { + const urlStr = url.toString(); + + if (urlStr.includes("allsessanddept")) { + return new Response(JSON.stringify({ + uniqueSessions: [{ session: "2026-2027 (Odd)", currentSession: true }], + uniqueDept: [ + "Basic Sciences", + "Computer Science and Engineering", + "Electronics and Communication Engineering" + ] + })); + } + + if (urlStr.includes("getcode")) { + if (urlStr.includes("Basic%20Sciences")) { + return new Response(JSON.stringify("basic-sciences-code")); + } + if (urlStr.includes("Computer%20Science%20and%20Engineering")) { + return new Response(JSON.stringify("cse-code")); + } + if (urlStr.includes("Electronics%20and%20Communication%20Engineering")) { + return new Response(JSON.stringify("ece-code")); + } + } + + if (urlStr.includes("addsem")) { + if (urlStr.includes("code=basic-sciences-code")) { + return new Response(JSON.stringify([ + { _id: "1", sem: "B.Tech-CSE-SectionB6", code: "basic-sciences-code" } + ])); + } + if (urlStr.includes("code=cse-code")) { + return new Response(JSON.stringify([ + { _id: "2", sem: "B.Tech-CSE-3A", code: "cse-code" } + ])); + } + if (urlStr.includes("code=ece-code")) { + return new Response(JSON.stringify([ + { _id: "3", sem: "B.Tech-ECE-5B", code: "ece-code" } + ])); + } + } + + if (urlStr.includes("lockclasstt")) { + return new Response(JSON.stringify({ + timetableData: { + Monday: { + period1: [[{ subject: "Mock Subject", faculty: "Mock Faculty", room: "Mock Room" }]] + } + }, + notes: ["Mock notes"] + })); + } + + throw new Error(`Unexpected network fetch in tests: ${urlStr}`); + }) as any; +}); + +afterAll(() => { + globalThis.fetch = originalFetch; +}); + +describe("OpensourceNITJ API Integration Tests", () => { + describe("Health Endpoint", () => { + it("should return status ok", async () => { + const res = await app.request("/health"); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body.status).toBe("ok"); + expect(body.uptime).toBeTypeOf("number"); + }); + }); + + describe("Student Group Lookup", () => { + it("should return correct group for 1st Year (Batch 2025)", async () => { + const res = await app.request("/students/getGroup?rollNumber=25103001"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual({ + group: "B6", + subGroup: "a", + }); + }); + + it("should return correct group for 2nd Year (Batch 2024)", async () => { + const res = await app.request("/students/getGroup?rollNumber=24103002"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual({ + group: "CSE-A", + subGroup: "1", + }); + }); + + it("should return correct group for 3rd Year (Batch 2023)", async () => { + const res = await app.request("/students/getGroup?rollNumber=23104003"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual({ + group: "ECE-B", + subGroup: "2", + }); + }); + + it("should return 404 for unknown roll number", async () => { + const res = await app.request("/students/getGroup?rollNumber=99999999"); + expect(res.status).toBe(404); + }); + }); + + describe("Timetable Endpoints", () => { + it("should fetch 1st Year Timetable", async () => { + const res = await app.request("/timetable/year/1?group=B6"); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body).toHaveProperty("timetableData"); + expect(body.timetableData.Monday.period1[0][0].subject).toBe("Mock Subject"); + expect(body).toHaveProperty("notes"); + }); + + it("should fetch 2nd Year (CSE) Timetable with structured data and subsection", async () => { + const res = await app.request("/timetable/year/2?branch=CSE&group=A&subSection=1"); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body).toHaveProperty("timetableData"); + + const firstSlot = body.timetableData.Monday.period1[0][0]; + expect(firstSlot.subject).toBe("Mock Subject"); + expect(firstSlot).toHaveProperty("subjectCode"); + expect(firstSlot.startTime).toBe("08:30 AM"); + expect(firstSlot.endTime).toBe("09:30 AM"); + expect(firstSlot.duration).toBe("1 hour"); + expect(firstSlot.branch).toBe("CSE"); + expect(firstSlot.year).toBe("2"); + expect(firstSlot.section).toBe("A"); + expect(firstSlot.subSection).toBe("1"); + expect(firstSlot.courseType).toBe("Lecture"); + + expect(body).toHaveProperty("notes"); + }); + + it("should fetch 3rd Year (ECE) Timetable", async () => { + const res = await app.request("/timetable/year/3?branch=ECE&group=B"); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body).toHaveProperty("timetableData"); + expect(body.timetableData.Monday.period1[0][0].subject).toBe("Mock Subject"); + expect(body).toHaveProperty("notes"); + }); + + it("should return 400 or 404 for non-existent branch or section", async () => { + const res = await app.request("/timetable/year/2?branch=XYZ&group=Z"); + expect([400, 404]).toContain(res.status); + }); + }); +}); From d1645eb3ff8b3043c04271f9e52d7cd65367d6d9 Mon Sep 17 00:00:00 2001 From: Karthik Kanithi Date: Sat, 25 Jul 2026 08:20:06 +0530 Subject: [PATCH 2/2] fix: resolve higher-year timetable issues reported in #5 --- lib/higherYears.ts | 23 ++++++++++++++-- routes/timetable/year/higherYears.ts | 41 +++++++++++++++++++++------- test.spec.ts | 40 ++++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/lib/higherYears.ts b/lib/higherYears.ts index 6ed9bdb..bab6178 100644 --- a/lib/higherYears.ts +++ b/lib/higherYears.ts @@ -5,8 +5,27 @@ import type { sessionDeptDataType, } from "../types/xceed"; +export const allowedBranches = [ + "CSE", + "CS", + "IT", + "ECE", + "EC", + "EE", + "ME", + "CE", + "BT", + "CH", + "CHE", + "ICE", + "IPE", + "TT", + "MNC", + "MC", +] as const; + export const branchToDeptMap: Record< - string, + (typeof allowedBranches)[number], { deptName: string; semPrefix: string } > = { CSE: { @@ -124,7 +143,7 @@ export async function getHigherYearGroup( ); } - if (!semEntry && grpUpper === "") { + if (!semEntry) { semEntry = semData.find( (entry) => entry.code === code && diff --git a/routes/timetable/year/higherYears.ts b/routes/timetable/year/higherYears.ts index 35d0316..cf5a49e 100644 --- a/routes/timetable/year/higherYears.ts +++ b/routes/timetable/year/higherYears.ts @@ -1,6 +1,6 @@ import { OpenAPIHono, z } from "@hono/zod-openapi"; import config from "../../../config.json" assert { type: "json" }; -import { getHigherYearGroup } from "../../../lib/higherYears"; +import { getHigherYearGroup, allowedBranches } from "../../../lib/higherYears"; import type { timetableDataType } from "../../../types/xceed"; export const higherYears = new OpenAPIHono(); @@ -11,10 +11,7 @@ const pathSchema = z.object({ const querySchema = z.object({ branch: z - .string() - .min(2) - .max(5) - .regex(/^[A-Za-z]+$/, "Branch must contain only letters") + .enum(allowedBranches) .openapi({ example: "CSE" }), group: z .string() @@ -168,14 +165,37 @@ higherYears.openapi( period8: { start: "04:30 PM", end: "05:30 PM", duration: "1 hour" }, }; + const DAYS_OF_WEEK = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; const structuredTimetable: Record>>> = {}; for (const [day, periods] of Object.entries(rawData.timetableData)) { + if (!DAYS_OF_WEEK.includes(day)) { + continue; + } structuredTimetable[day] = {}; - for (const [periodName, slots] of Object.entries(periods)) { + for (const [periodName, rawSlots] of Object.entries(periods)) { + // Normalize slots to a 2D array defensively + let slots: Array> = []; + if (Array.isArray(rawSlots)) { + slots = rawSlots.map((item) => { + if (Array.isArray(item)) { + return item; + } else if (item && typeof item === "object") { + return [item]; + } + return []; + }).filter(list => list.length > 0); + } else if (rawSlots && typeof rawSlots === "object") { + const slotObj = rawSlots as any; + if (slotObj.subject || slotObj.faculty || slotObj.room) { + slots = [[slotObj]]; + } + } + structuredTimetable[day][periodName] = slots.map((concurrentSlotList) => { return concurrentSlotList.map((slot) => { - const subjectText = slot.subject.trim(); + if (!slot) return null; + const subjectText = (slot.subject || "").trim(); // Attempt to extract subject code if present in subjectText (e.g. "CSX-201 Data Structures" or "CSX 201") const codeMatch = subjectText.match(/^([A-Z]{2,4}[- ]?\d{3})/i); @@ -196,8 +216,8 @@ higherYears.openapi( return { subject: subjectText, subjectCode, - faculty: slot.faculty.trim(), - room: slot.room.trim(), + faculty: (slot.faculty || "").trim(), + room: (slot.room || "").trim(), day, startTime: timings.start, endTime: timings.end, @@ -208,7 +228,7 @@ higherYears.openapi( subSection: subSection || null, courseType, }; - }); + }).filter(Boolean); }); } } @@ -218,6 +238,7 @@ higherYears.openapi( notes: rawData.notes, }, 200); } catch (error: unknown) { + console.error("Error in higherYears timetable route handler:", error); if ( error instanceof Error && error.message.startsWith("Invalid branch abbreviation") diff --git a/test.spec.ts b/test.spec.ts index de6ebb2..9fba53a 100644 --- a/test.spec.ts +++ b/test.spec.ts @@ -66,7 +66,8 @@ beforeAll(async () => { uniqueDept: [ "Basic Sciences", "Computer Science and Engineering", - "Electronics and Communication Engineering" + "Electronics and Communication Engineering", + "Chemical Engineering" ] })); } @@ -81,6 +82,9 @@ beforeAll(async () => { if (urlStr.includes("Electronics%20and%20Communication%20Engineering")) { return new Response(JSON.stringify("ece-code")); } + if (urlStr.includes("Chemical%20Engineering")) { + return new Response(JSON.stringify("che-code")); + } } if (urlStr.includes("addsem")) { @@ -99,6 +103,11 @@ beforeAll(async () => { { _id: "3", sem: "B.Tech-ECE-5B", code: "ece-code" } ])); } + if (urlStr.includes("code=che-code")) { + return new Response(JSON.stringify([ + { _id: "4", sem: "B.Tech-CH-3", code: "che-code" } + ])); + } } if (urlStr.includes("lockclasstt")) { @@ -208,9 +217,38 @@ describe("OpensourceNITJ API Integration Tests", () => { expect(body).toHaveProperty("notes"); }); + it("should fetch 2nd Year (CHE) Timetable (single-section branch fallback)", async () => { + const res = await app.request("/timetable/year/2?branch=CHE"); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body).toHaveProperty("timetableData"); + expect(body.timetableData.Monday.period1[0][0].subject).toBe("Mock Subject"); + expect(body).toHaveProperty("notes"); + }); + it("should return 400 or 404 for non-existent branch or section", async () => { const res = await app.request("/timetable/year/2?branch=XYZ&group=Z"); expect([400, 404]).toContain(res.status); }); }); + + describe("OpenAPI Documentation", () => { + it("should expose branch enums correctly", async () => { + const res = await app.request("/openapi"); + expect(res.status).toBe(200); + const openapi = (await res.json()) as any; + + // Check that the query parameter schema for branch has an enum listing all branches + const branchQueryParam = openapi.paths["/timetable/year/:year"].get.parameters.find( + (p: any) => p.name === "branch" && p.in === "query" + ); + + expect(branchQueryParam).toBeDefined(); + expect(branchQueryParam.schema.enum).toEqual([ + "CSE", "CS", "IT", "ECE", "EC", "EE", "ME", "CE", + "BT", "CH", "CHE", "ICE", "IPE", "TT", "MNC", "MC" + ]); + }); + }); }); +