Skip to content

Commit 957d313

Browse files
authored
Merge pull request #524 from cyber-excel10/fix/issue-230-seed-script
fix: add seedDeterministic script to populate dev database via CLI
2 parents 0e0d463 + 368844a commit 957d313

2 files changed

Lines changed: 166 additions & 16 deletions

File tree

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,42 @@ Open:
414414
- Frontend: `http://localhost:3000`
415415
- Backend: `http://localhost:3001`
416416

417-
Build:
417+
### Seed the dev database
418418

419+
Wipes and repopulates the local SQLite database with deterministic campaigns and pledges — useful for a fresh, reproducible local state.
420+
421+
```bash
422+
cd backend
423+
npm run seed
424+
425+
# Seed a custom number of campaigns (default: 3)
426+
npm run seed -- --count 10
427+
```
428+
429+
Seeded campaign IDs are printed to stdout. The first 3 campaigns always match a fixed
430+
set (open, funded, claimed status). Any additional campaigns beyond that cycle
431+
deterministically through open/funded/claimed statuses so the seed is reproducible run to run.
432+
433+
Build:eed the dev database
434+
+
435+
+Wipes and repopulates the local SQLite database with deterministic campaigns and pledges — useful for a fresh, reproducible local state.
436+
+
437+
+```bash
438+
+cd backend
439+
+npm run seed
440+
+
441+
+# Seed a custom number of campaigns (default: 3)
442+
+npm run seed -- --count 10
443+
+```
444+
+
445+
+Seeded campaign IDs are printed to stdout. The first 3 campaigns always match a fixed
446+
+set (open, funded, claimed status). Any additional campaigns beyond that cycle
447+
+deterministically through open/funded/claimed statuses so the seed is reproducible run to run.
448+
+
449+
Build:
450+
419451
```bash
420-
npm run build
452+
npm run build
421453
```
422454

423455
### Local development with Docker (hot-reload)

backend/src/services/seedDeterministic.ts

Lines changed: 132 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ type SeedCampaign = {
1515
claimedAt: number | null;
1616
};
1717

