Skip to content
Open
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
11 changes: 11 additions & 0 deletions prisma/migrations/20260320011900_add_audit_log/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "audit_log" (
"id" SERIAL NOT NULL,
"action" TEXT NOT NULL,
"model" TEXT NOT NULL,
"recordId" INTEGER,
"userId" INTEGER,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "audit_log_pkey" PRIMARY KEY ("id")
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "item" ADD COLUMN "deleted_at" TIMESTAMP(3);

-- AlterTable
ALTER TABLE "loan_request" ADD COLUMN "deleted_at" TIMESTAMP(3);

-- AlterTable
ALTER TABLE "user" ADD COLUMN "deleted_at" TIMESTAMP(3);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "audit_log" ADD COLUMN "displayName" TEXT;
9 changes: 9 additions & 0 deletions prisma/migrations/20260321132645_fix_record_key/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:

- You are about to drop the column `recordId` on the `audit_log` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "audit_log" DROP COLUMN "recordId",
ADD COLUMN "recordKey" TEXT;
9 changes: 9 additions & 0 deletions prisma/migrations/20260321140242_fix_record_id/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:

- You are about to drop the column `recordKey` on the `audit_log` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "audit_log" DROP COLUMN "recordKey",
ADD COLUMN "recordId" INTEGER;
8 changes: 8 additions & 0 deletions prisma/migrations/20260321140822_temp/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- You are about to drop the column `displayName` on the `audit_log` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "audit_log" DROP COLUMN "displayName";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "ih" ADD COLUMN "deleted_at" TIMESTAMP(3);

-- AlterTable
ALTER TABLE "ih_member" ADD COLUMN "deleted_at" TIMESTAMP(3);

-- AlterTable
ALTER TABLE "loan_item_detail" ADD COLUMN "deleted_at" TIMESTAMP(3);

-- AlterTable
ALTER TABLE "sloc" ADD COLUMN "deleted_at" TIMESTAMP(3);
26 changes: 26 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ model User {
loanRequests LoanRequest[] @relation("RequesterLoanRequests")
handledLoans LoanRequest[] @relation("LoggieLoanRequests")

deletedAt DateTime? @map("deleted_at") // Soft delete

@@map("user")
}

Expand All @@ -69,6 +71,8 @@ model IHMember {
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
ih IH @relation(fields: [ihId], references: [ihId], onDelete: Cascade)

deletedAt DateTime? @map("deleted_at") // Soft delete

@@unique([userId, ihId])
@@map("ih_member")
}
Expand All @@ -80,6 +84,8 @@ model IH {
members IHMember[] // Multiple POCs via IHMember
items Item[] @relation("ItemIH")

deletedAt DateTime? @map("deleted_at") // Soft delete

@@map("ih")
}

Expand All @@ -100,6 +106,8 @@ model Item {
ih IH @relation("ItemIH", fields: [itemIh], references: [ihId], onDelete: Cascade, onUpdate: Cascade)
loanDetails LoanItemDetail[]

deletedAt DateTime? @map("deleted_at") // Soft delete

@@map("item")
}

Expand All @@ -112,6 +120,8 @@ model LoanItemDetail {
loanRequest LoanRequest @relation(fields: [refNo], references: [refNo], onDelete: Cascade, onUpdate: Cascade)
item Item @relation(fields: [itemId], references: [itemId], onDelete: Cascade, onUpdate: Cascade)

deletedAt DateTime? @map("deleted_at") // Soft delete

@@map("loan_item_detail")
}

Expand All @@ -133,6 +143,8 @@ model LoanRequest {

loanDetails LoanItemDetail[]

deletedAt DateTime? @map("deleted_at") // Soft delete

@@map("loan_request")
}

Expand All @@ -141,5 +153,19 @@ model Sloc {
slocName String @map("sloc_name")
items Item[] @relation("ItemSloc")

deletedAt DateTime? @map("deleted_at") // Soft delete

@@map("sloc")
}

// Audit Log
model AuditLog {
id Int @id @default(autoincrement())
action String // CREATE or UPDATE or DELETE
model String
recordId Int?
userId Int?
timestamp DateTime @default(now())

@@map("audit_log")
}
177 changes: 173 additions & 4 deletions src/lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PrismaClient } from '@prisma/client';
import { getSession } from '@/lib/auth/session';

