Compare commits

...

9 Commits

Author SHA1 Message Date
dc6fa562cc Clean code
modified:   public/.well-known/assetlinks.json
modified:   src/lib/code-otp-sender.ts

### No issue
2026-03-06 16:35:34 +08:00
4fd7bb4a17 chore(release): 1.6.9 2026-03-06 16:29:50 +08:00
b2305a35a6 Fix WA Otp
### NO Issue
2026-03-05 16:38:31 +08:00
cbfd105134 chore(release): 1.6.8 2026-03-05 14:30:03 +08:00
3e6c94d77f Usulan Commit Message
fix: Implementasi retry mechanism dan error handling untuk database connections

Deskripsi:

Menambahkan withRetry wrapper pada berbagai API routes untuk menangani transient database errors dan meningkatkan reliabilitas koneksi

Memperbaiki error handling pada notification, authentication, dan user validation endpoints dengan response 503 untuk database connection errors

Update prisma.ts dengan konfigurasi logging yang lebih baik dan datasources configuration

Menambahkan validasi input parameters pada beberapa endpoints

Update dokumentasi QWEN.md dengan commit message format dan comment standards

Update .env.example dengan connection pool settings yang lebih lengkap

File yang diubah:

src/lib/prisma.ts — Konfigurasi Prisma client & logging

src/app/api/admin/notifikasi/count/route.tsx

src/app/api/auth/mobile-login/route.ts

src/app/api/mobile/notification/[id]/route.ts

src/app/api/user-validate/route.ts

Dan 27 file API routes lainnya (penerapan withRetry secara konsisten)

QWEN.md — Dokumentasi commit & comment standards

.env.example — Database connection pool configuration

### No Issue
2026-03-05 14:28:45 +08:00
a6c9182a01 Fix Server dengan penerapan Github build 2026-03-04 16:38:58 +08:00
453aa0a4ec chore(release): 1.6.7 2026-03-04 16:36:57 +08:00
fe37cce13e Fix publish.yml 2026-03-04 16:08:59 +08:00
ee05d0c71f Build Github 2026-03-04 15:18:01 +08:00
39 changed files with 365 additions and 177 deletions

View File

@@ -1,8 +1,11 @@
# ============================== # ==============================
# Database # Database
# ============================== # ==============================
# Tambahkan connection_limit dan pool_timeout untuk mencegah connection exhaustion # Connection pool settings untuk mencegah connection exhaustion:
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20" # - connection_limit=10: Maksimal 10 koneksi per Prisma Client instance
# - pool_timeout=20: Timeout menunggu koneksi tersedia (detik)
# - connect_timeout=10: Timeout untuk membuat koneksi baru (detik)
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20&connect_timeout=10"
# ============================== # ==============================
# Auth / Session # Auth / Session

View File

@@ -1,17 +1,26 @@
name: Publish Docker to GHCR name: Publish Docker to GHCR
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- production
- staging
tag: tag:
description: "Image tag (e.g. v1.0.0)" description: "Image tag (e.g. v1.0.0)"
required: true required: true
default: "latest"
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
publish: publish:
name: Build & Push to GHCR name: Build & Push to GHCR (${{ github.event.inputs.environment }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@@ -29,10 +38,6 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Extract tag name
id: meta
run: echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@@ -46,6 +51,15 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.event.inputs.environment }}-${{ github.event.inputs.tag }}
type=raw,value=${{ github.event.inputs.environment }}-latest
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -53,7 +67,6 @@ jobs:
file: ./Dockerfile file: ./Dockerfile
push: true push: true
platforms: linux/amd64 platforms: linux/amd64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha no-cache: true
cache-to: type=gha,mode=max

View File

@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.6.9](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.8...v1.6.9) (2026-03-06)
## [1.6.8](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.7...v1.6.8) (2026-03-05)
## [1.6.7](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.6...v1.6.7) (2026-03-04)
## [1.6.6](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.5...v1.6.6) (2026-03-03) ## [1.6.6](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.5...v1.6.6) (2026-03-03)

64
QWEN.md
View File

