Skip to content

implement database schema - #1

Merged
AzrilFitrayana merged 1 commit into
mainfrom
feat/database-schema
Apr 20, 2026
Merged

AzrilFitrayana merged 1 commit into
mainfrom
feat/database-schema

Conversation

@AzrilFitrayana

@AzrilFitrayana AzrilFitrayana commented Apr 20, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Added database infrastructure to support departments and subjects management with relational constraints.
  • Chores

    • Configured ORM and database client initialization.
    • Added database migration system for schema management.
    • Installed required database dependencies.
    • Added db:generate and db:migrate npm scripts.

@coderabbitai

coderabbitai Bot commented Apr 20, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces Drizzle ORM database infrastructure with PostgreSQL configuration, schema definitions, and migrations. Establishes a Neon serverless database client, creates departments and subjects tables with referential integrity, and provides TypeScript type definitions and relation mappings.

Changes

Cohort / File(s) Summary
Drizzle Configuration
drizzle.config.ts, package.json
Adds Drizzle Kit configuration for PostgreSQL with schema and output paths. Introduces dependencies drizzle-orm, @neondatabase/serverless, dotenv, and devDependency drizzle-kit. Adds npm scripts db:generate and db:migrate.
Database Migrations
drizzle/0000_cynical_ultimates.sql, drizzle/0001_chubby_lady_bullseye.sql
Creates departments and subjects tables with identity primary keys, unique code constraints, and foreign key relationship. Updates description columns to text data type.
Drizzle Metadata
drizzle/meta/0000_snapshot.json, drizzle/meta/0001_snapshot.json, drizzle/meta/_journal.json
Adds Drizzle metadata snapshots documenting PostgreSQL schema structure and migration journal for tracking applied migrations.
Database Schema & Client
src/db/index.ts, src/db/schema/app.ts, src/db/schema/index.ts
Initializes Neon serverless database client wrapped with Drizzle ORM. Defines TypeScript schema for departments and subjects tables with bidirectional relations and inferred select/insert types.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Databases bloom in the PostgreSQL spring,
Departments and subjects now beautifully sing,
With Drizzle's relations and types flowing free,
The schema now dances so gracefully!
From Neon to ORM, the connections align,
A rabbit's delight in a setup so fine! 🌱

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the pull request, which is to establish the database schema with Drizzle ORM configuration and table definitions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/database-schema

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
drizzle/0000_cynical_ultimates.sql (2)

2-2: Consider GENERATED BY DEFAULT AS IDENTITY instead of ALWAYS.

GENERATED ALWAYS rejects any explicit id insert unless the caller uses OVERRIDING SYSTEM VALUE, which complicates seed scripts, data imports, and test fixtures. GENERATED BY DEFAULT still auto-generates when omitted but lets you specify an id when needed. Applies to subjects as well.

Purely a preference — keep as-is if strictness is intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle/0000_cynical_ultimates.sql` at line 2, The column definition uses
"GENERATED ALWAYS AS IDENTITY" for the "id" column (sequence
"departments_id_seq") which forbids explicit id inserts; change it to "GENERATED
BY DEFAULT AS IDENTITY" for both the departments "id" and the subjects "id"
definitions so the DB still auto-generates ids when omitted but allows explicit
ids for seed scripts and imports, updating the identity clause for those fields
accordingly.

11-22: Add an index on subjects.department_id.

PostgreSQL does not automatically index foreign-key columns. Without an index on subjects.department_id:

  • ON DELETE RESTRICT forces a sequential scan of subjects for every attempted department delete.
  • Common queries like "list subjects for a department" will also seq-scan as the table grows.

Recommend adding an index via Drizzle (index("subjects_department_id_idx").on(subjects.departmentId) in src/db/schema/app.ts) and regenerating the migration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle/0000_cynical_ultimates.sql` around lines 11 - 22, Add a b-tree index
on the foreign-key column to avoid sequential scans: in your schema file
(src/db/schema/app.ts) add an index named subjects_department_id_idx on the
subjects.departmentId column (e.g.,
index("subjects_department_id_idx").on(subjects.departmentId)) and then
regenerate the migration so the new migration includes the CREATE INDEX for
subjects.department_id; ensure the index name matches subjects_department_id_idx
and the table/column symbols used are subjects and departmentId.
drizzle/0001_chubby_lady_bullseye.sql (1)

