Skip to content

Commit 4619f42

Browse files
committed
feat(seeders): implement seeder framework with registry and runner for database seeding
1 parent 51bf954 commit 4619f42

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { Seeder } from './types';
2+
import { UserSeeder } from '../modules/user/seeders/UserSeeder';
3+
4+
export const seeders: Seeder[] = [UserSeeder];
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import mongoose from 'mongoose';
2+
import { connectDatabase, disconnectDatabase } from '../lib/database';
3+
import config from '../config/env';
4+
import logger from '@/plugins/observability/logger';
5+
import type { Seeder, SeederContext } from './types';
6+
7+
type RunOptions = {
8+
group?: string;
9+
only?: string[];
10+
modules?: string[]; // reserved for future module filtering
11+
fresh?: boolean;
12+
force?: boolean;
13+
dryRun?: boolean;
14+
seed?: number;
15+
transaction?: boolean; // global override
16+
};
17+
18+
export const runSeeders = async (
19+
seeders: Seeder[],
20+
options: RunOptions = {},
21+
): Promise<void> => {
22+
const group = options.group ?? process.env.SEED_DEFAULT_GROUP ?? 'dev';
23+
const seed = options.seed ?? (Number(process.env.SEED_SEED) || 1);
24+
const dryRun = Boolean(options.dryRun ?? false);
25+
const useTransactions = options.transaction ?? true;
26+
const fresh = Boolean(options.fresh ?? false);
27+
const force = Boolean(options.force ?? false);
28+
29+
if (process.env.NODE_ENV === 'production' && !force) {
30+
throw new Error(
31+
'Seeding in production is blocked. Use --force or set ALLOW_SEED_IN_PROD=true.',
32+
);
33+
}
34+
35+
// Filter by group and explicit selection
36+
let list = seeders.filter((s) => !s.groups || s.groups.includes(group));
37+
if (options.only && options.only.length) {
38+
const onlySet = new Set(options.only.map((n) => n.toLowerCase()));
39+
list = list.filter((s) => onlySet.has(s.name.toLowerCase()));
40+
}
41+
42+
// Topological sort according to dependsOn
43+
const byName = new Map(list.map((s) => [s.name, s] as const));
44+
const inDegree = new Map<string, number>();
45+
const edges = new Map<string, string[]>();
46+
for (const s of list) {
47+
inDegree.set(s.name, 0);
48+
edges.set(s.name, []);
49+
}
50+
for (const s of list) {
51+
for (const dep of s.dependsOn ?? []) {
52+
if (!byName.has(dep)) {
53+
throw new Error(
54+
`Seeder ${s.name} depends on missing seeder ${dep} in group ${group}`,
55+
);
56+
}
57+
edges.get(dep)!.push(s.name);
58+
inDegree.set(s.name, (inDegree.get(s.name) ?? 0) + 1);
59+
}
60+
}
61+
const queue: string[] = [];
62+
for (const [name, deg] of inDegree) if (deg === 0) queue.push(name);
63+
const ordered: Seeder[] = [];
64+
while (queue.length) {
65+
const n = queue.shift()!;
66+
ordered.push(byName.get(n)!);
67+
for (const m of edges.get(n) ?? []) {
68+
const d = (inDegree.get(m) ?? 0) - 1;
69+
inDegree.set(m, d);
70+
if (d === 0) queue.push(m);
71+
}
72+
}
73+
if (ordered.length !== list.length) {
74+
throw new Error('Circular dependency detected among seeders');
75+
}
76+
77+
// Connect DB
78+
await connectDatabase();
79+
80+
try {
81+
const db = mongoose.connection;
82+
83+
const refs = new Map<string, unknown>();
84+
const ctx: SeederContext = {
85+
db,
86+
config,
87+
logger,
88+
refs: {
89+
set: (k, v) => refs.set(k, v),
90+
get: <T = unknown>(k: string) => refs.get(k) as T,
91+
has: (k) => refs.has(k),
92+
keys: () => Array.from(refs.keys()),
93+
},
94+
env: { group, dryRun, seed, now: new Date() },
95+
};
96+
97+
// Fresh: drop involved collections
98+
if (fresh) {
99+
const toDrop = new Set<string>();
100+
for (const s of ordered)
101+
for (const c of s.collections ?? []) toDrop.add(c);
102+
if (toDrop.size) {
103+
logger.warn(
104+
`Fresh mode: dropping collections: ${Array.from(toDrop).join(', ')}`,
105+
);
106+
for (const coll of toDrop) {
107+
try {
108+
const exists =
109+
(await db.db!.listCollections({ name: coll }).toArray()).length >
110+
0;
111+
if (exists && !dryRun) await db.dropCollection(coll);
112+
} catch (e) {
113+
logger.warn(
114+
`Failed to drop collection ${coll}: ${(e as Error).message}`,
115+
);
116+
}
117+
}
118+
}
119+
}
120+
121+
// Execute seeders
122+
for (const seeder of ordered) {
123+
const shouldTx = seeder.transaction ?? true;
124+
logger.info(`→ Running ${seeder.name} (group=${group})`);
125+
126+
if (dryRun) {
127+
logger.info(`[dry-run] Skipping execution of ${seeder.name}`);
128+
continue;
129+
}
130+
131+
if (useTransactions && shouldTx) {
132+
const session = await db.startSession();
133+
try {
134+
await session.withTransaction(async () => {
135+
await seeder.run(ctx);
136+
});
137+
} finally {
138+
await session.endSession();
139+
}
140+
} else {
141+
await seeder.run(ctx);
142+
}
143+
logger.info(`✓ Completed ${seeder.name}`);
144+
}
145+
} finally {
146+
await disconnectDatabase();
147+
}
148+
};

0 commit comments

Comments
 (0)