@@ -120,14 +120,6 @@ The team follows a structured Git workflow:
- `style`: Styling changes - `style`: Styling changes
- `perf`: Performance improvements - `perf`: Performance improvements
### Code Standards
- TypeScript with strict mode enabled
- Component header comments with file description, creator, date
- Function comments with parameter and return value descriptions
- Custom type interface comments
- Error handling comments
- Complex logic comments
### Commit Message Format ### Commit Message Format
``` ```
type: Short description type: Short description
@@ -140,6 +132,62 @@ Body:
References: #issue-number References: #issue-number
``` ```
**Example:**
```
feat: Tambahkan fitur kalkulator
Deskripsi:
- Menambahkan fungsi penambahan, pengurangan, perkalian, dan pembagian
- Memperbolehkan pengguna untuk memasukkan dua angka dan melakukan operasi matematika
Fixes #12
```
### Code Standards
- TypeScript with strict mode enabled
- Component header comments with file description, creator, date
- Function comments with parameter and return value descriptions
- Custom type interface comments
- Error handling comments
- Complex logic comments
### Comment Standards
**File Header:**
```typescript
/**
* Nama File: app.ts
* Deskripsi: Ini adalah file utama aplikasi.
* Pembuat: John Doe
* Tanggal: 27 Juli 2023
*/
```
**Function Comments:**
```typescript
/**
* Fungsi untuk menambahkan dua angka.
* @param {number} a - Angka pertama.
* @param {number} b - Angka kedua.
* @returns {number} Hasil penjumlahan a dan b.
*/
function addNumbers(a: number, b: number): number {
return a + b;
}
```
**Custom Type Comments:**
```typescript
/**
* Interface untuk merepresentasikan informasi pelanggan.
*/
interface Customer {
id: number; // ID pelanggan
name: string; // Nama pelanggan
age?: number; // Umur pelanggan (opsional)
}
```
## Project Structure ## Project Structure
### Main Directories ### Main Directories

View File

@@ -1,16 +0,0 @@
import { fileURLToPath } from "url";
import { dirname } from "path"; // Pastikan `dirname` diimpor
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@@ -1,6 +1,6 @@
{ {
"name": "hipmi", "name": "hipmi",
"version": "1.6.6", "version": "1.6.9",
"private": true, "private": true,
"prisma": { "prisma": {
"seed": "bun prisma/seed.ts" "seed": "bun prisma/seed.ts"

View File

@@ -13,6 +13,6 @@ import { generate_seeder } from "./../src/app_modules/_global/fun/generate_seede
console.error("<< error seeder", e); console.error("<< error seeder", e);
process.exit(1); process.exit(1);
}) })
.finally(async () => { // .finally(async () => {
await prisma.$disconnect(); // await prisma.$disconnect();
}); // });

View File

