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.
Files changed (174) hide show
  1. package/README.md +4 -16
  2. package/package.json +12 -18
  3. package/src/adapters/google-auth.js +4 -43
  4. package/src/adapters/google-drive.js +12 -67
  5. package/src/app/api/audit/dashboard/route.js +14 -11
  6. package/src/app/api/audit/logs/route.js +2 -2
  7. package/src/app/api/audit/permissions/[id]/route.js +6 -3
  8. package/src/app/api/audit/permissions/route.js +10 -7
  9. package/src/app/api/audit/permissions/stats/route.js +8 -5
  10. package/src/app/api/audit/route.js +4 -4
  11. package/src/app/api/audit/stats/route.js +1 -1
  12. package/src/app/api/auth/google/callback/route.js +5 -2
  13. package/src/app/api/auth/google/route.js +4 -1
  14. package/src/app/api/auth/login/route.js +5 -2
  15. package/src/app/api/auth/mwr-bridge/route.js +11 -21
  16. package/src/app/api/auth/password-reset/route.js +15 -14
  17. package/src/app/api/cron/trigger/route.js +4 -1
  18. package/src/app/api/debug/[[...path]]/route.js +6 -5
  19. package/src/app/api/debug/sqlite/route.js +7 -12
  20. package/src/app/api/debug/sync/route.js +5 -5
  21. package/src/app/api/domains/route.js +4 -1
  22. package/src/app/api/email/allocate/batch/route.js +19 -29
  23. package/src/app/api/email/allocate/route.js +16 -17
  24. package/src/app/api/email/receive/route.js +31 -42
  25. package/src/app/api/email/send/route.js +28 -16
  26. package/src/app/api/email/unallocated/route.js +10 -13
  27. package/src/app/api/files/[id]/route.js +4 -1
  28. package/src/app/api/formula/[[...path]]/route.js +0 -4
  29. package/src/app/api/health/route.js +10 -9
  30. package/src/app/api/metrics/route.js +1 -1
  31. package/src/cli.js +4 -4
  32. package/src/config/config-loader.js +1 -21
  33. package/src/config/constants.js +0 -35
  34. package/src/config/env.js +0 -30
  35. package/src/config/spec-helpers.js +6 -95
  36. package/src/engine.js +23 -0
  37. package/src/engine.server.js +8 -78
  38. package/src/index.js +42 -67
  39. package/src/lib/alert-manager.js +7 -3
  40. package/src/lib/api-helpers.js +4 -3
  41. package/src/lib/api.js +10 -10
  42. package/src/lib/audit-logger-enhanced.js +77 -83
  43. package/src/lib/auth-middleware.js +1 -32
  44. package/src/lib/auth-route-helpers.js +5 -0
  45. package/src/lib/busybase-adapter.js +0 -5
  46. package/src/lib/busybase-audit-reads.js +142 -0
  47. package/src/lib/busybase-audit.js +41 -0
  48. package/src/lib/busybase-lucia-adapter.js +72 -0
  49. package/src/lib/busybase-store.js +338 -0
  50. package/src/lib/config-field-helpers.js +0 -17
  51. package/src/lib/config-generator-engine.js +19 -36
  52. package/src/lib/config-helpers.js +0 -21
  53. package/src/lib/crud-action-helpers.js +2 -2
  54. package/src/lib/crud-factory.js +1 -16
  55. package/src/lib/crud-handlers.js +17 -52
  56. package/src/lib/csrf-protection.js +5 -2
  57. package/src/lib/date-utils.js +3 -127
  58. package/src/lib/debug-registry.js +0 -5
  59. package/src/lib/domain-loader.js +4 -1
  60. package/src/lib/email-sender.js +25 -23
  61. package/src/lib/error-handler.js +5 -41
  62. package/src/lib/error-recovery.js +10 -7
  63. package/src/lib/error-resilience.js +7 -5
  64. package/src/lib/events-engine.js +9 -6
  65. package/src/lib/export-sink.js +0 -5
  66. package/src/lib/field-iterator.js +7 -76
  67. package/src/lib/field-registry.js +0 -22
  68. package/src/lib/generic-crud-handler.js +0 -16
  69. package/src/lib/hook-engine.js +5 -53
  70. package/src/lib/hot-reload/checkpoint.js +5 -2
  71. package/src/lib/hot-reload/promise-container.js +7 -3
  72. package/src/lib/hot-reload/route-wrapper.js +4 -1
  73. package/src/lib/hot-reload/supervisor.js +6 -3
  74. package/src/lib/hyperformula-service.js +0 -5
  75. package/src/lib/id-helpers.js +10 -0
  76. package/src/lib/index.js +4 -7
  77. package/src/lib/log-aggregator.js +0 -1
  78. package/src/lib/logger.js +0 -15
  79. package/src/lib/metrics-collector.js +2 -46
  80. package/src/lib/next-compat.js +15 -1
  81. package/src/lib/observability-bootstrap.js +0 -5
  82. package/src/lib/perf-monitor.js +4 -1
  83. package/src/lib/perf-profiler.js +0 -5
  84. package/src/lib/query-string-adapter.js +5 -47
  85. package/src/lib/realtime-server.js +4 -26
  86. package/src/lib/request-tracing.js +0 -5
  87. package/src/lib/resource-monitor.js +4 -1
  88. package/src/lib/response-formatter.js +0 -38
  89. package/src/lib/route-helpers.js +3 -1
  90. package/src/lib/safe-json.js +5 -1
  91. package/src/lib/server-bootstrap.js +10 -9
  92. package/src/lib/stage-pipeline.js +2 -2
  93. package/src/lib/state-transport-client.js +4 -1
  94. package/src/lib/static-server.js +2 -2
  95. package/src/lib/status-helpers.js +2 -51
  96. package/src/lib/tracing.js +0 -5
  97. package/src/lib/utils.js +6 -67
  98. package/src/lib/validate.js +5 -88
  99. package/src/lib/validators.js +0 -38
  100. package/src/lib/with-audit-logging.js +1 -1
  101. package/src/lib/workflow-engine.js +10 -77
  102. package/src/lib/xstate-workflow-engine.js +3 -9
  103. package/src/plugins/index.js +5 -3
  104. package/src/server/server.js +26 -27
  105. package/src/services/collaborator-role.service.js +21 -77
  106. package/src/services/email-sender.js +7 -34
  107. package/src/services/notification-engine.js +12 -33
  108. package/src/services/permission.service.js +10 -73
  109. package/src/ui/advanced-search-renderer.js +3 -2
  110. package/src/ui/advanced-widgets.js +1 -1
  111. package/src/ui/auth-pages.js +10 -9
  112. package/src/ui/auth-styles.js +1 -1
  113. package/src/ui/client.js +1 -1
  114. package/src/ui/component-engine.js +1 -1
  115. package/src/ui/dashboard-renderer.js +5 -5
  116. package/src/ui/dialog-engine.js +6 -9
  117. package/src/ui/dialog-factory.js +2 -2
  118. package/src/ui/engagement-cards.js +7 -7
  119. package/src/ui/engagement-dialogs.js +2 -3
  120. package/src/ui/engagement-grid-renderer.js +20 -19
  121. package/src/ui/entity-renderer.js +56 -42
  122. package/src/ui/file-dialogs.js +25 -9
  123. package/src/ui/flexup-report-renderer.js +21 -18
  124. package/src/ui/format-helpers.js +27 -28
  125. package/src/ui/highlight-threading-renderer.js +9 -9
  126. package/src/ui/job-management-renderer.js +18 -16
  127. package/src/ui/layout.js +51 -25
  128. package/src/ui/ml-console-renderer.js +15 -16
  129. package/src/ui/monitoring-dashboard-client.js +9 -8
  130. package/src/ui/notifications-renderer.js +11 -10
  131. package/src/ui/page-handler-admin.js +32 -23
  132. package/src/ui/page-handler-helpers.js +27 -23
  133. package/src/ui/page-handler-reviews.js +71 -35
  134. package/src/ui/page-handler-rfi.js +28 -14
  135. package/src/ui/page-handler.js +65 -48
  136. package/src/ui/picker-dialogs.js +26 -19
  137. package/src/ui/render-helpers.js +84 -46
  138. package/src/ui/review-comparison-renderer.js +8 -7
  139. package/src/ui/review-detail-panels.js +8 -9
  140. package/src/ui/review-detail-renderer.js +3 -3
  141. package/src/ui/review-detail-script.js +13 -12
  142. package/src/ui/review-mwr-renderer.js +6 -5
  143. package/src/ui/review-renderer.js +7 -6
  144. package/src/ui/review-widgets.js +3 -3
  145. package/src/ui/rfi-detail-renderer.js +37 -20
  146. package/src/ui/rfi-report-renderer.js +14 -9
  147. package/src/ui/settings-renderer-advanced.js +21 -18
  148. package/src/ui/settings-renderer-advanced2.js +20 -19
  149. package/src/ui/settings-renderer-teams.js +4 -4
  150. package/src/ui/settings-renderer.js +4 -4
  151. package/src/ui/spacing-system.js +5 -2
  152. package/src/ui/standalone-login.js +2 -2
  153. package/src/ui/styles.css +2 -1
  154. package/src/ui/styles2.css +75 -5
  155. package/src/ui/test-page.js +4 -4
  156. package/src/ui/widgets.js +20 -15
  157. package/src/lib/audit-logger.js +0 -193
  158. package/src/lib/database-core.js +0 -255
  159. package/src/lib/database-migrations.js +0 -350
  160. package/src/lib/db-backup.js +0 -97
  161. package/src/lib/error-boundary.js +0 -134
  162. package/src/lib/errors.js +0 -69
  163. package/src/lib/health-monitor.js +0 -134
  164. package/src/lib/index-optimizer.js +0 -96
  165. package/src/lib/query-engine-write.js +0 -221
  166. package/src/lib/query-engine.js +0 -446
  167. package/src/ui/collaboration-dialogs.js +0 -31
  168. package/src/ui/global-tags.js +0 -144
  169. package/src/ui/idle-logout.js +0 -156
  170. package/src/ui/letter-dialogs.js +0 -37
  171. package/src/ui/password-reset-page.js +0 -146
  172. package/src/ui/perf-renderer.js +0 -71
  173. package/src/ui/rfi-renderer.js +0 -194
  174. 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
- }