thatcher 1.0.7 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -16
- package/package.json +12 -18
- package/src/adapters/google-auth.js +4 -43
- package/src/adapters/google-drive.js +12 -67
- package/src/app/api/audit/dashboard/route.js +14 -11
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +6 -3
- package/src/app/api/audit/permissions/route.js +10 -7
- package/src/app/api/audit/permissions/stats/route.js +8 -5
- package/src/app/api/audit/route.js +4 -4
- package/src/app/api/audit/stats/route.js +1 -1
- package/src/app/api/auth/google/callback/route.js +5 -2
- package/src/app/api/auth/google/route.js +4 -1
- package/src/app/api/auth/login/route.js +5 -2
- package/src/app/api/auth/mwr-bridge/route.js +11 -21
- package/src/app/api/auth/password-reset/route.js +15 -14
- package/src/app/api/cron/trigger/route.js +4 -1
- package/src/app/api/debug/[[...path]]/route.js +6 -5
- package/src/app/api/debug/sqlite/route.js +7 -12
- package/src/app/api/debug/sync/route.js +5 -5
- package/src/app/api/domains/route.js +4 -1
- package/src/app/api/email/allocate/batch/route.js +19 -29
- package/src/app/api/email/allocate/route.js +16 -17
- package/src/app/api/email/receive/route.js +31 -42
- package/src/app/api/email/send/route.js +28 -16
- package/src/app/api/email/unallocated/route.js +10 -13
- package/src/app/api/files/[id]/route.js +4 -1
- package/src/app/api/formula/[[...path]]/route.js +0 -4
- package/src/app/api/health/route.js +10 -9
- package/src/app/api/metrics/route.js +1 -1
- package/src/cli.js +4 -4
- package/src/config/config-loader.js +1 -21
- package/src/config/constants.js +0 -35
- package/src/config/env.js +0 -30
- package/src/config/spec-helpers.js +6 -95
- package/src/engine.js +23 -0
- package/src/engine.server.js +8 -78
- package/src/index.js +42 -67
- package/src/lib/alert-manager.js +7 -3
- package/src/lib/api-helpers.js +4 -3
- package/src/lib/api.js +10 -10
- package/src/lib/audit-logger-enhanced.js +77 -83
- package/src/lib/auth-middleware.js +1 -32
- package/src/lib/auth-route-helpers.js +5 -0
- package/src/lib/busybase-adapter.js +0 -5
- package/src/lib/busybase-audit-reads.js +142 -0
- package/src/lib/busybase-audit.js +41 -0
- package/src/lib/busybase-lucia-adapter.js +72 -0
- package/src/lib/busybase-store.js +338 -0
- package/src/lib/config-field-helpers.js +0 -17
- package/src/lib/config-generator-engine.js +19 -36
- package/src/lib/config-helpers.js +0 -21
- package/src/lib/crud-action-helpers.js +2 -2
- package/src/lib/crud-factory.js +1 -16
- package/src/lib/crud-handlers.js +17 -52
- package/src/lib/csrf-protection.js +5 -2
- package/src/lib/date-utils.js +3 -127
- package/src/lib/debug-registry.js +0 -5
- package/src/lib/domain-loader.js +4 -1
- package/src/lib/email-sender.js +25 -23
- package/src/lib/error-handler.js +5 -41
- package/src/lib/error-recovery.js +10 -7
- package/src/lib/error-resilience.js +7 -5
- package/src/lib/events-engine.js +9 -6
- package/src/lib/export-sink.js +0 -5
- package/src/lib/field-iterator.js +7 -76
- package/src/lib/field-registry.js +0 -22
- package/src/lib/generic-crud-handler.js +0 -16
- package/src/lib/hook-engine.js +5 -53
- package/src/lib/hot-reload/checkpoint.js +5 -2
- package/src/lib/hot-reload/promise-container.js +7 -3
- package/src/lib/hot-reload/route-wrapper.js +4 -1
- package/src/lib/hot-reload/supervisor.js +6 -3
- package/src/lib/hyperformula-service.js +0 -5
- package/src/lib/id-helpers.js +10 -0
- package/src/lib/index.js +4 -7
- package/src/lib/log-aggregator.js +0 -1
- package/src/lib/logger.js +0 -15
- package/src/lib/metrics-collector.js +2 -46
- package/src/lib/next-compat.js +15 -1
- package/src/lib/observability-bootstrap.js +0 -5
- package/src/lib/perf-monitor.js +4 -1
- package/src/lib/perf-profiler.js +0 -5
- package/src/lib/query-string-adapter.js +5 -47
- package/src/lib/realtime-server.js +4 -26
- package/src/lib/request-tracing.js +0 -5
- package/src/lib/resource-monitor.js +4 -1
- package/src/lib/response-formatter.js +0 -38
- package/src/lib/route-helpers.js +3 -1
- package/src/lib/safe-json.js +5 -1
- package/src/lib/server-bootstrap.js +10 -9
- package/src/lib/stage-pipeline.js +2 -2
- package/src/lib/state-transport-client.js +4 -1
- package/src/lib/static-server.js +2 -2
- package/src/lib/status-helpers.js +2 -51
- package/src/lib/tracing.js +0 -5
- package/src/lib/utils.js +6 -67
- package/src/lib/validate.js +5 -88
- package/src/lib/validators.js +0 -38
- package/src/lib/with-audit-logging.js +1 -1
- package/src/lib/workflow-engine.js +10 -77
- package/src/lib/xstate-workflow-engine.js +3 -9
- package/src/plugins/index.js +5 -3
- package/src/server/server.js +26 -27
- package/src/services/collaborator-role.service.js +21 -77
- package/src/services/email-sender.js +7 -34
- package/src/services/notification-engine.js +12 -33
- package/src/services/permission.service.js +10 -73
- package/src/ui/advanced-search-renderer.js +3 -2
- package/src/ui/advanced-widgets.js +1 -1
- package/src/ui/auth-pages.js +10 -9
- package/src/ui/auth-styles.js +1 -1
- package/src/ui/client.js +1 -1
- package/src/ui/component-engine.js +1 -1
- package/src/ui/dashboard-renderer.js +5 -5
- package/src/ui/dialog-engine.js +6 -9
- package/src/ui/dialog-factory.js +2 -2
- package/src/ui/engagement-cards.js +7 -7
- package/src/ui/engagement-dialogs.js +2 -3
- package/src/ui/engagement-grid-renderer.js +20 -19
- package/src/ui/entity-renderer.js +56 -42
- package/src/ui/file-dialogs.js +25 -9
- package/src/ui/flexup-report-renderer.js +21 -18
- package/src/ui/format-helpers.js +27 -28
- package/src/ui/highlight-threading-renderer.js +9 -9
- package/src/ui/job-management-renderer.js +18 -16
- package/src/ui/layout.js +51 -25
- package/src/ui/ml-console-renderer.js +15 -16
- package/src/ui/monitoring-dashboard-client.js +9 -8
- package/src/ui/notifications-renderer.js +11 -10
- package/src/ui/page-handler-admin.js +32 -23
- package/src/ui/page-handler-helpers.js +27 -23
- package/src/ui/page-handler-reviews.js +71 -35
- package/src/ui/page-handler-rfi.js +28 -14
- package/src/ui/page-handler.js +65 -48
- package/src/ui/picker-dialogs.js +26 -19
- package/src/ui/render-helpers.js +84 -46
- package/src/ui/review-comparison-renderer.js +8 -7
- package/src/ui/review-detail-panels.js +8 -9
- package/src/ui/review-detail-renderer.js +3 -3
- package/src/ui/review-detail-script.js +13 -12
- package/src/ui/review-mwr-renderer.js +6 -5
- package/src/ui/review-renderer.js +7 -6
- package/src/ui/review-widgets.js +3 -3
- package/src/ui/rfi-detail-renderer.js +37 -20
- package/src/ui/rfi-report-renderer.js +14 -9
- package/src/ui/settings-renderer-advanced.js +21 -18
- package/src/ui/settings-renderer-advanced2.js +20 -19
- package/src/ui/settings-renderer-teams.js +4 -4
- package/src/ui/settings-renderer.js +4 -4
- package/src/ui/spacing-system.js +5 -2
- package/src/ui/standalone-login.js +2 -2
- package/src/ui/styles.css +2 -1
- package/src/ui/styles2.css +75 -5
- package/src/ui/test-page.js +4 -4
- package/src/ui/widgets.js +20 -15
- package/src/lib/audit-logger.js +0 -193
- package/src/lib/database-core.js +0 -255
- package/src/lib/database-migrations.js +0 -350
- package/src/lib/db-backup.js +0 -97
- package/src/lib/error-boundary.js +0 -134
- package/src/lib/errors.js +0 -69
- package/src/lib/health-monitor.js +0 -134
- package/src/lib/index-optimizer.js +0 -96
- package/src/lib/query-engine-write.js +0 -221
- package/src/lib/query-engine.js +0 -446
- package/src/ui/collaboration-dialogs.js +0 -31
- package/src/ui/global-tags.js +0 -144
- package/src/ui/idle-logout.js +0 -156
- package/src/ui/letter-dialogs.js +0 -37
- package/src/ui/password-reset-page.js +0 -146
- package/src/ui/perf-renderer.js +0 -71
- package/src/ui/rfi-renderer.js +0 -194
- package/src/ui/validation-ui.js +0 -147
package/src/lib/database-core.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database Core - SQLite initialization and schema management
|
|
3
|
-
* Adapted from moonlanding/src/lib/database-core.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import Database from 'better-sqlite3';
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import { runMigrations } from './database-migrations.js';
|
|
10
|
-
|
|
11
|
-
const SQL_TYPES = {
|
|
12
|
-
id: 'TEXT PRIMARY KEY',
|
|
13
|
-
text: 'TEXT',
|
|
14
|
-
textarea: 'TEXT',
|
|
15
|
-
email: 'TEXT',
|
|
16
|
-
int: 'INTEGER',
|
|
17
|
-
decimal: 'REAL',
|
|
18
|
-
bool: 'INTEGER',
|
|
19
|
-
date: 'INTEGER',
|
|
20
|
-
timestamp: 'INTEGER',
|
|
21
|
-
json: 'TEXT',
|
|
22
|
-
image: 'TEXT',
|
|
23
|
-
ref: 'TEXT',
|
|
24
|
-
enum: 'TEXT',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
let db = null;
|
|
28
|
-
let migrationComplete = false;
|
|
29
|
-
const moduleCache = new Map();
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get or create database instance
|
|
33
|
-
* @param {string} dbPath - Path to SQLite database file
|
|
34
|
-
* @returns {Database} better-sqlite3 database instance
|
|
35
|
-
*/
|
|
36
|
-
export function getDatabase(dbPath = null) {
|
|
37
|
-
if (db && !dbPath) return db;
|
|
38
|
-
|
|
39
|
-
const DB_PATH = dbPath || path.resolve(process.cwd(), 'data', 'app.db');
|
|
40
|
-
const dataDir = path.dirname(DB_PATH);
|
|
41
|
-
|
|
42
|
-
if (!fs.existsSync(dataDir)) {
|
|
43
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
db = new Database(DB_PATH);
|
|
47
|
-
db.pragma('journal_mode = WAL');
|
|
48
|
-
const BUSY_TIMEOUT_MS = process.env.DATABASE_BUSY_TIMEOUT_MS || '5000';
|
|
49
|
-
db.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
50
|
-
db.pragma('synchronous = NORMAL');
|
|
51
|
-
db.pragma('foreign_keys = ON');
|
|
52
|
-
db.pragma('auto_vacuum = INCREMENTAL');
|
|
53
|
-
|
|
54
|
-
return db;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Generate unique ID using nanoid-like algorithm
|
|
59
|
-
* @returns {string}
|
|
60
|
-
*/
|
|
61
|
-
export function genId() {
|
|
62
|
-
// Simple but effective ID generation
|
|
63
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Get current Unix timestamp (seconds)
|
|
68
|
-
* @returns {number}
|
|
69
|
-
*/
|
|
70
|
-
export function now() {
|
|
71
|
-
return Math.floor(Date.now() / 1000);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Build column definition from field spec
|
|
76
|
-
* @param {string} key - Field name
|
|
77
|
-
* @param {object} field - Field definition
|
|
78
|
-
* @returns {string} SQL column definition
|
|
79
|
-
*/
|
|
80
|
-
function buildColumnDef(key, field) {
|
|
81
|
-
let col = `"${key}" ${SQL_TYPES[field.type] || 'TEXT'}`;
|
|
82
|
-
|
|
83
|
-
if (field.required && field.type !== 'id') {
|
|
84
|
-
col += ' NOT NULL';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (field.unique) {
|
|
88
|
-
col += ' UNIQUE';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (field.default !== undefined) {
|
|
92
|
-
if (typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean') {
|
|
93
|
-
const defaultVal = typeof field.default === 'string'
|
|
94
|
-
? `'${field.default.replace(/'/g, "''")}'`
|
|
95
|
-
: field.default;
|
|
96
|
-
col += ` DEFAULT ${defaultVal}`;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return col;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Run migrations to create/update tables from entity specs
|
|
105
|
-
* @param {object} configEngine - Config engine with entity specs
|
|
106
|
-
*/
|
|
107
|
-
export function migrate(configEngine) {
|
|
108
|
-
if (migrationComplete) return;
|
|
109
|
-
|
|
110
|
-
const dbInstance = getDatabase();
|
|
111
|
-
let specsToUse = {};
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const allEntities = configEngine.getAllEntities();
|
|
115
|
-
for (const entityName of allEntities) {
|
|
116
|
-
specsToUse[entityName] = configEngine.generateEntitySpec(entityName);
|
|
117
|
-
}
|
|
118
|
-
} catch (e) {
|
|
119
|
-
console.error('[Database] Failed to get specs from ConfigEngine during migration:', e.message);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Create tables
|
|
124
|
-
for (const spec of Object.values(specsToUse)) {
|
|
125
|
-
if (!spec) continue;
|
|
126
|
-
|
|
127
|
-
const tableName = spec.name === 'user' ? 'users' : spec.name;
|
|
128
|
-
const columns = [];
|
|
129
|
-
const foreignKeys = [];
|
|
130
|
-
|
|
131
|
-
// Build columns from fields
|
|
132
|
-
for (const [key, field] of Object.entries(spec.fields || {})) {
|
|
133
|
-
columns.push(buildColumnDef(key, field));
|
|
134
|
-
|
|
135
|
-
if (field.type === 'ref' && field.ref) {
|
|
136
|
-
const refTable = field.ref === 'user' ? 'users' : field.ref;
|
|
137
|
-
foreignKeys.push(`FOREIGN KEY ("${key}") REFERENCES "${refTable}"(id)`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const fkPart = foreignKeys.length ? (',\n' + foreignKeys.join(',\n')) : '';
|
|
142
|
-
const sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.join(',\n')}${fkPart})`;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
dbInstance.exec(sql);
|
|
146
|
-
} catch (e) {
|
|
147
|
-
console.error(`[Database] Table creation failed for ${tableName}:`, e.message, '\nSQL:', sql);
|
|
148
|
-
throw e;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Add new columns (ALTER TABLE for schema evolution)
|
|
152
|
-
try {
|
|
153
|
-
const existingCols = new Set(
|
|
154
|
-
dbInstance.prepare(`PRAGMA table_info("${tableName}")`).all().map(c => c.name)
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
for (const [key, field] of Object.entries(spec.fields || {})) {
|
|
158
|
-
if (!existingCols.has(key)) {
|
|
159
|
-
let colType = SQL_TYPES[field.type] || 'TEXT';
|
|
160
|
-
let alterSql = `ALTER TABLE "${tableName}" ADD COLUMN "${key}" ${colType}`;
|
|
161
|
-
|
|
162
|
-
if (field.default !== undefined && (typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean')) {
|
|
163
|
-
const defaultVal = typeof field.default === 'string'
|
|
164
|
-
? `'${field.default.replace(/'/g, "''")}'`
|
|
165
|
-
: field.default;
|
|
166
|
-
alterSql += ` DEFAULT ${defaultVal}`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
dbInstance.exec(alterSql);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
} catch (e) {
|
|
173
|
-
console.error(`[Database] Column migration failed for ${tableName}:`, e.message);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Create indexes
|
|
178
|
-
for (const spec of Object.values(specsToUse)) {
|
|
179
|
-
if (!spec) continue;
|
|
180
|
-
|
|
181
|
-
const tableName = spec.name === 'user' ? 'users' : spec.name;
|
|
182
|
-
const searchFields = [];
|
|
183
|
-
|
|
184
|
-
for (const [key, field] of Object.entries(spec.fields || {})) {
|
|
185
|
-
if (field.type === 'ref' || field.sortable || field.search) {
|
|
186
|
-
try {
|
|
187
|
-
dbInstance.exec(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${key} ON "${tableName}"("${key}")`);
|
|
188
|
-
} catch (e) {
|
|
189
|
-
console.error(`[Database] Index creation failed for ${tableName}.${key}:`, e.message);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (field.search || key === 'name' || key === 'description') {
|
|
194
|
-
searchFields.push(`"${key}"`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Create FTS virtual table for search
|
|
199
|
-
if (searchFields.length > 0) {
|
|
200
|
-
try {
|
|
201
|
-
dbInstance.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${tableName}_fts USING fts5(${searchFields.join(', ')}, content="${tableName}", content_rowid=id)`);
|
|
202
|
-
} catch (e) {
|
|
203
|
-
console.error(`[Database] FTS table creation failed for ${tableName}:`, e.message);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Run additional migrations
|
|
209
|
-
runCustomMigrations(dbInstance, specsToUse);
|
|
210
|
-
|
|
211
|
-
migrationComplete = true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Run custom migration logic (triggers, data migrations, etc.)
|
|
216
|
-
* @param {Database} dbInstance
|
|
217
|
-
* @param {object} specs
|
|
218
|
-
*/
|
|
219
|
-
function runCustomMigrations(dbInstance, specs) {
|
|
220
|
-
// Delegate to the dedicated migrations module (ported from moonlanding):
|
|
221
|
-
// idempotent triggers, activity_log + notification tables, etc.
|
|
222
|
-
try {
|
|
223
|
-
runMigrations(dbInstance);
|
|
224
|
-
} catch (e) {
|
|
225
|
-
console.error('[Database] Custom migrations failed:', e.message);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Ensure database is initialized (lazy migration)
|
|
231
|
-
*/
|
|
232
|
-
export function ensureInitialized(configEngine) {
|
|
233
|
-
if (!migrationComplete) {
|
|
234
|
-
migrate(configEngine);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Reset database (for testing)
|
|
240
|
-
*/
|
|
241
|
-
export function resetDatabase() {
|
|
242
|
-
const DB_PATH = path.resolve(process.cwd(), 'data', 'app.db');
|
|
243
|
-
try {
|
|
244
|
-
if (db) db.close();
|
|
245
|
-
if (fs.existsSync(DB_PATH)) fs.unlinkSync(DB_PATH);
|
|
246
|
-
if (fs.existsSync(DB_PATH + '-wal')) fs.unlinkSync(DB_PATH + '-wal');
|
|
247
|
-
if (fs.existsSync(DB_PATH + '-shm')) fs.unlinkSync(DB_PATH + '-shm');
|
|
248
|
-
db = null;
|
|
249
|
-
migrationComplete = false;
|
|
250
|
-
} catch (e) {
|
|
251
|
-
console.error('[Database] Reset failed:', e.message);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export { SQL_TYPES };
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database Migrations - Custom SQL migrations and schema evolution
|
|
3
|
-
* Runs after auto-migration completes
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Run custom migrations
|
|
8
|
-
* @param {object} db - better-sqlite3 database instance
|
|
9
|
-
*/
|
|
10
|
-
export function runMigrations(db) {
|
|
11
|
-
// All migrations defined here are idempotent
|
|
12
|
-
const migrations = [
|
|
13
|
-
createTimestampTriggers,
|
|
14
|
-
createActivityLogTable,
|
|
15
|
-
createNotificationTable,
|
|
16
|
-
createSessionsTable,
|
|
17
|
-
createChatTables,
|
|
18
|
-
createAuditLogsTable,
|
|
19
|
-
createRfiTables,
|
|
20
|
-
createPasswordResetTokensTable,
|
|
21
|
-
addHighlightColumns,
|
|
22
|
-
migrateRfiSectionTable,
|
|
23
|
-
createSystemSettingsTable,
|
|
24
|
-
createRecreationLogTable,
|
|
25
|
-
createBugReportTable,
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
for (const migration of migrations) {
|
|
29
|
-
try {
|
|
30
|
-
migration(db);
|
|
31
|
-
} catch (err) {
|
|
32
|
-
console.error('[Migration] Failed:', err.message);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Create triggers to auto-update updated_at on row changes
|
|
39
|
-
* @param {object} db
|
|
40
|
-
*/
|
|
41
|
-
function createTimestampTriggers(db) {
|
|
42
|
-
const tables = ['users', 'engagements', 'rfis', 'reviews']; // Could be dynamic
|
|
43
|
-
|
|
44
|
-
for (const table of tables) {
|
|
45
|
-
try {
|
|
46
|
-
db.exec(`
|
|
47
|
-
CREATE TRIGGER IF NOT EXISTS set_timestamp_${table}
|
|
48
|
-
AFTER UPDATE ON ${table}
|
|
49
|
-
FOR EACH ROW
|
|
50
|
-
BEGIN
|
|
51
|
-
UPDATE ${table} SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
52
|
-
END;
|
|
53
|
-
`);
|
|
54
|
-
} catch (e) {
|
|
55
|
-
// Table might not exist yet
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Create activity_log table for audit trail
|
|
62
|
-
* @param {object} db
|
|
63
|
-
*/
|
|
64
|
-
function createActivityLogTable(db) {
|
|
65
|
-
db.exec(`
|
|
66
|
-
CREATE TABLE IF NOT EXISTS activity_log (
|
|
67
|
-
id TEXT PRIMARY KEY,
|
|
68
|
-
entity_type TEXT NOT NULL,
|
|
69
|
-
entity_id TEXT NOT NULL,
|
|
70
|
-
action TEXT NOT NULL,
|
|
71
|
-
message TEXT,
|
|
72
|
-
details TEXT,
|
|
73
|
-
user_id TEXT,
|
|
74
|
-
created_at INTEGER NOT NULL,
|
|
75
|
-
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
76
|
-
);
|
|
77
|
-
`);
|
|
78
|
-
|
|
79
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_activity_log_entity ON activity_log(entity_type, entity_id)`);
|
|
80
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_activity_log_user ON activity_log(user_id)`);
|
|
81
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_activity_log_created ON activity_log(created_at)`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Create notification table
|
|
86
|
-
* @param {object} db
|
|
87
|
-
*/
|
|
88
|
-
function createNotificationTable(db) {
|
|
89
|
-
db.exec(`
|
|
90
|
-
CREATE TABLE IF NOT EXISTS notification (
|
|
91
|
-
id TEXT PRIMARY KEY,
|
|
92
|
-
type TEXT NOT NULL,
|
|
93
|
-
user_id TEXT NOT NULL,
|
|
94
|
-
title TEXT NOT NULL,
|
|
95
|
-
message TEXT,
|
|
96
|
-
data TEXT,
|
|
97
|
-
entity_type TEXT,
|
|
98
|
-
entity_id TEXT,
|
|
99
|
-
created_at INTEGER NOT NULL,
|
|
100
|
-
read_at INTEGER,
|
|
101
|
-
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
CREATE INDEX IF NOT EXISTS idx_notification_user ON notification(user_id, read_at);
|
|
105
|
-
`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Create sessions table (Lucia auth)
|
|
110
|
-
* @param {object} db
|
|
111
|
-
*/
|
|
112
|
-
function createSessionsTable(db) {
|
|
113
|
-
db.exec(`
|
|
114
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
115
|
-
id TEXT PRIMARY KEY,
|
|
116
|
-
user_id TEXT NOT NULL,
|
|
117
|
-
expires_at INTEGER NOT NULL,
|
|
118
|
-
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
119
|
-
);
|
|
120
|
-
`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Create chat tables (messages + mentions)
|
|
125
|
-
* @param {object} db
|
|
126
|
-
*/
|
|
127
|
-
function createChatTables(db) {
|
|
128
|
-
db.exec(`
|
|
129
|
-
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
130
|
-
id TEXT PRIMARY KEY,
|
|
131
|
-
rfi_id TEXT,
|
|
132
|
-
user_id TEXT,
|
|
133
|
-
content TEXT,
|
|
134
|
-
attachments TEXT,
|
|
135
|
-
reactions TEXT DEFAULT '{}',
|
|
136
|
-
mentions TEXT DEFAULT '[]',
|
|
137
|
-
created_at INTEGER NOT NULL,
|
|
138
|
-
updated_at INTEGER,
|
|
139
|
-
FOREIGN KEY (rfi_id) REFERENCES rfi(id),
|
|
140
|
-
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
CREATE TABLE IF NOT EXISTS chat_mentions (
|
|
144
|
-
id TEXT PRIMARY KEY,
|
|
145
|
-
message_id TEXT NOT NULL,
|
|
146
|
-
user_id TEXT,
|
|
147
|
-
resolved BOOLEAN DEFAULT 0,
|
|
148
|
-
created_at INTEGER NOT NULL,
|
|
149
|
-
FOREIGN KEY (message_id) REFERENCES chat_messages(id),
|
|
150
|
-
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
CREATE INDEX IF NOT EXISTS idx_chat_messages_rfi ON chat_messages(rfi_id);
|
|
154
|
-
CREATE INDEX IF NOT EXISTS idx_chat_messages_user ON chat_messages(user_id);
|
|
155
|
-
CREATE INDEX IF NOT EXISTS idx_chat_mentions_message ON chat_mentions(message_id);
|
|
156
|
-
CREATE INDEX IF NOT EXISTS idx_chat_mentions_user ON chat_mentions(user_id);
|
|
157
|
-
`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Create audit_logs table (before/after state audit trail)
|
|
162
|
-
* Distinct from activity_log; preserved as an additive table.
|
|
163
|
-
* @param {object} db
|
|
164
|
-
*/
|
|
165
|
-
function createAuditLogsTable(db) {
|
|
166
|
-
db.exec(`
|
|
167
|
-
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
168
|
-
id TEXT PRIMARY KEY,
|
|
169
|
-
entity_type TEXT NOT NULL,
|
|
170
|
-
entity_id TEXT NOT NULL,
|
|
171
|
-
action TEXT NOT NULL,
|
|
172
|
-
user_id TEXT,
|
|
173
|
-
before_state TEXT,
|
|
174
|
-
after_state TEXT,
|
|
175
|
-
created_at INTEGER NOT NULL,
|
|
176
|
-
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
CREATE INDEX IF NOT EXISTS idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
|
|
180
|
-
CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id);
|
|
181
|
-
CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at);
|
|
182
|
-
`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Create RFI tables (rfis, questions, responses)
|
|
187
|
-
* @param {object} db
|
|
188
|
-
*/
|
|
189
|
-
function createRfiTables(db) {
|
|
190
|
-
db.exec(`
|
|
191
|
-
CREATE TABLE IF NOT EXISTS rfis (
|
|
192
|
-
id TEXT PRIMARY KEY,
|
|
193
|
-
engagement_id TEXT NOT NULL,
|
|
194
|
-
status TEXT DEFAULT 'draft',
|
|
195
|
-
created_at INTEGER NOT NULL,
|
|
196
|
-
updated_at INTEGER NOT NULL,
|
|
197
|
-
FOREIGN KEY (engagement_id) REFERENCES engagement(id)
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
CREATE TABLE IF NOT EXISTS rfi_questions (
|
|
201
|
-
id TEXT PRIMARY KEY,
|
|
202
|
-
rfi_id TEXT NOT NULL,
|
|
203
|
-
question TEXT NOT NULL,
|
|
204
|
-
category TEXT,
|
|
205
|
-
assigned_to TEXT,
|
|
206
|
-
due_date TEXT,
|
|
207
|
-
status TEXT DEFAULT 'pending',
|
|
208
|
-
created_at INTEGER NOT NULL,
|
|
209
|
-
updated_at INTEGER NOT NULL,
|
|
210
|
-
FOREIGN KEY (rfi_id) REFERENCES rfis(id),
|
|
211
|
-
FOREIGN KEY (assigned_to) REFERENCES users(id)
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
CREATE TABLE IF NOT EXISTS rfi_responses (
|
|
215
|
-
id TEXT PRIMARY KEY,
|
|
216
|
-
question_id TEXT NOT NULL,
|
|
217
|
-
response TEXT,
|
|
218
|
-
attachments TEXT,
|
|
219
|
-
created_at INTEGER NOT NULL,
|
|
220
|
-
updated_at INTEGER NOT NULL,
|
|
221
|
-
FOREIGN KEY (question_id) REFERENCES rfi_questions(id)
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
CREATE INDEX IF NOT EXISTS idx_rfis_engagement ON rfis(engagement_id);
|
|
225
|
-
CREATE INDEX IF NOT EXISTS idx_rfis_status ON rfis(status);
|
|
226
|
-
CREATE INDEX IF NOT EXISTS idx_rfi_questions_rfi ON rfi_questions(rfi_id);
|
|
227
|
-
CREATE INDEX IF NOT EXISTS idx_rfi_questions_status ON rfi_questions(status);
|
|
228
|
-
CREATE INDEX IF NOT EXISTS idx_rfi_responses_question ON rfi_responses(question_id);
|
|
229
|
-
`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Create password_reset_tokens table
|
|
234
|
-
* @param {object} db
|
|
235
|
-
*/
|
|
236
|
-
function createPasswordResetTokensTable(db) {
|
|
237
|
-
db.exec(`
|
|
238
|
-
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
|
239
|
-
id TEXT PRIMARY KEY,
|
|
240
|
-
user_id TEXT NOT NULL,
|
|
241
|
-
token TEXT NOT NULL UNIQUE,
|
|
242
|
-
expires_at INTEGER NOT NULL,
|
|
243
|
-
used INTEGER DEFAULT 0,
|
|
244
|
-
created_at INTEGER NOT NULL,
|
|
245
|
-
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token ON password_reset_tokens(token);
|
|
249
|
-
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user ON password_reset_tokens(user_id);
|
|
250
|
-
`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Add flags/tags columns to highlight table (idempotent — ignores duplicate-column error)
|
|
255
|
-
* @param {object} db
|
|
256
|
-
*/
|
|
257
|
-
function addHighlightColumns(db) {
|
|
258
|
-
for (const col of ['flags', 'tags']) {
|
|
259
|
-
try {
|
|
260
|
-
db.exec(`ALTER TABLE highlight ADD COLUMN ${col} TEXT`);
|
|
261
|
-
} catch (e) {
|
|
262
|
-
// Column already exists or table not present yet
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Rebuild rfi_section table if it lacks an id column (legacy shape)
|
|
269
|
-
* @param {object} db
|
|
270
|
-
*/
|
|
271
|
-
function migrateRfiSectionTable(db) {
|
|
272
|
-
let cols = [];
|
|
273
|
-
try {
|
|
274
|
-
cols = db.prepare('PRAGMA table_info(rfi_section)').all().map(c => c.name);
|
|
275
|
-
} catch (e) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
if (!cols.includes('id')) {
|
|
279
|
-
db.exec(`DROP TABLE IF EXISTS rfi_section`);
|
|
280
|
-
db.exec(`
|
|
281
|
-
CREATE TABLE IF NOT EXISTS rfi_section (
|
|
282
|
-
id TEXT PRIMARY KEY,
|
|
283
|
-
engagement_id TEXT NOT NULL,
|
|
284
|
-
name TEXT NOT NULL DEFAULT '',
|
|
285
|
-
sort_order INTEGER DEFAULT 0,
|
|
286
|
-
created_at INTEGER,
|
|
287
|
-
FOREIGN KEY (engagement_id) REFERENCES engagement(id)
|
|
288
|
-
);
|
|
289
|
-
CREATE INDEX IF NOT EXISTS idx_rfi_section_engagement ON rfi_section(engagement_id);
|
|
290
|
-
`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Create system_settings key/value table
|
|
296
|
-
* @param {object} db
|
|
297
|
-
*/
|
|
298
|
-
function createSystemSettingsTable(db) {
|
|
299
|
-
db.exec(`
|
|
300
|
-
CREATE TABLE IF NOT EXISTS system_settings (
|
|
301
|
-
key TEXT PRIMARY KEY,
|
|
302
|
-
value TEXT NOT NULL,
|
|
303
|
-
updated_at INTEGER
|
|
304
|
-
);
|
|
305
|
-
`);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Create recreation_log table
|
|
310
|
-
* @param {object} db
|
|
311
|
-
*/
|
|
312
|
-
function createRecreationLogTable(db) {
|
|
313
|
-
db.exec(`
|
|
314
|
-
CREATE TABLE IF NOT EXISTS recreation_log (
|
|
315
|
-
id TEXT PRIMARY KEY,
|
|
316
|
-
engagement_id TEXT,
|
|
317
|
-
client_id TEXT,
|
|
318
|
-
engagement_type_id TEXT,
|
|
319
|
-
status TEXT,
|
|
320
|
-
details TEXT,
|
|
321
|
-
error TEXT,
|
|
322
|
-
created_at INTEGER NOT NULL
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
CREATE INDEX IF NOT EXISTS idx_recreation_log_created ON recreation_log(created_at);
|
|
326
|
-
CREATE INDEX IF NOT EXISTS idx_recreation_log_status ON recreation_log(status);
|
|
327
|
-
`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Create bug_report table
|
|
332
|
-
* @param {object} db
|
|
333
|
-
*/
|
|
334
|
-
function createBugReportTable(db) {
|
|
335
|
-
db.exec(`
|
|
336
|
-
CREATE TABLE IF NOT EXISTS bug_report (
|
|
337
|
-
id TEXT PRIMARY KEY,
|
|
338
|
-
user_id TEXT,
|
|
339
|
-
summary TEXT,
|
|
340
|
-
description TEXT,
|
|
341
|
-
url TEXT,
|
|
342
|
-
user_agent TEXT,
|
|
343
|
-
viewport TEXT,
|
|
344
|
-
status TEXT DEFAULT 'open',
|
|
345
|
-
created_at INTEGER NOT NULL
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
CREATE INDEX IF NOT EXISTS idx_bug_report_created ON bug_report(created_at);
|
|
349
|
-
`);
|
|
350
|
-
}
|
package/src/lib/db-backup.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import Database from 'better-sqlite3';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_DB_PATH = path.join(process.cwd(), 'data', 'app.db');
|
|
6
|
-
const DEFAULT_BACKUP_DIR = path.join(process.cwd(), 'data', 'backups');
|
|
7
|
-
|
|
8
|
-
export function createBackup(options = {}) {
|
|
9
|
-
const dbPath = options.dbPath || DEFAULT_DB_PATH;
|
|
10
|
-
const backupDir = options.backupDir || DEFAULT_BACKUP_DIR;
|
|
11
|
-
const label = options.label || 'backup';
|
|
12
|
-
|
|
13
|
-
if (!fs.existsSync(dbPath)) {
|
|
14
|
-
throw new Error(`Database not found: ${dbPath}`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!fs.existsSync(backupDir)) {
|
|
18
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
22
|
-
const backupPath = path.join(backupDir, `app-${label}-${timestamp}.db`);
|
|
23
|
-
|
|
24
|
-
const db = new Database(dbPath);
|
|
25
|
-
try {
|
|
26
|
-
db.pragma('wal_checkpoint(TRUNCATE)');
|
|
27
|
-
fs.copyFileSync(dbPath, backupPath);
|
|
28
|
-
} finally {
|
|
29
|
-
db.close();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const sourceSize = fs.statSync(dbPath).size;
|
|
33
|
-
const backupSize = fs.statSync(backupPath).size;
|
|
34
|
-
|
|
35
|
-
if (sourceSize !== backupSize) {
|
|
36
|
-
throw new Error(`Backup size mismatch: source=${sourceSize}, backup=${backupSize}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return { path: backupPath, size: backupSize, timestamp };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function restoreBackup(backupPath, options = {}) {
|
|
43
|
-
const dbPath = options.dbPath || DEFAULT_DB_PATH;
|
|
44
|
-
|
|
45
|
-
if (!fs.existsSync(backupPath)) {
|
|
46
|
-
throw new Error(`Backup not found: ${backupPath}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const verifyDb = new Database(backupPath, { readonly: true });
|
|
50
|
-
try {
|
|
51
|
-
const result = verifyDb.pragma('integrity_check');
|
|
52
|
-
if (result[0]?.integrity_check !== 'ok') {
|
|
53
|
-
throw new Error(`Backup integrity check failed: ${JSON.stringify(result)}`);
|
|
54
|
-
}
|
|
55
|
-
} finally {
|
|
56
|
-
verifyDb.close();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const walPath = dbPath + '-wal';
|
|
60
|
-
const shmPath = dbPath + '-shm';
|
|
61
|
-
|
|
62
|
-
fs.copyFileSync(backupPath, dbPath);
|
|
63
|
-
if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
|
|
64
|
-
if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
|
|
65
|
-
|
|
66
|
-
return { restored: true, from: backupPath, to: dbPath };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function listBackups(options = {}) {
|
|
70
|
-
const backupDir = options.backupDir || DEFAULT_BACKUP_DIR;
|
|
71
|
-
|
|
72
|
-
if (!fs.existsSync(backupDir)) return [];
|
|
73
|
-
|
|
74
|
-
return fs.readdirSync(backupDir)
|
|
75
|
-
.filter(f => f.endsWith('.db') && f.startsWith('app-'))
|
|
76
|
-
.map(f => ({
|
|
77
|
-
name: f,
|
|
78
|
-
path: path.join(backupDir, f),
|
|
79
|
-
size: fs.statSync(path.join(backupDir, f)).size,
|
|
80
|
-
created: fs.statSync(path.join(backupDir, f)).mtime,
|
|
81
|
-
}))
|
|
82
|
-
.sort((a, b) => b.created - a.created);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function pruneBackups(options = {}) {
|
|
86
|
-
const maxBackups = options.maxBackups || 10;
|
|
87
|
-
const backups = listBackups(options);
|
|
88
|
-
|
|
89
|
-
if (backups.length <= maxBackups) return { pruned: 0 };
|
|
90
|
-
|
|
91
|
-
const toDelete = backups.slice(maxBackups);
|
|
92
|
-
for (const backup of toDelete) {
|
|
93
|
-
fs.unlinkSync(backup.path);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { pruned: toDelete.length };
|
|
97
|
-
}
|