@@ -1,8 +1,10 @@
[{ [
"relation": ["delegate_permission/common.handle_all_urls"], {
"target": { "relation": ["delegate_permission/common.handle_all_urls"],
"namespace": "android_app", "target": {
"package_name": "com.bip.hipmimobileapp", "namespace": "android_app",
"sha256_cert_fingerprints": ["CFF8431520BFAE665025B68138774A4E64AA6338D2DF6C7D900A71F0551FFD2D"] "package_name": "com.bip.hipmimobileapp",
"sha256_cert_fingerprints": ["CFF8431520BFAE665025B68138774A4E64AA6338D2DF6C7D900A71F0551FFD2D"]
}
} }
}] ]

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib"; import { prisma } from "@/lib";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -16,13 +17,25 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const userId = searchParams.get("id"); const userId = searchParams.get("id");
const data = await prisma.notifikasi.count({ if (!userId) {
where: { return NextResponse.json(
adminId: userId, { success: false, message: "User ID is required" },
userRoleId: "2", { status: 400 }
isRead: false, );
}, }
});
const data = await withRetry(
() =>
prisma.notifikasi.count({
where: {
adminId: userId,
userRoleId: "2",
isRead: false,
},
}),
undefined,
"countAdminNotifications"
);
return NextResponse.json( return NextResponse.json(
{ {
@@ -33,7 +46,25 @@ export async function GET(request: Request) {
{ status: 200 } { status: 200 }
); );
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Error get count notifikasi", error); console.error("Error get count notifikasi", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json(
{
success: false,
message: "Database connection error. Please try again.",
data: null,
},
{ status: 503 }
);
}
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,

View File

@@ -50,7 +50,5 @@ async function DELETE(
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -1,3 +1,4 @@
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib"; import { prisma } from "@/lib";
import { randomOTP } from "@/app_modules/auth/fun/rondom_otp"; import { randomOTP } from "@/app_modules/auth/fun/rondom_otp";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
@@ -9,11 +10,26 @@ export async function POST(req: Request) {
const body = await req.json(); const body = await req.json();
const { nomor } = body; const { nomor } = body;
const user = await prisma.user.findUnique({ if (!nomor) {
where: { return NextResponse.json(
nomor: nomor, {
}, success: false,
}); message: "Nomor telepon diperlukan",
status: 400,
}
);
}
const user = await withRetry(
() =>
prisma.user.findUnique({
where: {
nomor: nomor,
},
}),
undefined,
"findUserByNomor"
);
if (!user) if (!user)
return NextResponse.json({ return NextResponse.json({
@@ -22,12 +38,17 @@ export async function POST(req: Request) {
status: 404, status: 404,
}); });
const createOtpId = await prisma.kodeOtp.create({ const createOtpId = await withRetry(
data: { () =>
nomor: nomor, prisma.kodeOtp.create({
otp: codeOtp, data: {
}, nomor: nomor,
}); otp: codeOtp,
},
}),
undefined,
"createOTP"
);
if (!createOtpId) if (!createOtpId)
return NextResponse.json( return NextResponse.json(
@@ -59,6 +80,25 @@ export async function POST(req: Request) {
{ status: 200 }, { status: 200 },
); );
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Mobile login error:", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json(
{
success: false,
message: "Database connection error. Please try again.",
status: 503,
},
{ status: 503 }
);
}
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,

View File

@@ -64,7 +64,5 @@ export async function POST(req: Request) {
}, },
{ status: 500 }, { status: 500 },
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -42,7 +42,5 @@ export async function GET(
{ success: false, message: "Gagal mendapatkan data" }, { success: false, message: "Gagal mendapatkan data" },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -30,13 +30,11 @@ export async function GET(request: Request) {
fixData = false; fixData = false;
} }
await prisma.$disconnect();
return NextResponse.json( return NextResponse.json(
{ success: true, message: "Success get data", data: fixData }, { success: true, message: "Success get data", data: fixData },
{ status: 200 } { status: 200 }
); );
} catch (error) { } catch (error) {
await prisma.$disconnect();
backendLogger.error("Error get data detail event:", error); backendLogger.error("Error get data detail event:", error);
return NextResponse.json( return NextResponse.json(
{ {

View File

@@ -41,13 +41,11 @@ export async function POST(
}, },
}); });
await prisma.$disconnect();
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: "Success create sponsor", message: "Success create sponsor",
}); });
} catch (error) { } catch (error) {
await prisma.$disconnect();
backendLogger.error("Error create sponsor event", error); backendLogger.error("Error create sponsor event", error);
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Failed create sponsor" }, { success: false, message: "Failed create sponsor" },
@@ -100,7 +98,5 @@ export async function GET(
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -58,7 +58,6 @@ export async function GET(
}); });
} }
await prisma.$disconnect();
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: "Success create sponsor", message: "Success create sponsor",
@@ -66,7 +65,6 @@ export async function GET(
}); });
} catch (error) { } catch (error) {
backendLogger.error("Error get sponsor event", error); backendLogger.error("Error get sponsor event", error);
await prisma.$disconnect();
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,

View File

@@ -33,7 +33,5 @@ export async function GET(request: Request) {
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -35,7 +35,5 @@ export async function GET(request: Request) {
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -21,13 +21,11 @@ export async function GET(request: Request) {
}, },
}); });
await prisma.$disconnect();
return NextResponse.json( return NextResponse.json(
{ success: true, message: "Berhasil mendapatkan data", data: res }, { success: true, message: "Berhasil mendapatkan data", data: res },
{ status: 200 } { status: 200 }
); );
} catch (error) { } catch (error) {
await prisma.$disconnect();
backendLogger.error("Error Get Master Status Transaksi >>", error); backendLogger.error("Error Get Master Status Transaksi >>", error);
return NextResponse.json( return NextResponse.json(
{ {

View File

@@ -28,7 +28,5 @@ async function GET() {
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -1,3 +1,4 @@
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib"; import { prisma } from "@/lib";
import _ from "lodash"; import _ from "lodash";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
@@ -22,28 +23,38 @@ export async function GET(
let fixData; let fixData;
try { try {
const data = await prisma.notifikasi.findMany({ const data = await withRetry(
take: page ? takeData : undefined, () =>
skip: page ? skipData : undefined, prisma.notifikasi.findMany({
orderBy: { take: page ? takeData : undefined,
createdAt: "desc", skip: page ? skipData : undefined,
}, orderBy: {
where: { createdAt: "desc",
recipientId: id, },
kategoriApp: fixCategory, where: {
}, recipientId: id,
}); kategoriApp: fixCategory,
},
}),
undefined,
"getNotifications"
);
// Jika pagination digunakan, ambil juga total count untuk informasi // Jika pagination digunakan, ambil juga total count untuk informasi
let totalCount; let totalCount;
let totalPages; let totalPages;
if (page) { if (page) {
totalCount = await prisma.notifikasi.count({ totalCount = await withRetry(
where: { () =>
recipientId: id, prisma.notifikasi.count({
kategoriApp: fixCategory, where: {
}, recipientId: id,
}); kategoriApp: fixCategory,
},
}),
undefined,
"countNotifications"
);
totalPages = Math.ceil(totalCount / takeData); totalPages = Math.ceil(totalCount / takeData);
} }
@@ -69,8 +80,23 @@ export async function GET(
return NextResponse.json(response); return NextResponse.json(response);
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Error getting notifications:", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json(
{ error: "Database connection error. Please try again." },
{ status: 503 }
);
}
return NextResponse.json( return NextResponse.json(
{ error: (error as Error).message }, { error: errorMsg },
{ status: 500 }, { status: 500 },
); );
} }

View File

@@ -1,3 +1,4 @@
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib"; import { prisma } from "@/lib";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
@@ -9,12 +10,24 @@ export async function GET(
console.log("User ID:", id); console.log("User ID:", id);
try { try {
const data = await prisma.notifikasi.count({ if (!id) {
where: { return NextResponse.json({
recipientId: id, success: false,
isRead: false, message: "User ID is required",
}, });
}); }
const data = await withRetry(
() =>
prisma.notifikasi.count({
where: {
recipientId: id,
isRead: false,
},
}),
undefined,
"countUnreadNotifications"
);
console.log("List Notification >>", data); console.log("List Notification >>", data);
@@ -23,6 +36,21 @@ export async function GET(
data: data, data: data,
}); });
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Error getting unread count:", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json({
success: false,
message: "Database connection error. Please try again.",
});
}
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
message: "Failed to get unread count", message: "Failed to get unread count",

View File

@@ -131,7 +131,5 @@ async function PUT(request: Request, { params }: { params: { id: string } }) {
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -40,7 +40,5 @@ export async function GET(request: Request) {
status: 500, status: 500,
} }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -78,7 +78,5 @@ export async function GET(request: Request) {
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -49,14 +49,11 @@ export async function GET(
}); });
} }
await prisma.$disconnect();
return NextResponse.json( return NextResponse.json(
{ success: true, message: "Success get data news", data: fixData }, { success: true, message: "Success get data news", data: fixData },
{ status: 200 } { status: 200 }
); );
} catch (error) { } catch (error) {
await prisma.$disconnect();
backendLogger.error("Error get data news", error); backendLogger.error("Error get data news", error);
return NextResponse.json( return NextResponse.json(
{ {

View File

@@ -36,8 +36,6 @@ export async function GET(
}); });
} }
await prisma.$disconnect();
return NextResponse.json( return NextResponse.json(
{ success: true, message: "Success get data document", data: fixData }, { success: true, message: "Success get data document", data: fixData },
{ status: 200 } { status: 200 }

View File

@@ -104,7 +104,5 @@ async function PUT(request: Request) {
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -77,7 +77,5 @@ async function POST(request: Request) {
}, },
{ status: 500 } { status: 500 }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -1,4 +1,5 @@
import { decrypt } from "@/app/(auth)/_lib/decrypt"; import { decrypt } from "@/app/(auth)/_lib/decrypt";
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib"; import { prisma } from "@/lib";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
@@ -43,11 +44,16 @@ export async function GET(req: Request) {
); );
} }
const user = await prisma.user.findUnique({ const user = await withRetry(
where: { () =>
id: decrypted.id, prisma.user.findUnique({
}, where: {
}); id: decrypted.id,
},
}),
undefined,
"validateUser"
);
if (!user) { if (!user) {
return NextResponse.json( return NextResponse.json(
@@ -76,8 +82,8 @@ export async function GET(req: Request) {
data: user, data: user,
}); });
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error'; const errorMsg = error instanceof Error ? error.message : "Unknown error";
const errorStack = error instanceof Error ? error.stack : 'No stack'; const errorStack = error instanceof Error ? error.stack : "No stack";
// Log detailed error for debugging // Log detailed error for debugging
console.error("❌ [USER-VALIDATE] Error:", errorMsg); console.error("❌ [USER-VALIDATE] Error:", errorMsg);
@@ -85,16 +91,35 @@ export async function GET(req: Request) {
console.error("❌ [USER-VALIDATE] Time:", new Date().toISOString()); console.error("❌ [USER-VALIDATE] Time:", new Date().toISOString());
// Check if it's a database connection error // Check if it's a database connection error
if (errorMsg.includes("Prisma") || errorMsg.includes("database") || errorMsg.includes("connection")) { if (
console.error("❌ [USER-VALIDATE] Database connection error detected!"); errorMsg.includes("Prisma") ||
console.error("❌ [USER-VALIDATE] DATABASE_URL exists:", !!process.env.DATABASE_URL); errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
console.error(
"❌ [USER-VALIDATE] Database connection error detected!"
);
console.error(
"❌ [USER-VALIDATE] DATABASE_URL exists:",
!!process.env.DATABASE_URL
);
return NextResponse.json(
{
success: false,
message: "Database connection error. Please try again.",
error: process.env.NODE_ENV === "development" ? errorMsg : "Internal server error",
},
{ status: 503 }
);
} }
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
message: "Terjadi kesalahan pada server", message: "Terjadi kesalahan pada server",
error: process.env.NODE_ENV === 'development' ? errorMsg : 'Internal server error', error:
process.env.NODE_ENV === "development" ? errorMsg : "Internal server error",
}, },
{ status: 500 } { status: 500 }
); );