// add prisma to the NodeJS global type
interface GlobalPrisma {
Expand All @@ -8,17 +9,185 @@ interface GlobalPrisma {
// prevent multiple instances of Prisma Client in development
declare const global: GlobalPrisma & typeof globalThis;

let prisma: PrismaClient;
let prismaBase: PrismaClient;

// check to use this workaround only in development and not in production
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
prismaBase= new PrismaClient();
} else {
if (!global.prisma) {
global.prisma = new PrismaClient();
}
prisma = global.prisma;
prismaBase = global.prisma;
}

export default prisma;
// Support soft delete
const SOFT_DELETE = new Set([
'User',
'IHMember',
'IH',
'Item',
'LoanItemDetail',
'LoanRequest',
'Sloc'
]);

// Get id of record
const MODEL_ID_MAP: Record<string, string> = {
User: 'userId',
IHMember: 'id',
IH: 'ihId',
Item: 'itemId',
LoanItemDetail: 'loanDetailId',
LoanRequest: 'refNo',
Sloc: 'slocId'
};

// Access to model client by name
const MODEL_CLIENT_MAP: Record<string, any> = {
User: prismaBase.user,
IHMember: prismaBase.iHMember,
IH: prismaBase.iH,
Item: prismaBase.item,
LoanItemDetail: prismaBase.loanItemDetail,
LoanRequest: prismaBase.loanRequest,
Sloc: prismaBase.sloc,
};

// Audit
async function audit(
action: string,
model: string,
recordId: number | null,
userId?: number | null,
) {
try {
await prismaBase.auditLog.create({
data: {
action: action.toUpperCase(),
model,
recordId,
userId: userId ?? null,
},
});
} catch (err) {
console.error('Audit log failed:', err);
}
}

export function createPrisma() {
return prismaBase.$extends({
query: {
$allModels: {
async create({ model, args, query }: any) {
const result = await query(args);
const id = MODEL_ID_MAP[model];
const session = await getSession();
const userId = session?.user.userId ?? null;

await audit('create', model, result?.[id] ?? null, userId);
return result;
},

async update({ model, args, query }: any) {
const result = await query(args);
const id = MODEL_ID_MAP[model];
const session = await getSession();
const userId = session?.user.userId ?? null;

await audit('update', model, result?.[id] ?? null, userId);
return result;
},

async delete({ model, args, query }: any) {
const id = MODEL_ID_MAP[model];
const recordId = args?.where?.[id] ?? null;
const table = MODEL_CLIENT_MAP[model];
const session = await getSession();
const userId = session?.user.userId ?? null;

if (!SOFT_DELETE.has(model)) {
const result = await table.delete(args);
await audit('delete', model, recordId, userId);
return result;
}

const result = await table.update({
where: args.where,
data: { deletedAt: new Date() },
});
await audit('delete', model, recordId, userId);
return result;
},

async createMany({ model, args, query }: any) {
const result = await query(args);
const session = await getSession();
const userId = session?.user.userId ?? null;

await audit('createMany', model, null, userId);
return result;
},

async updateMany({ model, args, query }: any) {
const result = await query(args);
const session = await getSession();
const userId = session?.user.userId ?? null;

await audit('updateMany', model, null, userId);
return result;
},

async deleteMany({ model, args, query }: any) {
const table = MODEL_CLIENT_MAP[model];
const session = await getSession();
const userId = session?.user.userId ?? null;

if (!SOFT_DELETE.has(model)) {
const result = await table.deleteMany(args);
await audit('deleteMany', model, null, userId);
return result;
}

const result = await table.updateMany({
...args,
data: { deletedAt: new Date() },
});
await audit('deleteMany', model, null, userId);
return result;
},

// For soft delete
async findMany({ model, args, query }: any) {
if (!SOFT_DELETE.has(model)) {
return query(args);
}

return query({
...args,
where: {
...args.where,
deletedAt: null,
},
});
},

async findFirst({ model, args, query }: any) {
if (!SOFT_DELETE.has(model)) {
return query(args);
}

return query({
...args,
where: {
...args.where,
deletedAt: null,
},
});
},
},
}
})};

const prisma = createPrisma();
export default prisma;