forked from ritik4ever/stellar-goal-vault
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdb.ts
More file actions
188 lines (159 loc) · 5.73 KB
/
Copy pathdb.ts
File metadata and controls
188 lines (159 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import Database from 'better-sqlite3';
import path from 'path';
type SQLiteDatabase = ReturnType<typeof Database>;
let db: SQLiteDatabase | null = null;
function resolveDbPath(): string {
return process.env.DB_PATH || path.join(__dirname, '..', '..', 'data', 'campaigns.db');
}
export type DbHealthStatus = 'up' | 'down';
export function getDb(): SQLiteDatabase {
if (!db) {
throw new Error('Database not initialized. Call initDb() first.');
}
return db;
}
export function initDb(): void {
if (db) {
return;
}
const fs = require('fs') as typeof import('fs');
const dbPath = resolveDbPath();
const dir = path.dirname(dbPath);
if (dbPath !== ':memory:' && !fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
db = new Database(dbPath);
// Enable Write-Ahead Logging (WAL) mode.
// This is the chosen journal mode to prevent unnecessary lock contention,
// allowing reads and writes to occur concurrently without blocking each other.
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('foreign_keys = ON');
migrate(db);
}
export function resetDbForTests(): void {
if (db) {
db.close();
db = null;
}
}
export function checkDbHealth(): {
status: DbHealthStatus;
reachable: boolean;
error?: string;
} {
try {
const database = getDb();
database.prepare('SELECT 1 AS ok').get();
return {
status: 'up',
reachable: true,
};
} catch (error) {
return {
status: 'down',
reachable: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function migrate(database: SQLiteDatabase): void {
database.exec(`
CREATE TABLE IF NOT EXISTS campaigns (
id TEXT PRIMARY KEY,
creator TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
accepted_tokens_json TEXT NOT NULL,
target_amount REAL NOT NULL,
pledged_amount REAL NOT NULL DEFAULT 0,
deadline INTEGER NOT NULL,
created_at INTEGER NOT NULL,
claimed_at INTEGER,
failed_at INTEGER,
metadata_json TEXT,
max_per_contributor INTEGER
);
CREATE TABLE IF NOT EXISTS pledges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id TEXT NOT NULL,
contributor TEXT NOT NULL,
amount REAL NOT NULL,
asset_code TEXT NOT NULL,
created_at INTEGER NOT NULL,
refunded_at INTEGER,
transaction_hash TEXT,
FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
);
CREATE TABLE IF NOT EXISTS campaign_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id TEXT NOT NULL,
event_type TEXT NOT NULL,
timestamp INTEGER NOT NULL,
actor TEXT,
amount REAL,
metadata TEXT,
blockchain_metadata TEXT,
FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
);
CREATE INDEX IF NOT EXISTS idx_pledges_campaign_id ON pledges(campaign_id);
CREATE INDEX IF NOT EXISTS idx_campaign_events_campaign_id ON campaign_events(campaign_id);
CREATE INDEX IF NOT EXISTS idx_campaign_events_timestamp ON campaign_events(timestamp);
`);
const pledgeColumns = database.prepare(`PRAGMA table_info(pledges)`).all() as Array<{
name: string;
}>;
const hasTransactionHash = pledgeColumns.some((column) => column.name === 'transaction_hash');
if (!hasTransactionHash) {
database.exec(`ALTER TABLE pledges ADD COLUMN transaction_hash TEXT`);
}
const hasAssetCode = pledgeColumns.some((column) => column.name === 'asset_code');
if (!hasAssetCode) {
database.exec(`ALTER TABLE pledges ADD COLUMN asset_code TEXT NOT NULL DEFAULT 'XLM'`);
}
// Add deleted_at column if not exists
const campaignColumns = database.prepare(`PRAGMA table_info(campaigns)`).all() as Array<{
name: string;
}>;
if (!campaignColumns.some((column) => column.name === 'deleted_at')) {
database.exec(`ALTER TABLE campaigns ADD COLUMN deleted_at INTEGER`);
}
// Add failed_at column if not exists
if (!campaignColumns.some((column) => column.name === 'failed_at')) {
database.exec(`ALTER TABLE campaigns ADD COLUMN failed_at INTEGER`);
}
// Migrate asset_code to accepted_tokens_json if needed
if (
campaignColumns.some((column) => column.name === 'asset_code') &&
!campaignColumns.some((column) => column.name === 'accepted_tokens_json')
) {
database.exec(
`ALTER TABLE campaigns ADD COLUMN accepted_tokens_json TEXT NOT NULL DEFAULT '[]'`,
);
// Migrate existing asset_code to accepted_tokens_json
database.exec(`UPDATE campaigns SET accepted_tokens_json = json_array(asset_code)`);
// Optionally drop asset_code column (SQLite doesn't support DROP COLUMN directly)
}
database.exec(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_pledges_transaction_hash
ON pledges(transaction_hash)
WHERE transaction_hash IS NOT NULL
`);
try {
database.exec(`ALTER TABLE campaign_events ADD COLUMN blockchain_metadata TEXT;`);
} catch {
// Column already exists, ignore error.
}
const hasMaxPerContributor = campaignColumns.some(
(column) => column.name === 'max_per_contributor',
);
if (!hasMaxPerContributor) {
database.exec(`ALTER TABLE campaigns ADD COLUMN max_per_contributor INTEGER`);
}
database.exec(`
CREATE INDEX IF NOT EXISTS idx_campaign_events_tx_hash
ON campaign_events(json_extract(blockchain_metadata, '$.txHash'));
CREATE INDEX IF NOT EXISTS idx_campaign_events_ledger
ON campaign_events(json_extract(blockchain_metadata, '$.ledgerNumber'));
`);
}