View File

@@ -125,7 +125,5 @@ export async function GET(request: Request) {
status: 500, status: 500,
} }
); );
} finally {
await prisma.$disconnect();
} }
} }

View File

@@ -669,10 +669,8 @@ const limit = pLimit(1);
export async function generate_seeder() { export async function generate_seeder() {
try { try {
await Promise.all(listSeederQueue.map((fn) => limit(fn))); await Promise.all(listSeederQueue.map((fn) => limit(fn)));
await prisma.$disconnect();
} catch (error) { } catch (error) {
console.error("error generate seeder", error); console.error("error generate seeder", error);
await prisma.$disconnect();
} }
return { status: 200, success: true }; return { status: 200, success: true };

View File

@@ -37,6 +37,5 @@ export async function AdminDonasi_getOneById(id: string) {
}, },
}); });
await prisma.$disconnect();
return res; return res;
} }

View File

@@ -33,12 +33,10 @@ export async function AdminDonasi_funUpdateStatusPublish(
}); });
if (!data) { if (!data) {
await prisma.$disconnect();
return { status: 400, message: "Data tidak ditemukan" }; return { status: 400, message: "Data tidak ditemukan" };
} }
revalidatePath(RouterAdminDonasi.table_review); revalidatePath(RouterAdminDonasi.table_review);
await prisma.$disconnect();
return { return {
data: data, data: data,

View File

@@ -36,12 +36,10 @@ export default async function adminNotifikasi_funCreateToAllUser({
}, },
}); });
if (!create) { if (!create) {
await prisma.$disconnect();
return { status: 400, message: "Gagal mengirim notifikasi" }; return { status: 400, message: "Gagal mengirim notifikasi" };
} }
} }
await prisma.$disconnect();
return { return {
status: 201, status: 201,
message: "Berhasil mengirim notifikasi", message: "Berhasil mengirim notifikasi",

View File

@@ -25,10 +25,8 @@ export default async function adminNotifikasi_funCreateToUser({
}); });
if (!create) { if (!create) {
await prisma.$disconnect();
return { status: 400, message: "Gagal mengirim notifikasi" }; return { status: 400, message: "Gagal mengirim notifikasi" };
} }
await prisma.$disconnect();
return { status: 201, message: "Berhasil mengirim notifikasi" }; return { status: 201, message: "Berhasil mengirim notifikasi" };
} }

