feat(db): migration mekanizması ve ilk index migration'ı
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "butcem",
|
"name": "butcem",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "BÜTÇEM — aile bütçe, cari, birikim, ev stoğu ve akıllı alışveriş uygulaması",
|
"description": "BÜTÇEM — aile bütçe, cari, birikim, ev stoğu ve akıllı alışveriş uygulaması",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
"seed": "tsx --conditions=react-server --env-file=.env.local scripts/seed.ts",
|
"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",
|
"seed:reset": "tsx --conditions=react-server --env-file=.env.local scripts/seed.ts --reset",
|
||||||
"icons": "tsx scripts/generate-icons.ts",
|
"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": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^7.0.2",
|
"@hookform/resolvers": "^7.0.2",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -6,6 +6,7 @@ import { env } from "@/lib/env";
|
|||||||
import { HouseholdModel } from "@/models/household";
|
import { HouseholdModel } from "@/models/household";
|
||||||
import { HouseholdMemberModel } from "@/models/household-member";
|
import { HouseholdMemberModel } from "@/models/household-member";
|
||||||
import { storedEmail } from "@/shared/account-email";
|
import { storedEmail } from "@/shared/account-email";
|
||||||
|
import { runMigrations } from "../migrations";
|
||||||
import { ADMIN_USERNAME, SEED_HOUSEHOLDS, SEED_USERS, type SeedUser } from "./fixtures";
|
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 AUTH_COLLECTIONS = ["user", "account", "session", "verification", "rateLimit"];
|
||||||
const DOMAIN_COLLECTIONS = ["households", "householdMembers"];
|
const DOMAIN_COLLECTIONS = ["households", "householdMembers", "auditLogs"];
|
||||||
|
|
||||||
type AuthApi = ReturnType<typeof getAuth>["api"];
|
type AuthApi = ReturnType<typeof getAuth>["api"];
|
||||||
|
|
||||||
@@ -83,9 +84,8 @@ export async function runSeed({
|
|||||||
log("• Kimlik doğrulama ve hane koleksiyonları temizlendi.");
|
log("• Kimlik doğrulama ve hane koleksiyonları temizlendi.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index'ler (production'da Mongoose autoIndex kapalı olduğu için açıkça oluşturulur).
|
// Index'ler ve şema değişiklikleri (production'da Mongoose autoIndex kapalı).
|
||||||
await HouseholdModel.createIndexes();
|
await runMigrations(log);
|
||||||
await HouseholdMemberModel.createIndexes();
|
|
||||||
|
|
||||||
// 1) Admin
|
// 1) Admin
|
||||||
const adminEmail = e.SEED_ADMIN_EMAIL.toLowerCase();
|
const adminEmail = e.SEED_ADMIN_EMAIL.toLowerCase();
|
||||||
|
|||||||
Reference in New Issue
Block a user