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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ COPY prisma.config.ts ./prisma.config.ts
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts --store-dir=/pnpm/store

# The install above intentionally skips lifecycle scripts, so generate the
# typed client explicitly before compiling the application.
RUN pnpm prisma generate

# Copy source code
COPY . .

Expand Down
10 changes: 10 additions & 0 deletions __tests__/prisma-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { prisma } from "@/lib/prisma";

describe("shared Prisma client", () => {
it("exposes generated model delegates through the shared client", () => {
expect(prisma).toBeDefined();
expect(prisma.vausers).toBeDefined();
expect(prisma.vastudents).toBeDefined();
expect(prisma.vabatches).toBeDefined();
});
});
40 changes: 11 additions & 29 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
import { passkey } from "@better-auth/passkey";
import { prismaAdapter } from "@better-auth/prisma-adapter";
import type { Prisma } from "@prisma/client";
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
import { customSession } from "better-auth/plugins";
import { createPool } from "mysql2/promise";
import { prisma } from "./prisma";
import { sendAuthEmail } from "./auth-email";

const database = createPool({
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT ?? 3306),
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
timezone: "Z",
connectionLimit: 10,
});

type VaUser = {
id: number;
name: string | null;
role: string | null;
};
type VaUser = Prisma.vausersGetPayload<{
select: { id: true; name: true; role: true };
}>;

async function findVaUser(email: string): Promise<VaUser | null> {
const [rows] = await database.execute("SELECT id, name, role FROM vausers WHERE LOWER(email) = LOWER(?) LIMIT 1", [
email,
]);

if (!Array.isArray(rows) || rows.length === 0) return null;
const row = rows[0];
if (!row || typeof row !== "object" || !("id" in row)) return null;
return {
id: Number(row.id),
name: typeof row.name === "string" ? row.name : null,
role: typeof row.role === "string" ? row.role : null,
};
return prisma.vausers.findFirst({
where: { email },
select: { id: true, name: true, role: true },
});
}