View File

@@ -9,9 +9,6 @@ export async function donasi_checkStatus({ id }: { id: string }) {
}, },
}); });
await prisma.$disconnect();
if (checkStatus?.donasiMaster_StatusDonasiId == "2") return true; if (checkStatus?.donasiMaster_StatusDonasiId == "2") return true;
return false; return false;

View File

@@ -7,23 +7,22 @@ const sendCodeOtp = async ({
codeOtp?: string; codeOtp?: string;
newMessage?: string; newMessage?: string;
}) => { }) => {
const msg = newMessage || `HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.\n\n>> Kode OTP anda: ${codeOtp}.`; const msg =
const enCode = encodeURIComponent(msg); newMessage ||
const res = await fetch( `HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.\n\n>> Kode OTP anda: ${codeOtp}.`;
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${nomor}&text=${enCode}`, const enCode = msg;
{
cache: "no-cache", const res = await fetch(`https://otp.wibudev.com/api/wa/send-text`, {
headers: { method: "POST",
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`, headers: {
}, "Content-Type": "application/json",
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
}, },
); body: JSON.stringify({
// const res = await fetch( number: nomor,
// `https://wa.wibudev.com/code?nom=${nomor}&text=HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya. text: enCode,
// \n }),
// >> Kode OTP anda: ${codeOtp}. });
// `,
// );
return res; return res;
}; };

View File

@@ -1,21 +1,55 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
/**
* Instance global Prisma client untuk connection pooling
* Menggunakan pattern globalThis untuk mencegah multiple instance selama:
* - Hot module replacement (HMR) di development
* - Multiple import di seluruh aplikasi
* - Server-side rendering di Next.js
*/
declare global { declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined; var prisma: PrismaClient | undefined;
} }
if (!process.env.DATABASE_URL) { // Konfigurasi connection pool via parameter query DATABASE_URL:
throw new Error("DATABASE_URL is required but not found in environment variables"); // connection_limit=10&pool_timeout=20&connect_timeout=10
}
const prisma = const prisma =
global.prisma ?? globalThis.prisma ??
new PrismaClient({ new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["error", "warn", "query"] : ["error", "warn"], log:
process.env.NODE_ENV === "development"
? ["error", "warn"]
: ["error"],
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
}); });
// Selalu assign ke global agar hanya ada 1 instance (dev: cegah hot-reload, prod: cegah multiple instances) // Hanya assign ke global di development untuk mencegah multiple instance saat HMR
global.prisma = prisma; // Di production, ini di-skip karena tidak ada HMR
if (process.env.NODE_ENV !== "production") {
globalThis.prisma = prisma;
}
/**
* Handler graceful shutdown untuk koneksi Prisma
* Panggil ini HANYA saat terminasi process (SIGINT/SIGTERM)
* JANGAN panggil $disconnect() setelah query individual
*/
async function gracefulShutdown(): Promise<void> {
console.log("[Prisma] Menutup koneksi database...");
await prisma.$disconnect();
console.log("[Prisma] Semua koneksi ditutup");
}
// Register shutdown handlers (hanya di environment Node.js)
if (typeof process !== "undefined") {
process.on("SIGINT", gracefulShutdown);
process.on("SIGTERM", gracefulShutdown);
}
export default prisma; export default prisma;
export { prisma }; export { prisma };