Files
butcem/scripts/seed/run.ts
T

359 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import mongoose, { Types } from "mongoose";
import { getAuth } from "@/lib/auth";
import { getDb } from "@/lib/db/mongo";
import { connectMongoose } from "@/lib/db/mongoose";
import { env } from "@/lib/env";
import { getStorage } from "@/lib/storage";
import { installDefaultCategories } from "@/features/categories/service";
import { AccountModel } from "@/models/account";
import { HouseholdModel } from "@/models/household";
import { HouseholdMemberModel } from "@/models/household-member";
import { storedEmail } from "@/shared/account-email";
import { runMigrations } from "../migrations";
import { seedContacts } from "./contacts";
import { seedAssets } from "./assets";
import { seedBudgets } from "./budgets";
import { seedShopping } from "./shopping";
import { seedStock } from "./stock";
import { seedInKind } from "./in-kind";
import { seedInstallments } from "./installments";
import { seedRecurring } from "./recurring";
import { seedTransactions } from "./transactions";
import {
ADMIN_USERNAME,
SEED_ACCOUNTS,
SEED_HOUSEHOLDS,
SEED_USERS,
type SeedUser,
} from "./fixtures";
/**
* İdempotent seed. Kullanıcılar Better Auth API'siyle oluşturulur (şifreler doğru hashlenir);
* tekrar çalıştırınca kopya oluşturmaz (kullanıcı adı / hane adı / üyelik çiftine göre).
* Production'da yalnızca admin oluşturulur; örnek kullanıcı ve hane oluşturulmaz.
*/
export type SeedOptions = { reset?: boolean; log?: (line: string) => void };
export type SeedAccountRow = {
rol: string;
kullaniciAdi: string;
eposta: string;
durum: string;
haneler: string;
};
export type SeedSummary = {
created: {
users: number;
households: number;
memberships: number;
categories: number;
accounts: number;
transactions: number;
recurringRules: number;
installmentPlans: number;
contacts: number;
inKind: number;
assets: number;
budgets: number;
stock: number;
stores: number;
products: number;
};
accounts: SeedAccountRow[];
};
const AUTH_COLLECTIONS = ["user", "account", "session", "verification", "rateLimit"];
const DOMAIN_COLLECTIONS = [
"households",
"householdMembers",
"auditLogs",
"categories",
"accounts",
"transactions",
"payees",
"attachments",
"reconciliations",
"recurringRules",
"statements",
"contacts",
"assetPrices",
"inKindDebts",
"assets",
"assetMovements",
"budgets",
"stockCategories",
"productTypes",
"stockMovements",
"stockCounts",
"stores",
"products",
];
type AuthApi = ReturnType<typeof getAuth>["api"];
async function findUserIdByUsername(username: string): Promise<string | null> {
const doc = await getDb()
.collection("user")
.findOne({ username }, { projection: { _id: 1 } });
return doc ? String(doc._id) : null;
}
/** Admin olarak giriş yapıp oturum başlıklarını döner (ban gibi oturum isteyen admin API'leri için). */
async function adminHeaders(api: AuthApi, password: string): Promise<Headers> {
const { headers } = await api.signInUsername({
body: { username: ADMIN_USERNAME, password },
returnHeaders: true,
});
const cookie = headers
.getSetCookie()
.map((c) => c.split(";")[0])
.join("; ");
return new Headers({ cookie });
}
export async function runSeed({
reset = false,
log = console.log,
}: SeedOptions = {}): Promise<SeedSummary> {
const e = env();
const isProd = e.NODE_ENV === "production";
if (reset && isProd) throw new Error("seed:reset production ortamında çalıştırılamaz.");
if (!e.SEED_ADMIN_EMAIL || !e.SEED_ADMIN_PASSWORD) {
throw new Error("SEED_ADMIN_EMAIL ve SEED_ADMIN_PASSWORD tanımlı olmalı.");
}
if (!isProd && !e.SEED_USER_PASSWORD) {
throw new Error("SEED_USER_PASSWORD tanımlı olmalı (örnek kullanıcıların ortak şifresi).");
}
const api = getAuth().api;
const db = getDb();
await connectMongoose();
const summary: SeedSummary = {
created: {
users: 0,
households: 0,
memberships: 0,
categories: 0,
accounts: 0,
transactions: 0,
recurringRules: 0,
installmentPlans: 0,
contacts: 0,
inKind: 0,
assets: 0,
budgets: 0,
stock: 0,
stores: 0,
products: 0,
},
accounts: [],
};
if (reset) {
// Ek dosyaların diskteki kopyaları da silinsin (yetim dosya kalmasın).
const storage = getStorage();
for await (const a of db
.collection("attachments")
.find({}, { projection: { storageKey: 1, thumbKey: 1 } })) {
await storage.delete(String(a.storageKey));
if (a.thumbKey) await storage.delete(String(a.thumbKey));
}
for (const name of [...AUTH_COLLECTIONS, ...DOMAIN_COLLECTIONS]) {
await db.collection(name).deleteMany({});
}
log("• Kimlik doğrulama ve hane koleksiyonları temizlendi.");
}
// Index'ler ve şema değişiklikleri (production'da Mongoose autoIndex kapalı).
await runMigrations(log);
// 1) Admin
const adminEmail = e.SEED_ADMIN_EMAIL.toLowerCase();
let adminId = await findUserIdByUsername(ADMIN_USERNAME);
if (!adminId) {
const res = await api.createUser({
body: {
email: adminEmail,
password: e.SEED_ADMIN_PASSWORD,
name: "Yönetici",
role: "admin",
data: { username: ADMIN_USERNAME },
},
});
adminId = res.user.id;
summary.created.users++;
log(`✓ Admin oluşturuldu: ${adminEmail} (kullanıcı adı: ${ADMIN_USERNAME})`);
} else {
log(`• Admin zaten var: ${ADMIN_USERNAME}`);
}
if (isProd) {
log("• Production: örnek kullanıcı ve hane oluşturulmadı.");
return summary;
}
// 2) Örnek kullanıcılar
const ids = new Map<string, string>([[ADMIN_USERNAME, adminId]]);
const toBan: SeedUser[] = [];
for (const u of SEED_USERS) {
let id = await findUserIdByUsername(u.username);
if (!id) {
const res = await api.createUser({
body: {
email: storedEmail(u.username, u.email),
password: e.SEED_USER_PASSWORD!,
name: u.name,
role: u.role,
data: { username: u.username },
},
});
id = res.user.id;
summary.created.users++;
log(`✓ Kullanıcı: ${u.name} (@${u.username})`);
}
ids.set(u.username, id);
if (u.banReason) toBan.push(u);
}
// 3) Askıdaki kullanıcı(lar) — admin API'si oturum ister.
const pendingBans: { user: SeedUser; id: string }[] = [];
for (const u of toBan) {
const id = ids.get(u.username)!;
const doc = await db
.collection("user")
.findOne({ _id: new Types.ObjectId(id) }, { projection: { banned: 1 } });
if (!doc?.banned) pendingBans.push({ user: u, id });
}
if (pendingBans.length) {
const headers = await adminHeaders(api, e.SEED_ADMIN_PASSWORD);
for (const { user, id } of pendingBans) {
await api.banUser({ headers, body: { userId: id, banReason: user.banReason } });
log(`✓ Askıya alındı: @${user.username}`);
}
}
// 4) Haneler ve üyelikler
const memberships = new Map<string, string[]>();
const actor = new Types.ObjectId(adminId);
for (const h of SEED_HOUSEHOLDS) {
let household = await HouseholdModel.findOne({ name: h.name });
if (!household) {
household = await HouseholdModel.create({ name: h.name, createdBy: actor, updatedBy: actor });
summary.created.households++;
log(`✓ Hane: ${h.name}`);
}
for (const m of h.members) {
const userId = new Types.ObjectId(ids.get(m.username)!);
const result = await HouseholdMemberModel.updateOne(
{ householdId: household._id, userId },
{
$setOnInsert: { householdId: household._id, userId, createdBy: actor },
$set: { role: m.role, updatedBy: actor },
},
{ upsert: true },
);
if (result.upsertedCount) summary.created.memberships++;
memberships.set(m.username, [
...(memberships.get(m.username) ?? []),
`${h.name} (${m.role})`,
]);
}
}
// 5) Varsayılan kategoriler ve örnek hesaplar
const householdIds = new Map<string, Types.ObjectId>();
for (const h of SEED_HOUSEHOLDS) {
const doc = await HouseholdModel.findOne({ name: h.name }).select({ _id: 1 }).lean();
householdIds.set(h.name, doc!._id);
summary.created.categories += await installDefaultCategories(doc!._id, actor);
}
const openingDate = new Date(Date.now() - 183 * 24 * 60 * 60 * 1000);
for (const [order, a] of SEED_ACCOUNTS.entries()) {
const householdId = householdIds.get(a.household)!;
if (await AccountModel.exists({ householdId, name: a.name })) continue;
const payer = a.creditCard
? await AccountModel.findOne({ householdId, name: a.creditCard.paymentAccount })
.select({ _id: 1 })
.lean()
: null;
await AccountModel.create({
householdId,
name: a.name,
type: a.type,
institution: a.institution ?? "",
mealCardProvider: a.mealCardProvider ?? null,
openingBalanceMinor: a.openingBalanceMinor,
openingDate,
color: a.color,
icon: a.icon,
order,
ownerUserId: a.owner ? new Types.ObjectId(ids.get(a.owner)!) : null,
isPrivate: a.isPrivate ?? false,
overdraft: a.overdraft ? { enabled: true, ...a.overdraft } : null,
creditCard: a.creditCard
? {
limitMinor: a.creditCard.limitMinor,
statementDay: a.creditCard.statementDay,
dueRule: { kind: "daysAfter", value: a.creditCard.dueDays },
paymentAccountId: payer?._id ?? null,
}
: null,
createdBy: actor,
updatedBy: actor,
});
summary.created.accounts++;
}
if (summary.created.categories) log(`✓ Varsayılan kategoriler: ${summary.created.categories}`);
if (summary.created.accounts) log(`✓ Örnek hesaplar: ${summary.created.accounts}`);
// 6) Son 6 ayın örnek işlemleri (faker.seed(42), idempotent)
const userIdMap = new Map([...ids.entries()]);
summary.created.transactions = await seedTransactions(householdIds, userIdMap);
if (summary.created.transactions) log(`✓ Örnek işlemler: ${summary.created.transactions}`);
summary.created.recurringRules = await seedRecurring(householdIds, userIdMap);
if (summary.created.recurringRules)
log(`✓ Örnek düzenli işlemler: ${summary.created.recurringRules}`);
summary.created.installmentPlans = await seedInstallments(
householdIds.get("Çakır Ailesi")!,
userIdMap,
);
if (summary.created.installmentPlans)
log(`✓ Örnek taksitli alışverişler: ${summary.created.installmentPlans}`);
summary.created.contacts = await seedContacts(householdIds, userIdMap);
if (summary.created.contacts) log(`✓ Örnek cariler: ${summary.created.contacts}`);
summary.created.inKind = await seedInKind(householdIds, userIdMap);
if (summary.created.inKind) log(`✓ Örnek aynî kayıtlar ve fiyatlar: ${summary.created.inKind}`);
summary.created.assets = await seedAssets(householdIds, userIdMap);
if (summary.created.assets) log(`✓ Örnek varlıklar: ${summary.created.assets}`);
summary.created.budgets = await seedBudgets(householdIds, userIdMap);
if (summary.created.budgets) log(`✓ Örnek bütçeler: ${summary.created.budgets}`);
summary.created.stock = await seedStock(householdIds, userIdMap);
if (summary.created.stock) log(`✓ Örnek stok ürün tipleri: ${summary.created.stock}`);
const shopping = await seedShopping(householdIds, userIdMap);
summary.created.stores = shopping.stores;
summary.created.products = shopping.products;
if (shopping.stores || shopping.products)
log(`✓ Örnek mağazalar ve ürünler: ${shopping.stores} / ${shopping.products}`);
// 7) Geliştirme hesapları tablosu
const all = [
{ username: ADMIN_USERNAME, name: "Yönetici", email: adminEmail, role: "admin" as const },
...SEED_USERS,
];
summary.accounts = all.map((u) => ({
rol: u.role,
kullaniciAdi: u.username,
eposta: u.email ?? "—",
durum: "banReason" in u && u.banReason ? "askıda" : "aktif",
haneler: (memberships.get(u.username) ?? ["—"]).join(", "),
}));
return summary;
}
/** Seed sonrası bağlantıları kapatır (CLI ve testler). */
export async function closeSeedConnections() {
await mongoose.disconnect();
}