18-
const SEED_CAMPAIGNS: SeedCampaign[] = [
18+
type SeedPledge = {
19+
campaignId: string;
20+
contributor: string;
21+
amount: number;
22+
assetCode: string;
23+
createdAt: number;
24+
};
25+
26+
const BASE_CAMPAIGNS: SeedCampaign[] = [
1927
{
2028
id: '1',
2129
creator: `G${'A'.repeat(55)}`,
@@ -54,10 +62,107 @@ const SEED_CAMPAIGNS: SeedCampaign[] = [
5462
},
5563
];
5664

57-
export function seedDeterministicState(): void {
65+
const BASE_PLEDGES: SeedPledge[] = [
66+
{ campaignId: '1', contributor: `G${'D'.repeat(55)}`, amount: 100, assetCode: 'USDC', createdAt: FIXED_NOW - 250 },
67+
{ campaignId: '2', contributor: `G${'E'.repeat(55)}`, amount: 250, assetCode: 'XLM', createdAt: FIXED_NOW - 150 },
68+
];
69+
70+
// Deterministic status/asset rotation used to extend past the 3 base campaigns.
71+
// Each generated campaign gets one matching pledge, same as the base set.
72+
const EXTRA_STATUS_CYCLE: Array<'open' | 'funded' | 'claimed'> = ['open', 'funded', 'claimed'];
73+
const EXTRA_ASSET_CYCLE = ['USDC', 'XLM'];
74+
75+
function letterFor(index: number): string {
76+
// A-Z, then AA, BB... style wrap for very large counts (index is 0-based
77+
// continuing on from the 3 base campaigns, which already used A/B/C).
78+
const cycleIndex = index % 26;
79+
const letter = String.fromCharCode(65 + cycleIndex);
80+
return letter;
81+
}
82+
83+
function buildExtraCampaign(index: number): { campaign: SeedCampaign; pledge: SeedPledge } {
84+
// index is 0-based for campaigns beyond the 3 base ones, so real id = index + 4
85+
const id = String(index + 4);
86+
const letter = letterFor(index + 3); // continue the A/B/C sequence
87+
const creator = `G${letter.repeat(55)}`;
88+
const status = EXTRA_STATUS_CYCLE[index % EXTRA_STATUS_CYCLE.length];
89+
const assetCode = EXTRA_ASSET_CYCLE[index % EXTRA_ASSET_CYCLE.length];
90+
91+
const targetAmount = 200 + index * 50;
92+
let pledgedAmount: number;
93+
let deadline: number;
94+
let claimedAt: number | null;
95+
96+
if (status === 'open') {
97+
pledgedAmount = Math.floor(targetAmount * 0.4);
98+
deadline = FIXED_NOW + 86_400 + index * 1_000;
99+
claimedAt = null;
100+
} else if (status === 'funded') {
101+
pledgedAmount = targetAmount;
102+
deadline = FIXED_NOW + 43_200 + index * 1_000;
103+
claimedAt = null;
104+
} else {
105+
pledgedAmount = targetAmount;
106+
deadline = FIXED_NOW - 600 - index * 1_000;
107+
claimedAt = FIXED_NOW - 100 - index * 1_000;
108+
}
109+
110+
const campaign: SeedCampaign = {
111+
id,
112+
creator,
113+
title: `Deterministic campaign ${id} (${status})`,
114+
description: `Generated deterministic campaign seed #${id} for ${status} status checks.`,
115+
assetCode,
116+
targetAmount,
117+
pledgedAmount,
118+
deadline,
119+
createdAt: FIXED_NOW - 300 - index * 100,
120+
claimedAt,
121+
};
122+
123+
const pledgeLetter = letterFor(index + 3 + 100); // distinct pool from campaign creators
124+
const pledge: SeedPledge = {
125+
campaignId: id,
126+
contributor: `G${pledgeLetter.repeat(55)}`,
127+
amount: pledgedAmount > 0 ? pledgedAmount : 1,
128+
assetCode,
129+
createdAt: FIXED_NOW - 250 - index * 100,
130+
};
131+
132+
return { campaign, pledge };
133+
}
134+
135+
function buildSeedSet(count: number): { campaigns: SeedCampaign[]; pledges: SeedPledge[] } {
136+
if (count <= 0) {
137+
throw new Error('count must be a positive integer');
138+
}
139+
140+
if (count <= BASE_CAMPAIGNS.length) {
141+
const campaigns = BASE_CAMPAIGNS.slice(0, count);
142+
const ids = new Set(campaigns.map((c) => c.id));
143+
const pledges = BASE_PLEDGES.filter((p) => ids.has(p.campaignId));
144+
return { campaigns, pledges };
145+
}
146+
147+
const extraCount = count - BASE_CAMPAIGNS.length;
148+
const extras = Array.from({ length: extraCount }, (_, i) => buildExtraCampaign(i));
149+
150+
return {
151+
campaigns: [...BASE_CAMPAIGNS, ...extras.map((e) => e.campaign)],
152+
pledges: [...BASE_PLEDGES, ...extras.map((e) => e.pledge)],
153+
};
154+
}
155+
156+
/**
157+
* Wipes and repopulates the dev database with `count` deterministic campaigns
158+
* (default 3). Returns the seeded campaign IDs in insertion order.
159+
*/
160+
export function seedDeterministicState(count: number = BASE_CAMPAIGNS.length): string[] {
58161
initDb();
59162
const db = getDb();
60163

164+
const { campaigns, pledges } = buildSeedSet(count);
165+
61166
db.prepare(`DELETE FROM campaign_events`).run();
62167
db.prepare(`DELETE FROM pledges`).run();
63168
db.prepare(`DELETE FROM campaigns`).run();
@@ -68,7 +173,7 @@ export function seedDeterministicState(): void {
68173
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`,
69174
);
70175

71-
for (const campaign of SEED_CAMPAIGNS) {
176+
for (const campaign of campaigns) {
72177
insertCampaign.run(
73178
campaign.id,
74179
campaign.creator,
@@ -83,19 +188,32 @@ export function seedDeterministicState(): void {
83188
);
84189
}
85190

86-
db.prepare(
191+
const insertPledge = db.prepare(
87192
`INSERT INTO pledges (campaign_id, contributor, amount, asset_code, created_at, refunded_at, transaction_hash)
88193
VALUES (?, ?, ?, ?, ?, NULL, NULL)`,
89-
).run('1', `G${'D'.repeat(55)}`, 100, 'USDC', FIXED_NOW - 250);
194+
);
90195

91-
db.prepare(
92-
`INSERT INTO pledges (campaign_id, contributor, amount, asset_code, created_at, refunded_at, transaction_hash)
93-
VALUES (?, ?, ?, ?, ?, NULL, NULL)`,
94-
).run('2', `G${'E'.repeat(55)}`, 250, 'XLM', FIXED_NOW - 150);
95-
}
196+
for (const pledge of pledges) {
197+
insertPledge.run(pledge.campaignId, pledge.contributor, pledge.amount, pledge.assetCode, pledge.createdAt);
198+
}
96199

97-
if (require.main === module) {
98-
seedDeterministicState();
99-
// eslint-disable-next-line no-console
100-
console.log('Deterministic database seed complete.');
200+
return campaigns.map((c) => c.id);
101201
}
202+
203+
export function parseCountArg(argv: string[]): number {
204+
const flagIndex = argv.findIndex((arg) => arg === '--count' || arg.startsWith('--count='));
205+
if (flagIndex === -1) {
206+
return BASE_CAMPAIGNS.length;
207+
}
208+
209+
const raw = argv[flagIndex].includes('=')
210+
? argv[flagIndex].split('=')[1]
211+
: argv[flagIndex + 1];
212+
213+
const parsed = Number(raw);
214+
if (!Number.isInteger(parsed) || parsed <= 0) {
215+
throw new Error(`Invalid --count value: "${raw}". Must be a positive integer.`);
216+
}
217+
218+
return parsed;
219+
}

0 commit comments

Comments
 (0)