1-2: Consider squashing 0000 + 0001 before merging.

This migration only exists to convert description from varchar(255) → text on tables that were created in the immediately preceding migration. Since this is the initial schema and nothing has been deployed, it's cleaner to delete both 0000/0001 SQL files (and their snapshots/journal entries) and regenerate a single initial migration that declares description as text from the start. Keeps history minimal and avoids a column rewrite on first deploy.

Not blocking — safe to keep as-is if you prefer preserving the authoring history.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle/0001_chubby_lady_bullseye.sql` around lines 1 - 2, The two migrations
(0000 and 0001) currently create tables with description as varchar(255) then
alter them to text (see ALTER TABLE "departments" ALTER COLUMN "description" SET
DATA TYPE text and ALTER TABLE "subjects" ALTER COLUMN "description" SET DATA
TYPE text); remove/squash these two files and their snapshots/journal entries
and regenerate a single initial migration that defines the description columns
as text for the departments and subjects tables to avoid an unnecessary column
rewrite on first deploy.
package.json (1)

10-27: Scripts and dependencies look correct.

db:generate/db:migrate align with the added drizzle.config.ts, and the runtime (@neondatabase/serverless, drizzle-orm, dotenv) and dev (drizzle-kit) dependency split is appropriate.

One optional consideration: @types/node major version (25) should track the Node.js runtime major you deploy on. If you're running on an LTS (e.g. 22/24), pin @types/node to that major instead to avoid typing drift against unavailable runtime APIs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 10 - 27, The `@types/node` devDependency is pinned
to major 25 which may not match your deployed Node.js LTS; update the
"@types/node" entry in package.json (under devDependencies) to the major that
matches your runtime (e.g., ^24.0.0 or ^22.0.0) and re-run your package manager
(npm/yarn) to install the matching types so TypeScript typings align with the
deployed Node.js APIs.
src/db/schema/app.ts (2)

40-45: Unused many destructure.

subjectsRelations destructures many but never uses it.

-export const subjectsRelations = relations(subjects, ({ one, many }) => ({
+export const subjectsRelations = relations(subjects, ({ one }) => ({
   department: one(departments, {
     fields: [subjects.departmentId], //foreign key field(s) in subject table
     references: [departments.id],  //primary key field(s) in department table
   }),
 }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/schema/app.ts` around lines 40 - 45, The relations call for
subjectsRelations destructures both one and many but never uses many; update the
lambda to only destructure the used helper (one) — e.g., change
relations(subjects, ({ one, many }) => ({ ... })) to relations(subjects, ({ one
}) => ({ department: one(departments, { fields: [subjects.departmentId],
references: [departments.id] }) })) so the unused many identifier is removed.

21-23: Consider indexing subjects.department_id.

Foreign key columns are not automatically indexed in Postgres. Queries like "list all subjects for a department" and cascading integrity checks on department updates/deletes will benefit from an explicit index on department_id, especially as the table grows.

