feat(db): migration mekanizması ve ilk index migration'ı

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 02:35:01 +03:00
parent 3644e081b1
commit b79951601c
7 changed files with 82 additions and 6 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "butcem",
"version": "0.4.0",
"version": "0.5.0",
"private": true,
"description": "BÜTÇEM — aile bütçe, cari, birikim, ev stoğu ve akıllı alışveriş uygulaması",
"engines": {
@@ -21,7 +21,8 @@
"seed": "tsx --conditions=react-server --env-file=.env.local scripts/seed.ts",
"seed:reset": "tsx --conditions=react-server --env-file=.env.local scripts/seed.ts --reset",
"icons": "tsx scripts/generate-icons.ts",
"test:integration": "vitest run --config vitest.integration.config.mts"
"test:integration": "vitest run --config vitest.integration.config.mts",
"migrate": "tsx --conditions=react-server --env-file=.env.local scripts/migrate.ts"
},
"dependencies": {
"@hookform/resolvers": "^7.0.2",
+17
View File
@@ -0,0 +1,17 @@
/**
* Migration'ları uygular: npm run migrate (canlıda deploy sonrası; bkz. docs/DEPLOY.md, Faz 15).
* `server-only` modülleri Next dışında yükleyebilmek için --conditions=react-server ile çalışır.
*/
import mongoose from "mongoose";
import { getMongoClient } from "@/lib/db/mongo";
import { runMigrations } from "./migrations";
runMigrations()
.catch((err: unknown) => {
console.error(err instanceof Error ? err.message : err);
process.exitCode = 1;
})
.finally(async () => {
await mongoose.disconnect();
await getMongoClient().close();
});
View File
+15
View File
@@ -0,0 +1,15 @@
import { AuditLogModel } from "@/models/audit-log";
import { HouseholdModel } from "@/models/household";
import { HouseholdMemberModel } from "@/models/household-member";
import type { Migration } from "./types";
/** Hane, üyelik ve denetim kaydı index'leri (Mongoose şemalarındaki tanımlarla). */
export const migration: Migration = {
name: "001-initial-indexes",
description: "households, householdMembers ve auditLogs index'leri",
async up() {
await HouseholdModel.createIndexes();
await HouseholdMemberModel.createIndexes();
await AuditLogModel.createIndexes();
},
};
+35
View File
@@ -0,0 +1,35 @@
import { getDb } from "@/lib/db/mongo";
import { connectMongoose } from "@/lib/db/mongoose";
import { migration as m001 } from "./001-initial-indexes";
import type { Migration } from "./types";
/** Sıralı migration listesi. Yeni migration: dosyayı ekle ve buraya sırayla ekle. */
export const MIGRATIONS: Migration[] = [m001];
const COLLECTION = "migrations";
/**
* Uygulanmamış migration'ları sırayla çalıştırır; her biri bir kez (`migrations` koleksiyonu).
* Aynı anda iki çalıştırma olursa benzersiz `name` index'i ikinci kaydı reddeder.
*/
export async function runMigrations(log: (line: string) => void = console.log): Promise<string[]> {
const db = getDb();
await connectMongoose();
const col = db.collection<{ name: string; appliedAt: Date; durationMs: number }>(COLLECTION);
await col.createIndex({ name: 1 }, { unique: true });
const applied = new Set(
(await col.find({}, { projection: { name: 1 } }).toArray()).map((d) => d.name),
);
const ran: string[] = [];
for (const m of MIGRATIONS) {
if (applied.has(m.name)) continue;
const started = Date.now();
await m.up(db);
await col.insertOne({ name: m.name, appliedAt: new Date(), durationMs: Date.now() - started });
ran.push(m.name);
log(`✓ Migration: ${m.name} — ${m.description}`);
}
if (!ran.length) log("• Bekleyen migration yok.");
return ran;
}
+8
View File
@@ -0,0 +1,8 @@
import type { Db } from "mongodb";
/** Bir migration: adı dosya adıyla aynı olmalı (ör. "001-initial-indexes"). Geri alma yok; ileri yönlü. */
export type Migration = {
name: string;
description: string;
up: (db: Db) => Promise<void>;
};
+4 -4
View File
@@ -6,6 +6,7 @@ import { env } from "@/lib/env";
import { HouseholdModel } from "@/models/household";
import { HouseholdMemberModel } from "@/models/household-member";
import { storedEmail } from "@/shared/account-email";
import { runMigrations } from "../migrations";
import { ADMIN_USERNAME, SEED_HOUSEHOLDS, SEED_USERS, type SeedUser } from "./fixtures";
/**
@@ -30,7 +31,7 @@ export type SeedSummary = {
};
const AUTH_COLLECTIONS = ["user", "account", "session", "verification", "rateLimit"];
const DOMAIN_COLLECTIONS = ["households", "householdMembers"];
const DOMAIN_COLLECTIONS = ["households", "householdMembers", "auditLogs"];
type AuthApi = ReturnType<typeof getAuth>["api"];
@@ -83,9 +84,8 @@ export async function runSeed({
log("• Kimlik doğrulama ve hane koleksiyonları temizlendi.");
}
// Index'ler (production'da Mongoose autoIndex kapalı olduğu için açıkça oluşturulur).
await HouseholdModel.createIndexes();
await HouseholdMemberModel.createIndexes();
// Index'ler ve şema değişiklikleri (production'da Mongoose autoIndex kapalı).
await runMigrations(log);
// 1) Admin
const adminEmail = e.SEED_ADMIN_EMAIL.toLowerCase();