const baseURL = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_BASE_URL;
Expand All @@ -43,7 +25,7 @@ export const auth = betterAuth({
appName: "Vision-Aid STATS",
baseURL,
secret: process.env.BETTER_AUTH_SECRET,
database,
database: prismaAdapter(prisma, { provider: "mysql", transaction: true }),
session: {
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60,
Expand Down
40 changes: 0 additions & 40 deletions lib/db.ts

This file was deleted.

27 changes: 27 additions & 0 deletions lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { PrismaClient } from "@prisma/client";

declare global {
// eslint-disable-next-line no-unused-vars
var prisma: PrismaClient | undefined;
}

const adapter = new PrismaMariaDb({
host: process.env.MYSQL_HOST ?? "localhost",
port: Number(process.env.MYSQL_PORT ?? 3306),
user: process.env.MYSQL_USER ?? "",
password: process.env.MYSQL_PASSWORD ?? "",
database: process.env.MYSQL_DATABASE ?? "",
connectionLimit: 10,
});

export const prisma =
globalThis.prisma ??
new PrismaClient({
adapter,
log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["error"],
});

if (process.env.NODE_ENV !== "production") {
globalThis.prisma = prisma;
}
11 changes: 0 additions & 11 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,6 @@ module.exports = withPWA({
output: "standalone",
pageExtensions: ["mdx", "md", "tsx", "ts", "svg"],

/**
* mysql2 uses dynamic requires; bundling it for API routes can fail with
* `Cannot find module './chunks/undefined'`. Load it from node_modules at runtime.
*/
webpack: (config, { isServer }) => {
if (isServer) {
config.externals.push("mysql2", "mysql2/promise");
}
return config;
},

async headers() {
return [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schema: spec-driven
created: 2026-09-16
skip_specs: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
## Context

The Prisma schema is split across `prisma/models` and `prisma/enums` and already models the primary application tables and relations. The application has no shared Prisma client yet. Instead, 57 TypeScript API/utilities import `lib/db.ts`, which opens a MySQL connection per call and exposes rows as `Record<string, unknown>`; `lib/auth.ts` also owns a separate MySQL pool for a custom `vausers` lookup and passes that pool to Better Auth.

The migration must preserve Pages Router endpoints, existing JSON shapes, legacy column names and enum values, MySQL behavior, and the Better Auth integration. Some current SQL contains joins, aggregates, dynamic filters, upserts, or vendor-specific expressions that may require a deliberately typed raw-query exception rather than a semantic rewrite.

## Goals / Non-Goals

**Goals:**

- Establish one shared server-side Prisma Client entry point for application-owned database access.
- Replace generic query/mutation result handling with generated model, enum, relation, and input types.
- Preserve existing API behavior, response keys, error behavior, transaction boundaries, and database semantics.
- Make raw SQL exceptional, parameterized, locally typed, and easy to audit.
- Verify the migration with compiler, test, build, and representative database-backed route checks.

**Non-Goals:**

- Redesigning endpoint payloads, route structure, UI behavior, authentication policy, or database schema.
- Replacing Better Auth's internal database adapter unless the installed Better Auth version provides a compatible Prisma adapter and the migration can preserve its required schema and behavior.
- Converting reporting queries to a new data model or optimizing queries beyond what is required to preserve behavior.
- Adding a broad strict-mode or unrelated type-safety initiative.

## Decisions

### Use a singleton Prisma client module

Create a server-only module such as `lib/prisma.ts` that exports a single `PrismaClient`, with development global reuse to avoid exhausting connections during hot reload and normal production singleton behavior. The module will be the only normal import boundary for `@prisma/client` in application code.

Alternative considered: instantiate `PrismaClient` in every route. This would duplicate lifecycle behavior and can create too many connections during development and serverless execution.

### Migrate by domain and preserve route response mapping

Group routes by domain—configuration, courses/batches, students/enrollment, attendance/grades/fees, users/auth, remarks, audit, and reporting. Replace each SQL statement with a typed Prisma operation, then map the selected records to the existing response shape at the route boundary where SQL aliases or date formatting were previously used. Prefer explicit `select` objects over `include` when the endpoint only needs a subset.

Alternative considered: expose Prisma records directly from every route. That would leak schema naming and Date/BigInt serialization details into the public API and would change response contracts.

### Prefer relations and typed aggregate operations

Use Prisma relations for joins and nested writes, `where`/`orderBy`/`select` for filtering and projection, `count`/`aggregate`/`groupBy` for supported metrics, and `$transaction` for multi-step mutations that currently depend on ordered statements. Use generated enum values rather than string literals for modeled enums.

Alternative considered: retain SQL strings behind a new wrapper. That would preserve the weak result typing and would not deliver the requested Prisma type safety.

### Constrain raw SQL to a typed exception path

When Prisma cannot express a MySQL-specific query without changing behavior, use `$queryRaw`/`$executeRaw` with Prisma template-tag parameterization. Define a local result type for each raw query and keep the query in the owning domain module. Dynamic identifiers must come from an allowlisted mapping, never interpolated request input. Each exception will be documented with why the typed model API is insufficient.

Alternative considered: force every query through Prisma model APIs. This risks changing report totals, date/string semantics, regex filtering, or upsert behavior in legacy MySQL queries.

### Keep data normalization explicit at external boundaries

Use generated Prisma result types internally, then explicitly normalize values needed by JSON responses: dates, BigInt fields, nullable legacy columns, counts, and enum strings. Request bodies and query parameters remain runtime-validated before being used in Prisma `where` or `data` objects. No generic `QueryRow`, `QueryResult`, or unchecked cast-based escape hatch will replace the old helper.

Alternative considered: serialize Prisma results generically. That hides nullable/BigInt/date issues and weakens the type contract at exactly the route boundary being improved.

### Separate application queries from Better Auth adapter ownership

Move `findVaUser` to Prisma because it is an application-owned query. Keep Better Auth's database option on its supported adapter unless a compatible Prisma adapter is available for the installed version; if one is available, evaluate it as a focused subtask and verify the Better Auth tables and lifecycle hooks before changing it. This avoids pretending that an adapter swap is safe while still removing the hand-written custom MySQL lookup.

Alternative considered: replace Better Auth's adapter without compatibility verification. That could break session, account, passkey, verification, or transaction behavior even if application routes compile.

## Risks / Trade-offs

- [Risk] Prisma model field types or relations do not fully match legacy columns and nullability. → Compare every migrated statement with the schema and migration SQL; correct only clearly missing Prisma mappings and require explicit review for schema-affecting changes.
- [Risk] Date, BigInt, enum, alias, or count serialization changes API responses. → Preserve existing response mappers and add fixture-level assertions for representative endpoints.
- [Risk] Converting multi-statement mutations changes atomicity or ordering. → Use `$transaction` only where the existing operation is logically a unit, and test partial-failure behavior before and after migration.
- [Risk] Raw SQL exceptions preserve behavior but reduce type safety. → Parameterize them, define exact result types, allowlist identifiers, document each exception, and track them for future reduction.
- [Risk] A shared client or Better Auth adapter change causes connection leaks or authentication regressions. → Add lifecycle tests/checks, keep adapter changes isolated, and run sign-in, session, password, and passkey smoke checks.
- [Risk] The large route count makes incomplete migration easy to miss. → Use repository-wide searches for `lib/db`, `mysql2`, `executeQuery`, and unapproved SQL after each domain batch and make type-check/build gates mandatory.

## Migration Plan

1. Inventory every `executeQuery` call, direct MySQL query, response shape, transaction-like sequence, and Prisma schema mismatch; capture current type-check, test, and build baselines.
2. Add the shared Prisma client module and test its initialization/lifecycle behavior without changing route code.
3. Migrate low-risk configuration, course, batch, user, and remark endpoints in domain batches, adding typed selects, mutations, relation queries, and response mappers.
4. Migrate student enrollment, attendance, grades, fees, bulk operations, and multi-statement mutations with explicit transaction decisions.
5. Migrate reports, dashboard counts, audit queries, dropdown utilities, and dynamic-filter paths; document any remaining raw SQL exception.
6. Migrate the custom authentication `vausers` lookup to Prisma and separately verify whether Better Auth's adapter should remain MySQL-backed or can safely use Prisma.
7. Remove `lib/db.ts` and its exported generic query types once repository searches show no callers; remove `mysql2` only if no supported Better Auth or other runtime integration still requires it.
8. Run formatting, lint, type-check, Jest, production build, Prisma validation/generation, and targeted API/auth/database smoke checks. Compare representative response fixtures and mutation results against the baseline.

Rollback is a source/deployment rollback to the last known-good commit. No data migration is planned; if a Prisma mapping requires a database migration, pause that route batch and review it separately rather than applying an implicit schema change.

## Open Questions

- Whether the installed Better Auth release exposes a supported Prisma adapter compatible with the existing `user`, `account`, `session`, `verification`, and `passkey` models. This can be resolved during the auth batch without changing the overall application-query migration approach.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Prisma migration inventory

Captured on the `feature/switch-db-queries-to-prisma` branch.

## Current database access

- 57 TypeScript files import `@/lib/db` and call `executeQuery`.
- `lib/db.ts` creates and closes a `mysql2/promise` connection for each call.
- `lib/auth.ts` owns a separate `mysql2/promise` pool for the custom `vausers` lookup and Better Auth database adapter.
- Prisma 7.10.0 is installed, the schema is valid, and the repository currently has no shared application Prisma client module.

## Domains

- Configuration: `utils/dropdown-config.ts`, `utils/batch-status-rules-config.ts`
- Courses/batches: course and batch CRUD, batch details/reports, enrollment counts/trends
- Users/auth: user CRUD/dropdowns/counts, `lib/auth.ts`
- Students/enrollment: student CRUD, registration, duplicate checks, student/batch membership
- Attendance/grades/fees: attendance, assignments, grades, fee/document updates
- Remarks/audit: VA remarks, telecaller remarks, audit log routes
- Metrics: monthly count routes and dashboard/report data

## Baseline commands

- `pnpm type-check`: passed
- `pnpm prisma validate`: passed
- `pnpm test -- --runInBand`: pre-existing failure; no tests found
- `pnpm build`: compiled successfully and generated all static pages; the command did not return cleanly in the current tool session and needs repeat verification after implementation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Why

The application has a Prisma schema and generated client available, but most API routes and utilities still issue untyped SQL through `lib/db.ts`, while authentication maintains a separate direct MySQL query path. This creates weak `Record<string, unknown>` boundaries, duplicates connection handling, and prevents Prisma model types, relations, enums, and checked input types from protecting database-backed code.

## What Changes

- Replace all application queries currently routed through `lib/db.ts` with Prisma Client model operations and generated Prisma types.
- Migrate the direct `mysql2` query in `lib/auth.ts` to the shared Prisma client so all application-owned reads and writes use one typed database access layer.
- Add a server-only, reusable Prisma client module with the repository’s expected development and production lifecycle behavior.
- Convert SQL joins, filters, ordering, aggregates, mutations, and transactions to typed Prisma queries, preserving existing endpoint response shapes and runtime behavior.
- Use Prisma relations, `select`/`include`, enums, generated input types, and explicit result types at API and utility boundaries; eliminate generic query-row/mutation-result typing from application code.
- Use parameterized Prisma raw SQL only for operations that cannot be represented without changing behavior, with a documented and narrowly typed exception policy.
- Remove `lib/db.ts`, its MySQL connection-per-query implementation, and any now-unused `mysql2` application dependency after all callers are migrated.
- Add or update tests and validation so type-checking fails when query results or mutation inputs bypass Prisma-generated types.

## Capabilities

### New Capabilities

None. This is a behavior-preserving data-access refactor and type-safety improvement.

### Modified Capabilities

None. Existing routes, response payloads, authentication behavior, and database semantics remain unchanged.

## Impact

- Affects every API route and utility importing `lib/db.ts`, plus `lib/auth.ts`.
- Affects the shared server database client, Prisma schema/model mappings where gaps are discovered, and database-related tests.
- Removes the application’s generic SQL query abstraction and likely removes direct `mysql2` usage after confirming Better Auth’s adapter requirements.
- Requires verification across type-checking, unit tests, production build, representative read/write API routes, authentication hooks, transactions, and database error handling.
- No database migration or user-facing API change is intended; any schema correction required to faithfully model existing columns must be separately reviewed before implementation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Typed Prisma raw-query exceptions

The remaining raw SQL uses Prisma's `$queryRaw` or `$queryRawUnsafe` with explicit result types. These queries remain SQL because the legacy MySQL implementation relies on date functions, CTEs, grouped report aggregates, or multi-join projections that would otherwise change response semantics.

- Monthly count/report routes: MySQL `YEAR`, `QUARTER`, `STR_TO_DATE`, and BigInt aggregate normalization.
- `getBatchesWithEnrollCount`, `getCoursesWithEnrollCount`, and `getYearlyEnrollmentTrend`: grouped dashboard aggregates.
- `getbatchdetails`: five legacy multi-join projections used to preserve the existing response contract.
- `getdocumentsfee`: the document-field projection must tolerate legacy empty strings that cannot be decoded as the current `YesNo` Prisma enum.
- `getbatchesdata`, `getbatchesreport`, and `getstudentsdata`: complex CTE/report projections with role filters and grouped financial/attendance metrics.

All request-derived values are passed as bound parameters. The dynamic fragments in `getBatchesWithEnrollCount` are selected from fixed application branches, not request-provided identifiers. No generic database helper remains.
Loading
Loading