thatcher 1.0.6 → 1.0.33
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 +29 -88
- 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 +14 -5
- 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/audit-logger.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { getDatabase, genId, now } from '@/lib/database-core';
|
|
2
|
-
import { Mutex } from '@/lib/hot-reload/mutex';
|
|
3
|
-
|
|
4
|
-
const db = getDatabase();
|
|
5
|
-
const auditMutex = new Mutex('audit-logger');
|
|
6
|
-
|
|
7
|
-
export const ACTIVITY_TYPES = {
|
|
8
|
-
RFI_QUESTION_VIEW: 'rfi_question_view',
|
|
9
|
-
RFI_QUESTION_RESPOND: 'rfi_question_respond',
|
|
10
|
-
RFI_QUESTION_COMMENT: 'rfi_question_comment',
|
|
11
|
-
RFI_QUESTION_CREATE: 'rfi_question_create',
|
|
12
|
-
RFI_QUESTION_UPDATE: 'rfi_question_update',
|
|
13
|
-
RFI_QUESTION_DELETE: 'rfi_question_delete',
|
|
14
|
-
RFI_QUESTION_ASSIGN: 'rfi_question_assign',
|
|
15
|
-
RFI_QUESTION_DEADLINE: 'rfi_question_deadline',
|
|
16
|
-
HIGHLIGHT_CREATE: 'highlight_create',
|
|
17
|
-
HIGHLIGHT_RESOLVE: 'highlight_resolve',
|
|
18
|
-
HIGHLIGHT_REOPEN: 'highlight_reopen',
|
|
19
|
-
HIGHLIGHT_DELETE: 'highlight_delete',
|
|
20
|
-
REVIEW_CREATE: 'review_create',
|
|
21
|
-
REVIEW_UPDATE: 'review_update',
|
|
22
|
-
REVIEW_ARCHIVE: 'review_archive',
|
|
23
|
-
COLLABORATOR_ADD: 'collaborator_add',
|
|
24
|
-
COLLABORATOR_REMOVE: 'collaborator_remove',
|
|
25
|
-
PERMISSION_CHANGE: 'permission_change',
|
|
26
|
-
ROLE_CHANGE: 'role_change',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const parseJson = (val) => val ? JSON.parse(val) : null;
|
|
30
|
-
|
|
31
|
-
const parseRow = (row) => row ? {
|
|
32
|
-
...row,
|
|
33
|
-
old_permissions: parseJson(row.old_permissions),
|
|
34
|
-
new_permissions: parseJson(row.new_permissions),
|
|
35
|
-
metadata: parseJson(row.metadata),
|
|
36
|
-
before_state: parseJson(row.before_state),
|
|
37
|
-
after_state: parseJson(row.after_state),
|
|
38
|
-
} : null;
|
|
39
|
-
|
|
40
|
-
export const logAction = (entityType, entityId, action, userId, beforeState, afterState) => {
|
|
41
|
-
const id = genId();
|
|
42
|
-
const timestamp = now();
|
|
43
|
-
db.prepare(`
|
|
44
|
-
INSERT INTO audit_logs (id, entity_type, entity_id, action, user_id, before_state, after_state, created_at)
|
|
45
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
46
|
-
`).run(id, entityType, entityId, action, userId || null,
|
|
47
|
-
beforeState ? JSON.stringify(beforeState) : null,
|
|
48
|
-
afterState ? JSON.stringify(afterState) : null,
|
|
49
|
-
timestamp
|
|
50
|
-
);
|
|
51
|
-
return { id, timestamp };
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const getAuditHistory = (filters = {}, page = 1, pageSize = 50) => {
|
|
55
|
-
const wc = [], params = [];
|
|
56
|
-
if (filters.entityType) { wc.push('entity_type = ?'); params.push(filters.entityType); }
|
|
57
|
-
if (filters.entityId) { wc.push('entity_id = ?'); params.push(filters.entityId); }
|
|
58
|
-
if (filters.userId) { wc.push('user_id = ?'); params.push(filters.userId); }
|
|
59
|
-
if (filters.action) { wc.push('action = ?'); params.push(filters.action); }
|
|
60
|
-
if (filters.fromDate) { wc.push('created_at >= ?'); params.push(filters.fromDate); }
|
|
61
|
-
if (filters.toDate) { wc.push('created_at <= ?'); params.push(filters.toDate); }
|
|
62
|
-
const where = wc.length ? 'WHERE ' + wc.join(' AND ') : '';
|
|
63
|
-
const { count: total } = db.prepare(`SELECT COUNT(*) as count FROM audit_logs ${where}`).get(...params);
|
|
64
|
-
const offset = (page - 1) * pageSize;
|
|
65
|
-
const items = db.prepare(`SELECT * FROM audit_logs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, pageSize, offset);
|
|
66
|
-
return {
|
|
67
|
-
items: items.map(i => ({
|
|
68
|
-
id: i.id, entityType: i.entity_type, entityId: i.entity_id, action: i.action,
|
|
69
|
-
userId: i.user_id, beforeState: parseJson(i.before_state), afterState: parseJson(i.after_state), createdAt: i.created_at,
|
|
70
|
-
})),
|
|
71
|
-
pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) },
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export const getEntityAuditTrail = (entityType, entityId) => {
|
|
76
|
-
return db.prepare(`SELECT * FROM audit_logs WHERE entity_type = ? AND entity_id = ? ORDER BY created_at DESC`).all(entityType, entityId)
|
|
77
|
-
.map(i => ({ id: i.id, action: i.action, userId: i.user_id, beforeState: parseJson(i.before_state), afterState: parseJson(i.after_state), createdAt: i.created_at }));
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const statsSql = (col) => `SELECT ${col}, COUNT(*) as count FROM audit_logs WHERE created_at >= ? AND created_at <= ? GROUP BY ${col} ORDER BY count DESC`;
|
|
81
|
-
export const getActionStats = (fromDate, toDate) => db.prepare(statsSql('action')).all(fromDate, toDate);
|
|
82
|
-
export const getUserStats = (fromDate, toDate) => db.prepare(statsSql('user_id')).all(fromDate, toDate);
|
|
83
|
-
|
|
84
|
-
export const logPermissionChange = ({
|
|
85
|
-
userId, entityType, entityId, action, oldPermissions = null, newPermissions = null,
|
|
86
|
-
reason = null, reasonCode = 'other', affectedUserId = null, ipAddress = null, sessionId = null, metadata = null,
|
|
87
|
-
}) => {
|
|
88
|
-
if (!userId || !entityType || !entityId || !action) throw new Error('Missing required audit fields');
|
|
89
|
-
const auditId = genId();
|
|
90
|
-
const timestamp = now();
|
|
91
|
-
db.prepare(`
|
|
92
|
-
INSERT INTO permission_audit (id, user_id, entity_type, entity_id, action, old_permissions, new_permissions,
|
|
93
|
-
reason, reason_code, timestamp, ip_address, session_id, affected_user_id, metadata, created_at, updated_at, created_by, updated_by)
|
|
94
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
95
|
-
`).run(auditId, userId, entityType, entityId, action,
|
|
96
|
-
oldPermissions ? JSON.stringify(oldPermissions) : null, newPermissions ? JSON.stringify(newPermissions) : null,
|
|
97
|
-
reason, reasonCode, timestamp, ipAddress, sessionId, affectedUserId,
|
|
98
|
-
metadata ? JSON.stringify(metadata) : null, timestamp, timestamp, userId, userId
|
|
99
|
-
);
|
|
100
|
-
return auditId;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export const auditPermissionChange = async ({ user, entityType, entityId, action, oldPermissions = null,
|
|
104
|
-
newPermissions = null, affectedUserId = null, reason = null, reasonCode = 'admin_action', metadata = null }) => {
|
|
105
|
-
return auditMutex.runExclusive(() => {
|
|
106
|
-
try {
|
|
107
|
-
logPermissionChange({ userId: user.id, entityType, entityId, action, oldPermissions, newPermissions,
|
|
108
|
-
reason: reason || `Permission ${action}`, reasonCode, affectedUserId, metadata });
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.error('[Audit] Failed to log permission change:', error);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export const auditRoleChange = async ({ user, targetUserId, oldRole, newRole, reason = null, metadata = null }) => {
|
|
116
|
-
await auditPermissionChange({ user, entityType: 'user', entityId: targetUserId, action: 'role_change',
|
|
117
|
-
oldPermissions: { role: oldRole }, newPermissions: { role: newRole },
|
|
118
|
-
affectedUserId: targetUserId, reason: reason || `Role changed from ${oldRole} to ${newRole}`,
|
|
119
|
-
reasonCode: 'role_change', metadata: { ...metadata, old_role: oldRole, new_role: newRole } });
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export const auditCollaboratorAdded = async ({ user, reviewId, collaboratorId, collaboratorUserId, permissions = null, reason = null, metadata = null }) => {
|
|
123
|
-
await auditPermissionChange({ user, entityType: 'review', entityId: reviewId, action: 'grant',
|
|
124
|
-
newPermissions: permissions || { access: 'collaborator' }, affectedUserId: collaboratorUserId,
|
|
125
|
-
reason: reason || 'Collaborator added to review', reasonCode: 'collaborator_added',
|
|
126
|
-
metadata: { ...metadata, collaborator_id: collaboratorId, review_id: reviewId } });
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export const auditCollaboratorRemoved = async ({ user, reviewId, collaboratorId, collaboratorUserId, permissions = null, reason = null, metadata = null }) => {
|
|
130
|
-
await auditPermissionChange({ user, entityType: 'review', entityId: reviewId, action: 'revoke',
|
|
131
|
-
oldPermissions: permissions || { access: 'collaborator' }, affectedUserId: collaboratorUserId,
|
|
132
|
-
reason: reason || 'Collaborator removed from review', reasonCode: 'collaborator_removed',
|
|
133
|
-
metadata: { ...metadata, collaborator_id: collaboratorId, review_id: reviewId } });
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export const auditLifecycleTransition = async ({ user, entityType, entityId, fromStage, toStage, metadata = null }) => {
|
|
137
|
-
await auditPermissionChange({ user, entityType, entityId, action: 'modify',
|
|
138
|
-
oldPermissions: { stage: fromStage }, newPermissions: { stage: toStage },
|
|
139
|
-
reason: `Lifecycle transition: ${fromStage} -> ${toStage}`, reasonCode: 'lifecycle_transition',
|
|
140
|
-
metadata: { ...metadata, from_stage: fromStage, to_stage: toStage } });
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
export const auditPermissionModify = async ({ user, entityType, entityId, oldPermissions, newPermissions,
|
|
144
|
-
affectedUserId = null, reason = null, reasonCode = 'admin_action', metadata = null }) => {
|
|
145
|
-
await auditPermissionChange({ user, entityType, entityId, action: 'modify',
|
|
146
|
-
oldPermissions, newPermissions, affectedUserId, reason: reason || 'Permissions modified', reasonCode, metadata });
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
export const getPermissionAuditTrail = ({ entityType, entityId, userId, affectedUserId, limit = 100, offset = 0 }) => {
|
|
150
|
-
let sql = 'SELECT * FROM permission_audit WHERE 1=1';
|
|
151
|
-
const params = [];
|
|
152
|
-
if (entityType) { sql += ' AND entity_type = ?'; params.push(entityType); }
|
|
153
|
-
if (entityId) { sql += ' AND entity_id = ?'; params.push(entityId); }
|
|
154
|
-
if (userId) { sql += ' AND user_id = ?'; params.push(userId); }
|
|
155
|
-
if (affectedUserId) { sql += ' AND affected_user_id = ?'; params.push(affectedUserId); }
|
|
156
|
-
sql += ' ORDER BY timestamp DESC LIMIT ? OFFSET ?';
|
|
157
|
-
return db.prepare(sql).all(...params, limit, offset).map(parseRow);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
export const getPermissionAuditById = (auditId) => parseRow(db.prepare('SELECT * FROM permission_audit WHERE id = ?').get(auditId));
|
|
161
|
-
export const getPermissionAuditStats = () => db.prepare(`SELECT COUNT(*) as total_audits, COUNT(DISTINCT user_id) as unique_users, COUNT(DISTINCT entity_type) as entity_types, MIN(timestamp) as earliest_change, MAX(timestamp) as latest_change FROM permission_audit`).get();
|
|
162
|
-
export const getPermissionAuditBreakdown = (field) => db.prepare(`SELECT ${field}, COUNT(*) as count FROM permission_audit GROUP BY ${field} ORDER BY count DESC`).all();
|
|
163
|
-
|
|
164
|
-
export const searchPermissionAudit = (searchTerm, limit = 100) => {
|
|
165
|
-
const pattern = `%${searchTerm}%`;
|
|
166
|
-
return db.prepare(`SELECT * FROM permission_audit WHERE reason LIKE ? OR entity_type LIKE ? OR entity_id LIKE ? ORDER BY timestamp DESC LIMIT ?`)
|
|
167
|
-
.all(pattern, pattern, pattern, limit).map(parseRow);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
export const getPermissionAuditByDateRange = (startDate, endDate, limit = 100) =>
|
|
171
|
-
db.prepare(`SELECT * FROM permission_audit WHERE timestamp >= ? AND timestamp <= ? ORDER BY timestamp DESC LIMIT ?`).all(startDate, endDate, limit).map(parseRow);
|
|
172
|
-
|
|
173
|
-
export const exportPermissionAuditCSV = async (filters = {}) => {
|
|
174
|
-
const trail = getPermissionAuditTrail({ ...filters, limit: 10000 });
|
|
175
|
-
const h = 'Timestamp,Changed By,Entity Type,Entity ID,Action,Reason Code,Reason,Affected User,IP Address';
|
|
176
|
-
const rows = trail.map(a => [new Date(a.timestamp * 1000).toISOString(), a.user_id, a.entity_type,
|
|
177
|
-
a.entity_id, a.action, a.reason_code, a.reason || '', a.affected_user_id || '', a.ip_address || '']
|
|
178
|
-
.map(c => `"${c}"`).join(','));
|
|
179
|
-
return [h, ...rows].join('\n');
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
export const logQuestionActivity = (questionId, action, userId, details = null) => {
|
|
183
|
-
return logAction('rfi_question', questionId, action, userId, null, details);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
export const getPermissionDiff = (oldPerms, newPerms) => {
|
|
187
|
-
const diff = { added: [], removed: [], unchanged: [] };
|
|
188
|
-
const oldSet = new Set(Array.isArray(oldPerms) ? oldPerms : []);
|
|
189
|
-
const newSet = new Set(Array.isArray(newPerms) ? newPerms : []);
|
|
190
|
-
for (const p of newSet) { if (!oldSet.has(p)) diff.added.push(p); else diff.unchanged.push(p); }
|
|
191
|
-
for (const p of oldSet) { if (!newSet.has(p)) diff.removed.push(p); }
|
|
192
|
-
return diff;
|
|
193
|
-
};
|
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 };
|