♻️ Suggested index
-import { integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
+import { index, integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
@@
-export const subjects = pgTable("subjects", {
-  id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
-  departmentId: integer("department_id")
-    .notNull()
-    .references(() => departments.id, { onDelete: "restrict" }),
-
-  name: varchar("name", { length: 255 }).notNull(),
-  code: varchar("code", { length: 50 }).notNull().unique(),
-  description: text("description"),
-
-  ...timestamps,
-});
+export const subjects = pgTable(
+  "subjects",
+  {
+    id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+    departmentId: integer("department_id")
+      .notNull()
+      .references(() => departments.id, { onDelete: "restrict" }),
+    name: varchar("name", { length: 255 }).notNull(),
+    code: varchar("code", { length: 50 }).notNull().unique(),
+    description: text("description"),
+    ...timestamps,
+  },
+  (t) => ({
+    subjectsDepartmentIdIdx: index("subjects_department_id_idx").on(t.departmentId),
+  }),
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/schema/app.ts` around lines 21 - 23, Add an explicit index on
subjects.department_id: locate the subjects table schema where departmentId
(department_id) is defined and add an index on that column (e.g., using your
schema builder's index API or by adding a migration that creates
subjects_department_id_idx on department_id) so queries like "list all subjects
for a department" and FK cascade checks are performant as the table grows.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/db/index.ts`:
- Around line 1-10: Current code uses drizzle-orm/neon-http which does not
support transactions; replace that setup by importing neon from
'drizzle-orm/neon-serverless' and creating a WebSocket-enabled Pool from
'@neondatabase/serverless' instead of neon(process.env.DATABASE_URL);
instantiate new Pool({ connectionString: process.env.DATABASE_URL }), pass that
Pool into neon(...) to create the sql client and then call drizzle(sql) to
export db (referencing the existing symbols: db, sql, neon, drizzle, Pool); keep
the DATABASE_URL check and ensure the Pool is created only when the env var
exists.

In `@src/db/schema/app.ts`:
- Line 26: The subjects table currently marks the code column as unique (code:
varchar("code", { length: 50 }).notNull().unique()), which enforces global
uniqueness; if you intend codes to be unique only within a department, remove
the column-level unique() from the "code" column and add a composite unique
constraint on (department_id, code) at the table level (i.e., define a unique
index/constraint referencing department_id and code in the subjects schema
instead of per-column unique).
- Around line 4-7: The timestamps object defines createdAt and updatedAt using
timestamp(...).defaultNow(), but defaultNow() only sets the value on INSERT so
updatedAt won't change on UPDATE; modify the timestamps.updatedAt property (in
the timestamps const) to use Drizzle's $onUpdate hook to set the value to the
current timestamp on every update (e.g., call the DB current timestamp via the
appropriate SQL expression) so updates done through Drizzle refresh updatedAt;
if your app performs raw SQL updates outside Drizzle, instead add a Postgres
trigger to set updated_at on each UPDATE to ensure consistency.

---

Nitpick comments:
In `@drizzle/0000_cynical_ultimates.sql`:
- Line 2: The column definition uses "GENERATED ALWAYS AS IDENTITY" for the "id"
column (sequence "departments_id_seq") which forbids explicit id inserts; change
it to "GENERATED BY DEFAULT AS IDENTITY" for both the departments "id" and the
subjects "id" definitions so the DB still auto-generates ids when omitted but
allows explicit ids for seed scripts and imports, updating the identity clause
for those fields accordingly.
- Around line 11-22: Add a b-tree index on the foreign-key column to avoid
sequential scans: in your schema file (src/db/schema/app.ts) add an index named
subjects_department_id_idx on the subjects.departmentId column (e.g.,
index("subjects_department_id_idx").on(subjects.departmentId)) and then
regenerate the migration so the new migration includes the CREATE INDEX for
subjects.department_id; ensure the index name matches subjects_department_id_idx
and the table/column symbols used are subjects and departmentId.

In `@drizzle/0001_chubby_lady_bullseye.sql`:
- Around line 1-2: The two migrations (0000 and 0001) currently create tables
with description as varchar(255) then alter them to text (see ALTER TABLE
"departments" ALTER COLUMN "description" SET DATA TYPE text and ALTER TABLE
"subjects" ALTER COLUMN "description" SET DATA TYPE text); remove/squash these
two files and their snapshots/journal entries and regenerate a single initial
migration that defines the description columns as text for the departments and
subjects tables to avoid an unnecessary column rewrite on first deploy.

In `@package.json`:
- Around line 10-27: The `@types/node` devDependency is pinned to major 25 which
may not match your deployed Node.js LTS; update the "@types/node" entry in
package.json (under devDependencies) to the major that matches your runtime
(e.g., ^24.0.0 or ^22.0.0) and re-run your package manager (npm/yarn) to install
the matching types so TypeScript typings align with the deployed Node.js APIs.

In `@src/db/schema/app.ts`:
- Around line 40-45: The relations call for subjectsRelations destructures both
one and many but never uses many; update the lambda to only destructure the used
helper (one) — e.g., change relations(subjects, ({ one, many }) => ({ ... })) to
relations(subjects, ({ one }) => ({ department: one(departments, { fields:
[subjects.departmentId], references: [departments.id] }) })) so the unused many
identifier is removed.
- Around line 21-23: Add an explicit index on subjects.department_id: locate the
subjects table schema where departmentId (department_id) is defined and add an
index on that column (e.g., using your schema builder's index API or by adding a
migration that creates subjects_department_id_idx on department_id) so queries
like "list all subjects for a department" and FK cascade checks are performant
as the table grows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30fbb007-6107-48ad-a161-ff69aa7eeac0

📥 Commits

Reviewing files that changed from the base of the PR and between ce4c88b and dc0ca96.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • drizzle.config.ts
  • drizzle/0000_cynical_ultimates.sql
  • drizzle/0001_chubby_lady_bullseye.sql
  • drizzle/meta/0000_snapshot.json
  • drizzle/meta/0001_snapshot.json
  • drizzle/meta/_journal.json
  • package.json
  • src/db/index.ts
  • src/db/schema/app.ts
  • src/db/schema/index.ts

Comment thread src/db/index.ts
Comment on lines +1 to +10
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';

if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is not defined');
}

const sql = neon(process.env.DATABASE_URL);
export const db = drizzle(sql);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

drizzle-orm neon-http vs neon-serverless transaction support

💡 Result:

Drizzle ORM's neon-http driver does not support interactive transactions (db.transaction), throwing "No transactions support in neon-http driver". It is optimized for single, non-interactive queries over HTTP, faster for simple operations but lacks full session/transaction features. The neon-serverless driver (WebSocket-based) supports full interactive transactions via Pool or Client from @neondatabase/serverless, providing session support and pg compatibility. Use it for transactions: import { drizzle } from 'drizzle-orm/neon-serverless'; with a Pool. Official Drizzle docs recommend neon-http for speed in non-interactive cases, neon-serverless for interactive transactions. Neon's driver docs confirm HTTP for non-interactive tx (limited, all-or-nothing), WebSockets/Pool for full support. Errors and fixes in SO/posts confirm switching to neon-serverless resolves transaction issues. No evidence of neon-http gaining full tx support as of 2026.

Citations:


neon-http adapter does not support interactive transactions.

The drizzle-orm/neon-http driver uses Neon's HTTP endpoint, which only supports single-statement queries and db.batch([...]). Calling db.transaction(async (tx) => { ... }) will throw "No transactions support in neon-http driver" at runtime. If you anticipate multi-statement atomic writes (likely for department/subject mutations and future tables), switch to drizzle-orm/neon-serverless with the WebSocket Pool from @neondatabase/serverless, which supports full transactions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/index.ts` around lines 1 - 10, Current code uses drizzle-orm/neon-http
which does not support transactions; replace that setup by importing neon from
'drizzle-orm/neon-serverless' and creating a WebSocket-enabled Pool from
'@neondatabase/serverless' instead of neon(process.env.DATABASE_URL);
instantiate new Pool({ connectionString: process.env.DATABASE_URL }), pass that
Pool into neon(...) to create the sql client and then call drizzle(sql) to
export db (referencing the existing symbols: db, sql, neon, drizzle, Pool); keep
the DATABASE_URL check and ensure the Pool is created only when the env var
exists.

Comment thread src/db/schema/app.ts
Comment on lines +4 to +7
const timestamps = {
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

drizzle-orm timestamp $onUpdate auto update column

💡 Result:

In Drizzle ORM, use the $onUpdate or $onUpdateFn method on timestamp columns to automatically set a value (like current timestamp) during UPDATE queries at runtime. This does not affect schema generation/migrations (drizzle-kit ignores it); it's handled by the ORM during query execution. Example for PostgreSQL/MySQL/SQLite (works across dialects since v0.30.5): import { timestamp, pgTable } from 'drizzle-orm/pg-core'; // or mysql-core/sqlite-core export const users = pgTable('users', { id: serial('id').primaryKey, updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow.$onUpdate( => new Date), }); - The callback runs on UPDATE if no value provided for the column. - If no .default or $defaultFn, it also runs on INSERT. - For DB server time (not app server): Use mode: 'string' and sqlnow: timestamp('updated_at', { mode: 'string' }).$onUpdate( => sqlnow). - MySQL-specific schema-level: .onUpdateCurrentTimestamp or .default(sqlCURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) for DB-handled updates (drizzle-kit supported). When updating, omit the column from .set: await db.update(users).set({ name: 'New' }).where(eq(users.id, 1)); The ORM injects the $onUpdate value automatically. Note: Official docs lack detail (mostly GitHub release/PRs/issues). Use latest version (beta as of 2026 has fixes for sql in $onUpdate).

Citations:


🏁 Script executed:

# Check if Drizzle ORM is used in the project
rg "drizzle" package.json -A 5 -B 5

Repository: AzrilFitrayana/classroom-backend

Length of output: 743


🏁 Script executed:

# Look for the specific file mentioned in the review
cat -n src/db/schema/app.ts

Repository: AzrilFitrayana/classroom-backend

Length of output: 2661


🏁 Script executed:

# Search for any existing $onUpdate usage in the codebase to verify syntax
rg "\$onUpdate" --type ts --type js -B 2 -A 2

Repository: AzrilFitrayana/classroom-backend

Length of output: 58


updatedAt will not auto-update on row modification.

defaultNow() only sets the value on INSERT. Subsequent UPDATEs will leave updated_at unchanged, so the column name becomes misleading. Use Drizzle's $onUpdate (application-side) or a Postgres trigger (DB-side) to refresh it on every update.

♻️ Suggested app-side fix
 const timestamps = {
   createdAt: timestamp("created_at").defaultNow().notNull(),
-  updatedAt: timestamp("updated_at").defaultNow().notNull(),
+  updatedAt: timestamp("updated_at")
+    .defaultNow()
+    .$onUpdate(() => new Date())
+    .notNull(),
 };

Note: $onUpdate only fires for updates performed through Drizzle; if you expect raw SQL updates from elsewhere, add a Postgres trigger instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/schema/app.ts` around lines 4 - 7, The timestamps object defines
createdAt and updatedAt using timestamp(...).defaultNow(), but defaultNow() only
sets the value on INSERT so updatedAt won't change on UPDATE; modify the
timestamps.updatedAt property (in the timestamps const) to use Drizzle's
$onUpdate hook to set the value to the current timestamp on every update (e.g.,
call the DB current timestamp via the appropriate SQL expression) so updates
done through Drizzle refresh updatedAt; if your app performs raw SQL updates
outside Drizzle, instead add a Postgres trigger to set updated_at on each UPDATE
to ensure consistency.

Comment thread src/db/schema/app.ts
.references(() => departments.id, { onDelete: "restrict" }),

name: varchar("name", { length: 255 }).notNull(),
code: varchar("code", { length: 50 }).notNull().unique(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Confirm subjects.code should be globally unique.

code is declared unique() at the table level, so two departments cannot share the same subject code (e.g., both CS and MATH defining "101"). If codes are intended to be unique per department, replace the column-level unique with a composite unique on (department_id, code).

♻️ Alternative if per-department uniqueness is desired
-import { integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
+import { integer, pgTable, text, timestamp, unique, varchar } from "drizzle-orm/pg-core";
@@
-export const subjects = pgTable("subjects", {
-  id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
-  departmentId: integer("department_id")
-    .notNull()
-    .references(() => departments.id, { onDelete: "restrict" }),
-
-  name: varchar("name", { length: 255 }).notNull(),
-  code: varchar("code", { length: 50 }).notNull().unique(),
-  description: text("description"),
-
-  ...timestamps,
-});
+export const subjects = pgTable(
+  "subjects",
+  {
+    id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+    departmentId: integer("department_id")
+      .notNull()
+      .references(() => departments.id, { onDelete: "restrict" }),
+    name: varchar("name", { length: 255 }).notNull(),
+    code: varchar("code", { length: 50 }).notNull(),
+    description: text("description"),
+    ...timestamps,
+  },
+  (t) => ({
+    subjectsDeptCodeUnique: unique("subjects_department_id_code_unique").on(
+      t.departmentId,
+      t.code,
+    ),
+  }),
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/schema/app.ts` at line 26, The subjects table currently marks the code
column as unique (code: varchar("code", { length: 50 }).notNull().unique()),
which enforces global uniqueness; if you intend codes to be unique only within a
department, remove the column-level unique() from the "code" column and add a
composite unique constraint on (department_id, code) at the table level (i.e.,
define a unique index/constraint referencing department_id and code in the
subjects schema instead of per-column unique).

@AzrilFitrayana
AzrilFitrayana merged commit e064d2c into main Apr 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant