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
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
|
|
2
|
-
|
|
3
|
-
const errorBoundaryState = new Map();
|
|
4
|
-
|
|
5
|
-
export class ErrorBoundary {
|
|
6
|
-
constructor(id, options = {}) {
|
|
7
|
-
this.id = id;
|
|
8
|
-
this.fallback = options.fallback || this.defaultFallback.bind(this);
|
|
9
|
-
this.onError = options.onError;
|
|
10
|
-
this.reset = this.reset.bind(this);
|
|
11
|
-
|
|
12
|
-
errorBoundaryState.set(id, {
|
|
13
|
-
hasError: false,
|
|
14
|
-
error: null,
|
|
15
|
-
errorInfo: null,
|
|
16
|
-
retryCount: 0
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
getState() {
|
|
21
|
-
return errorBoundaryState.get(this.id);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
setState(updates) {
|
|
25
|
-
const current = this.getState();
|
|
26
|
-
errorBoundaryState.set(this.id, { ...current, ...updates });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
catchError(error, errorInfo = {}) {
|
|
30
|
-
const state = this.getState();
|
|
31
|
-
|
|
32
|
-
console.error(`[ErrorBoundary:${this.id}] Caught error:`, error);
|
|
33
|
-
|
|
34
|
-
this.setState({
|
|
35
|
-
hasError: true,
|
|
36
|
-
error,
|
|
37
|
-
errorInfo,
|
|
38
|
-
retryCount: state.retryCount + 1
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (this.onError) {
|
|
42
|
-
this.onError(error, errorInfo);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
reset() {
|
|
47
|
-
this.setState({
|
|
48
|
-
hasError: false,
|
|
49
|
-
error: null,
|
|
50
|
-
errorInfo: null
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
defaultFallback(error, retry) {
|
|
55
|
-
const state = this.getState();
|
|
56
|
-
|
|
57
|
-
return _jsxs('div', {
|
|
58
|
-
className: 'p-6 bg-error-content border border-error rounded-lg',
|
|
59
|
-
children: [
|
|
60
|
-
_jsx('h2', {
|
|
61
|
-
className: 'text-xl font-bold mb-2 text-error',
|
|
62
|
-
children: 'Something went wrong'
|
|
63
|
-
}),
|
|
64
|
-
_jsx('p', {
|
|
65
|
-
className: 'text-sm mb-4',
|
|
66
|
-
children: String(error?.message || 'An unexpected error occurred')
|
|
67
|
-
}),
|
|
68
|
-
state.retryCount < 3 && _jsx('button', {
|
|
69
|
-
onClick: retry,
|
|
70
|
-
className: 'btn btn-primary btn-sm',
|
|
71
|
-
children: 'Try again'
|
|
72
|
-
}),
|
|
73
|
-
state.retryCount >= 3 && _jsx('p', {
|
|
74
|
-
className: 'text-sm text-error',
|
|
75
|
-
children: 'Please refresh the page or contact support'
|
|
76
|
-
})
|
|
77
|
-
]
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
render(children) {
|
|
82
|
-
const state = this.getState();
|
|
83
|
-
|
|
84
|
-
if (state.hasError) {
|
|
85
|
-
return this.fallback(state.error, this.reset);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return children;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function createErrorBoundary(id, options = {}) {
|
|
93
|
-
return new ErrorBoundary(id, options);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function wrapWithErrorBoundary(component, id, options = {}) {
|
|
97
|
-
const boundary = createErrorBoundary(id, options);
|
|
98
|
-
|
|
99
|
-
return (...args) => {
|
|
100
|
-
try {
|
|
101
|
-
const result = component(...args);
|
|
102
|
-
return boundary.render(result);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
boundary.catchError(error);
|
|
105
|
-
return boundary.render(null);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function tryCatch(fn, fallback = null) {
|
|
111
|
-
try {
|
|
112
|
-
return await fn();
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error('[tryCatch] Error caught:', error);
|
|
115
|
-
return fallback;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function safeRender(component, props = {}, fallback = null) {
|
|
120
|
-
try {
|
|
121
|
-
return component(props);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
console.error('[safeRender] Render error:', error);
|
|
124
|
-
return fallback || _jsx('div', {
|
|
125
|
-
className: 'p-4 text-error',
|
|
126
|
-
children: 'Failed to render component'
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (typeof global !== 'undefined') {
|
|
132
|
-
global.errorBoundaryState = errorBoundaryState;
|
|
133
|
-
global.createErrorBoundary = createErrorBoundary;
|
|
134
|
-
}
|
package/src/lib/errors.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { HTTP } from '@/config/constants';
|
|
2
|
-
|
|
3
|
-
export class AppError extends Error {
|
|
4
|
-
constructor(message, code = 'ERROR', statusCode = HTTP.INTERNAL_ERROR, context = {}) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.name = 'AppError';
|
|
7
|
-
this.code = code;
|
|
8
|
-
this.statusCode = statusCode;
|
|
9
|
-
this.context = context;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
toJSON() {
|
|
13
|
-
return {
|
|
14
|
-
status: 'error',
|
|
15
|
-
message: this.message,
|
|
16
|
-
code: this.code,
|
|
17
|
-
statusCode: this.statusCode,
|
|
18
|
-
context: this.context,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class ValidationError extends AppError {
|
|
24
|
-
constructor(message, errors = {}) {
|
|
25
|
-
super(message, 'VALIDATION_ERROR', HTTP.BAD_REQUEST);
|
|
26
|
-
this.errors = errors;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
toJSON() {
|
|
30
|
-
return {
|
|
31
|
-
status: 'error',
|
|
32
|
-
message: this.message,
|
|
33
|
-
code: this.code,
|
|
34
|
-
statusCode: this.statusCode,
|
|
35
|
-
errors: this.errors,
|
|
36
|
-
context: this.context,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class AuthError extends AppError {
|
|
42
|
-
constructor(message = 'Authentication required') {
|
|
43
|
-
super(message, 'UNAUTHORIZED', HTTP.UNAUTHORIZED);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class ForbiddenError extends AppError {
|
|
48
|
-
constructor(message = 'Permission denied') {
|
|
49
|
-
super(message, 'FORBIDDEN', HTTP.FORBIDDEN);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class NotFoundError extends AppError {
|
|
54
|
-
constructor(message = 'Not found') {
|
|
55
|
-
super(message, 'NOT_FOUND', HTTP.NOT_FOUND);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export class ConflictError extends AppError {
|
|
60
|
-
constructor(message = 'Conflict') {
|
|
61
|
-
super(message, 'CONFLICT', HTTP.CONFLICT);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export class ServerError extends AppError {
|
|
66
|
-
constructor(message = 'Server error', code = 'SERVER_ERROR') {
|
|
67
|
-
super(message, code, HTTP.INTERNAL_ERROR);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { getDatabase } from '@/engine';
|
|
2
|
-
|
|
3
|
-
const healthState = {
|
|
4
|
-
checks: new Map(),
|
|
5
|
-
history: [],
|
|
6
|
-
lastCheck: null,
|
|
7
|
-
status: 'unknown'
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function registerHealthCheck(name, checkFn, options = {}) {
|
|
11
|
-
const { critical = false, timeout = 5000 } = options;
|
|
12
|
-
|
|
13
|
-
healthState.checks.set(name, {
|
|
14
|
-
name,
|
|
15
|
-
checkFn,
|
|
16
|
-
critical,
|
|
17
|
-
timeout,
|
|
18
|
-
lastResult: null,
|
|
19
|
-
lastCheck: null
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function runHealthCheck(name) {
|
|
24
|
-
const check = healthState.checks.get(name);
|
|
25
|
-
if (!check) return null;
|
|
26
|
-
|
|
27
|
-
const start = Date.now();
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
31
|
-
setTimeout(() => reject(new Error('Health check timeout')), check.timeout)
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const checkPromise = check.checkFn();
|
|
35
|
-
|
|
36
|
-
await Promise.race([checkPromise, timeoutPromise]);
|
|
37
|
-
|
|
38
|
-
const result = {
|
|
39
|
-
status: 'healthy',
|
|
40
|
-
latency: Date.now() - start,
|
|
41
|
-
timestamp: new Date().toISOString()
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
check.lastResult = result;
|
|
45
|
-
check.lastCheck = Date.now();
|
|
46
|
-
|
|
47
|
-
return result;
|
|
48
|
-
} catch (error) {
|
|
49
|
-
const result = {
|
|
50
|
-
status: 'unhealthy',
|
|
51
|
-
error: String(error?.message || error),
|
|
52
|
-
latency: Date.now() - start,
|
|
53
|
-
timestamp: new Date().toISOString()
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
check.lastResult = result;
|
|
57
|
-
check.lastCheck = Date.now();
|
|
58
|
-
|
|
59
|
-
if (check.critical) {
|
|
60
|
-
console.error(`[HEALTH] Critical check failed: ${name}`, error);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function runAllHealthChecks() {
|
|
68
|
-
const results = {};
|
|
69
|
-
const checks = Array.from(healthState.checks.keys());
|
|
70
|
-
|
|
71
|
-
for (const name of checks) {
|
|
72
|
-
results[name] = await runHealthCheck(name);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const allHealthy = Object.values(results).every(r => r.status === 'healthy');
|
|
76
|
-
const criticalUnhealthy = Array.from(healthState.checks.values())
|
|
77
|
-
.filter(c => c.critical && c.lastResult?.status === 'unhealthy')
|
|
78
|
-
.length > 0;
|
|
79
|
-
|
|
80
|
-
const status = criticalUnhealthy ? 'critical' : allHealthy ? 'healthy' : 'degraded';
|
|
81
|
-
|
|
82
|
-
const summary = {
|
|
83
|
-
status,
|
|
84
|
-
timestamp: new Date().toISOString(),
|
|
85
|
-
checks: results,
|
|
86
|
-
uptime: process.uptime()
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
healthState.lastCheck = summary;
|
|
90
|
-
healthState.status = status;
|
|
91
|
-
healthState.history.push(summary);
|
|
92
|
-
|
|
93
|
-
if (healthState.history.length > 100) {
|
|
94
|
-
healthState.history.shift();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return summary;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function getHealthStatus() {
|
|
101
|
-
return healthState.lastCheck || { status: 'unknown', checks: {} };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function getHealthHistory() {
|
|
105
|
-
return healthState.history.slice(-20);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
registerHealthCheck('database', async () => {
|
|
109
|
-
const db = getDatabase();
|
|
110
|
-
db.prepare('SELECT 1').get();
|
|
111
|
-
}, { critical: true, timeout: 2000 });
|
|
112
|
-
|
|
113
|
-
registerHealthCheck('memory', async () => {
|
|
114
|
-
const usage = process.memoryUsage();
|
|
115
|
-
const heapUsedMB = usage.heapUsed / 1024 / 1024;
|
|
116
|
-
|
|
117
|
-
if (heapUsedMB > 512) {
|
|
118
|
-
throw new Error(`High memory usage: ${heapUsedMB.toFixed(0)}MB`);
|
|
119
|
-
}
|
|
120
|
-
}, { critical: false, timeout: 1000 });
|
|
121
|
-
|
|
122
|
-
registerHealthCheck('uptime', async () => {
|
|
123
|
-
const uptime = process.uptime();
|
|
124
|
-
if (uptime < 5) {
|
|
125
|
-
throw new Error('Recently restarted');
|
|
126
|
-
}
|
|
127
|
-
}, { critical: false, timeout: 100 });
|
|
128
|
-
|
|
129
|
-
if (typeof global !== 'undefined') {
|
|
130
|
-
global.healthState = healthState;
|
|
131
|
-
global.runHealthCheck = runHealthCheck;
|
|
132
|
-
global.runAllHealthChecks = runAllHealthChecks;
|
|
133
|
-
global.getHealthStatus = getHealthStatus;
|
|
134
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { getDatabase } from '@/lib/database-core';
|
|
2
|
-
|
|
3
|
-
const COMPOSITE_INDEXES = [
|
|
4
|
-
{ name: 'idx_engagement_client_stage', table: 'engagement', columns: ['client_id', 'stage'], where: "status != 'deleted'" },
|
|
5
|
-
{ name: 'idx_engagement_status_created', table: 'engagement', columns: ['status', 'created_at'] },
|
|
6
|
-
{ name: 'idx_engagement_stage_created', table: 'engagement', columns: ['stage', 'created_at'] },
|
|
7
|
-
{ name: 'idx_rfi_engagement_client_status', table: 'rfi', columns: ['engagement_id', 'client_status'] },
|
|
8
|
-
{ name: 'idx_rfi_engagement_auditor_status', table: 'rfi', columns: ['engagement_id', 'auditor_status'] },
|
|
9
|
-
{ name: 'idx_rfi_engagement_status', table: 'rfi', columns: ['engagement_id', 'status'] },
|
|
10
|
-
{ name: 'idx_users_client_role', table: 'users', columns: ['client_id', 'role', 'status'] },
|
|
11
|
-
{ name: 'idx_users_email_active', table: 'users', columns: ['email'], where: "status = 'active'" },
|
|
12
|
-
{ name: 'idx_users_type_status', table: 'users', columns: ['type', 'status'] },
|
|
13
|
-
{ name: 'idx_sessions_expires', table: 'sessions', columns: ['expires_at'] },
|
|
14
|
-
{ name: 'idx_sessions_user_expires', table: 'sessions', columns: ['user_id', 'expires_at'] },
|
|
15
|
-
{ name: 'idx_audit_composite', table: 'audit_logs', columns: ['entity_type', 'entity_id', 'created_at'] },
|
|
16
|
-
{ name: 'idx_audit_user_created', table: 'audit_logs', columns: ['user_id', 'created_at'] },
|
|
17
|
-
{ name: 'idx_email_status_created', table: 'email', columns: ['status', 'created_at'] },
|
|
18
|
-
{ name: 'idx_email_recipient', table: 'email', columns: ['recipient_email', 'created_at'] },
|
|
19
|
-
{ name: 'idx_rfi_questions_rfi_status', table: 'rfi_questions', columns: ['rfi_id', 'status'] },
|
|
20
|
-
{ name: 'idx_rfi_questions_assigned', table: 'rfi_questions', columns: ['assigned_to', 'status'] },
|
|
21
|
-
{ name: 'idx_chat_messages_rfi_created', table: 'chat_messages', columns: ['rfi_id', 'created_at'] },
|
|
22
|
-
{ name: 'idx_chat_mentions_user_resolved', table: 'chat_mentions', columns: ['user_id', 'resolved'] },
|
|
23
|
-
{ name: 'idx_password_reset_expires', table: 'password_reset_tokens', columns: ['expires_at', 'used'] }
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const indexExists = (db, indexName) => {
|
|
27
|
-
try {
|
|
28
|
-
const result = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name=?").get(indexName);
|
|
29
|
-
return !!result;
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const createOptimizedIndexes = () => {
|
|
36
|
-
const db = getDatabase();
|
|
37
|
-
const created = [];
|
|
38
|
-
const skipped = [];
|
|
39
|
-
const errors = [];
|
|
40
|
-
|
|
41
|
-
for (const idx of COMPOSITE_INDEXES) {
|
|
42
|
-
if (indexExists(db, idx.name)) {
|
|
43
|
-
skipped.push(idx.name);
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(idx.table);
|
|
49
|
-
if (!tableExists) {
|
|
50
|
-
skipped.push(`${idx.name} (table ${idx.table} not found)`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const columns = idx.columns.map(c => `"${c}"`).join(', ');
|
|
55
|
-
const wherePart = idx.where ? ` WHERE ${idx.where}` : '';
|
|
56
|
-
const sql = `CREATE INDEX IF NOT EXISTS "${idx.name}" ON "${idx.table}"(${columns})${wherePart}`;
|
|
57
|
-
|
|
58
|
-
db.exec(sql);
|
|
59
|
-
created.push(idx.name);
|
|
60
|
-
} catch (e) {
|
|
61
|
-
errors.push({ index: idx.name, error: e.message });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return { created, skipped, errors, total: COMPOSITE_INDEXES.length };
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const analyzeQuery = (sql) => {
|
|
69
|
-
const db = getDatabase();
|
|
70
|
-
try {
|
|
71
|
-
const plan = db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all();
|
|
72
|
-
return plan;
|
|
73
|
-
} catch (e) {
|
|
74
|
-
return [{ error: e.message }];
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const getIndexUsage = () => {
|
|
79
|
-
const db = getDatabase();
|
|
80
|
-
try {
|
|
81
|
-
const indexes = db.prepare("SELECT name, tbl_name FROM sqlite_master WHERE type='index' AND sql IS NOT NULL ORDER BY tbl_name, name").all();
|
|
82
|
-
return indexes;
|
|
83
|
-
} catch {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const optimizeDatabase = () => {
|
|
89
|
-
const db = getDatabase();
|
|
90
|
-
try {
|
|
91
|
-
db.exec('ANALYZE');
|
|
92
|
-
return { status: 'success', message: 'Database statistics updated' };
|
|
93
|
-
} catch (e) {
|
|
94
|
-
return { status: 'error', error: e.message };
|
|
95
|
-
}
|
|
96
|
-
};
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Query Engine - Write operations (create, update, delete)
|
|
3
|
-
* Adapted from moonlanding/src/lib/query-engine-write.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getDatabase } from './database-core.js';
|
|
7
|
-
import { getSpec } from '../config/spec-helpers.js';
|
|
8
|
-
import { RECORD_STATUS } from '../config/constants.js';
|
|
9
|
-
import { now, genId } from './database-core.js';
|
|
10
|
-
import { executeHook } from './hook-engine.js';
|
|
11
|
-
import { broadcastUpdate } from './realtime-server.js'; // Optional
|
|
12
|
-
import { createLogger } from './logger.js';
|
|
13
|
-
import { getConfigEngineSync } from './config-generator-engine.js';
|
|
14
|
-
|
|
15
|
-
const db = getDatabase();
|
|
16
|
-
const logger = createLogger('[WriteEngine]');
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create a new record
|
|
20
|
-
* @param {string} entity - Entity name
|
|
21
|
-
* @param {object} data - Record data
|
|
22
|
-
* @param {object} user - User performing the action
|
|
23
|
-
* @returns {object} Created record
|
|
24
|
-
*/
|
|
25
|
-
export function create(entity, data, user) {
|
|
26
|
-
const spec = getSpec(entity);
|
|
27
|
-
if (!spec) throw new Error(`Entity not found: ${entity}`);
|
|
28
|
-
|
|
29
|
-
const tableName = entity === 'user' ? 'users' : entity;
|
|
30
|
-
const record = {
|
|
31
|
-
...data,
|
|
32
|
-
id: genId(),
|
|
33
|
-
created_by: user?.id || null,
|
|
34
|
-
created_at: now(),
|
|
35
|
-
updated_at: null,
|
|
36
|
-
status: data.status || RECORD_STATUS.ACTIVE,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Auto fields and field coercion
|
|
40
|
-
for (const [key, field] of Object.entries(spec.fields || {})) {
|
|
41
|
-
if (field.auto === 'increment') {
|
|
42
|
-
// Get max + 1 (simplified)
|
|
43
|
-
const maxRow = db.prepare(`SELECT MAX("${key}") as max FROM "${tableName}"`).get();
|
|
44
|
-
record[key] = (maxRow?.max || 0) + 1;
|
|
45
|
-
}
|
|
46
|
-
if (field.auto === 'uuid' && !record[key]) {
|
|
47
|
-
record[key] = genId();
|
|
48
|
-
}
|
|
49
|
-
if (field.auto === 'timestamp' && !record[key]) {
|
|
50
|
-
record[key] = now();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Build INSERT
|
|
55
|
-
const columns = Object.keys(record).filter(k => record[k] !== undefined);
|
|
56
|
-
const placeholders = columns.map(() => '?').join(', ');
|
|
57
|
-
const values = columns.map(k => record[k]);
|
|
58
|
-
|
|
59
|
-
const sql = `INSERT INTO "${tableName}" (${columns.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const info = db.prepare(sql).run(...values);
|
|
63
|
-
record.id = info.lastInsertRowid ? String(info.lastInsertRowid) : record.id;
|
|
64
|
-
} catch (e) {
|
|
65
|
-
logger.error(`Create ${entity} failed`, { error: e.message, data: record });
|
|
66
|
-
throw new Error(`Failed to create ${entity}: ${e.message}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Execute hooks
|
|
70
|
-
executeHook(`create:${entity}:after`, {
|
|
71
|
-
entity,
|
|
72
|
-
id: record.id,
|
|
73
|
-
data: record,
|
|
74
|
-
user,
|
|
75
|
-
}).catch(err => logger.error('create hook error', err));
|
|
76
|
-
|
|
77
|
-
return record;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Update an existing record
|
|
82
|
-
* @param {string} entity
|
|
83
|
-
* @param {string|number} id
|
|
84
|
-
* @param {object} data - Fields to update
|
|
85
|
-
* @param {object} user - User performing the action
|
|
86
|
-
* @returns {object} Updated record
|
|
87
|
-
*/
|
|
88
|
-
export function update(entity, id, data, user) {
|
|
89
|
-
const spec = getSpec(entity);
|
|
90
|
-
if (!spec) throw new Error(`Entity not found: ${entity}`);
|
|
91
|
-
|
|
92
|
-
const tableName = entity === 'user' ? 'users' : entity;
|
|
93
|
-
|
|
94
|
-
// Fetch existing record
|
|
95
|
-
const existing = db.prepare(`SELECT * FROM "${tableName}" WHERE id = ?`).get(id);
|
|
96
|
-
if (!existing) {
|
|
97
|
-
throw new Error(`${entity} with id ${id} not found`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const updateData = {
|
|
101
|
-
...data,
|
|
102
|
-
updated_at: now(),
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Build UPDATE statement
|
|
106
|
-
const setClauses = [];
|
|
107
|
-
const values = [];
|
|
108
|
-
|
|
109
|
-
for (const [key, value] of Object.entries(updateData)) {
|
|
110
|
-
if (value !== undefined) {
|
|
111
|
-
setClauses.push(`"${key}" = ?`);
|
|
112
|
-
values.push(value);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (setClauses.length === 0) {
|
|
117
|
-
return existing; // Nothing to update
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
values.push(id);
|
|
121
|
-
const sql = `UPDATE "${tableName}" SET ${setClauses.join(', ')} WHERE id = ?`;
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
db.prepare(sql).run(...values);
|
|
125
|
-
} catch (e) {
|
|
126
|
-
logger.error(`Update ${entity} ${id} failed`, { error: e.message, data: updateData });
|
|
127
|
-
throw new Error(`Failed to update ${entity}: ${e.message}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Fetch updated record
|
|
131
|
-
const updated = db.prepare(`SELECT * FROM "${tableName}" WHERE id = ?`).get(id);
|
|
132
|
-
|
|
133
|
-
// Execute hooks
|
|
134
|
-
executeHook(`update:${entity}:after`, {
|
|
135
|
-
entity,
|
|
136
|
-
id,
|
|
137
|
-
data: updated,
|
|
138
|
-
before: existing,
|
|
139
|
-
after: updated,
|
|
140
|
-
user,
|
|
141
|
-
}).catch(err => logger.error('update hook error', err));
|
|
142
|
-
|
|
143
|
-
return updated;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Delete (soft delete) a record
|
|
148
|
-
* @param {string} entity
|
|
149
|
-
* @param {string|number} id
|
|
150
|
-
* @param {object} user - User performing the action
|
|
151
|
-
* @returns {object} Deleted record
|
|
152
|
-
*/
|
|
153
|
-
export function remove(entity, id) {
|
|
154
|
-
const spec = getSpec(entity);
|
|
155
|
-
if (!spec) throw new Error(`Entity not found: ${entity}`);
|
|
156
|
-
|
|
157
|
-
const tableName = entity === 'user' ? 'users' : entity;
|
|
158
|
-
|
|
159
|
-
const existing = db.prepare(`SELECT * FROM "${tableName}" WHERE id = ?`).get(id);
|
|
160
|
-
if (!existing) {
|
|
161
|
-
throw new Error(`${entity} with id ${id} not found`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const hasStatus = spec.fields && 'status' in spec.fields;
|
|
165
|
-
const hasArchived = spec.fields && 'archived' in spec.fields;
|
|
166
|
-
|
|
167
|
-
let result;
|
|
168
|
-
|
|
169
|
-
if (hasStatus) {
|
|
170
|
-
// Soft delete
|
|
171
|
-
const sql = `UPDATE "${tableName}" SET status = ?, updated_at = ? WHERE id = ?`;
|
|
172
|
-
db.prepare(sql).run(RECORD_STATUS.DELETED, now(), id);
|
|
173
|
-
result = { ...existing, status: RECORD_STATUS.DELETED };
|
|
174
|
-
} else if (hasArchived) {
|
|
175
|
-
// Archive instead
|
|
176
|
-
const sql = `UPDATE "${tableName}" SET archived = 1, updated_at = ? WHERE id = ?`;
|
|
177
|
-
db.prepare(sql).run(now(), id);
|
|
178
|
-
result = { ...existing, archived: 1 };
|
|
179
|
-
} else {
|
|
180
|
-
// Hard delete
|
|
181
|
-
const sql = `DELETE FROM "${tableName}" WHERE id = ?`;
|
|
182
|
-
db.prepare(sql).run(id);
|
|
183
|
-
result = existing;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Execute hooks
|
|
187
|
-
executeHook(`delete:${entity}:after`, {
|
|
188
|
-
entity,
|
|
189
|
-
id,
|
|
190
|
-
data: result,
|
|
191
|
-
user,
|
|
192
|
-
}).catch(err => logger.error('delete hook error', err));
|
|
193
|
-
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Bulk create multiple records
|
|
199
|
-
* @param {string} entity
|
|
200
|
-
* @param {Array<object>} records
|
|
201
|
-
* @param {object} user
|
|
202
|
-
* @returns {Array} Created records
|
|
203
|
-
*/
|
|
204
|
-
export function bulkCreate(entity, records, user) {
|
|
205
|
-
return db.transaction((recordsData) => {
|
|
206
|
-
return recordsData.map(data => create(entity, data, user));
|
|
207
|
-
})(records);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Hard delete all records (for cleanup)
|
|
212
|
-
* @param {string} entity
|
|
213
|
-
*/
|
|
214
|
-
export function hardDeleteAll(entity) {
|
|
215
|
-
const spec = getSpec(entity);
|
|
216
|
-
if (!spec) throw new Error(`Entity not found: ${entity}`);
|
|
217
|
-
|
|
218
|
-
const tableName = entity === 'user' ? 'users' : entity;
|
|
219
|
-
const sql = `DELETE FROM "${tableName}"`;
|
|
220
|
-
db.prepare(sql).run();
|
|
221
|
-
}
|