thatcher 1.0.4
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/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export function createLinearProgress(options = {}) {
|
|
2
|
+
const {
|
|
3
|
+
percentage = 0,
|
|
4
|
+
label = '',
|
|
5
|
+
color = '#007bff',
|
|
6
|
+
height = 8,
|
|
7
|
+
showLabel = true
|
|
8
|
+
} = options;
|
|
9
|
+
|
|
10
|
+
const container = document.createElement('div');
|
|
11
|
+
container.className = 'linear-progress-container';
|
|
12
|
+
container.style.cssText = `
|
|
13
|
+
width: 100%;
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const bar = document.createElement('div');
|
|
17
|
+
bar.className = 'linear-progress-bar';
|
|
18
|
+
bar.style.cssText = `
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: ${height}px;
|
|
21
|
+
background: #e0e0e0;
|
|
22
|
+
border-radius: ${height / 2}px;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
position: relative;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const fill = document.createElement('div');
|
|
28
|
+
fill.className = 'linear-progress-fill';
|
|
29
|
+
fill.style.cssText = `
|
|
30
|
+
height: 100%;
|
|
31
|
+
width: ${Math.min(100, Math.max(0, percentage))}%;
|
|
32
|
+
background: ${color};
|
|
33
|
+
border-radius: ${height / 2}px;
|
|
34
|
+
transition: width 0.3s ease;
|
|
35
|
+
position: relative;
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
bar.appendChild(fill);
|
|
39
|
+
|
|
40
|
+
if (showLabel) {
|
|
41
|
+
const labelEl = document.createElement('div');
|
|
42
|
+
labelEl.className = 'linear-progress-label';
|
|
43
|
+
labelEl.style.cssText = `
|
|
44
|
+
text-align: center;
|
|
45
|
+
margin-top: 0.5rem;
|
|
46
|
+
font-size: 0.875rem;
|
|
47
|
+
color: #666;
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
`;
|
|
50
|
+
labelEl.textContent = label || `${percentage}%`;
|
|
51
|
+
|
|
52
|
+
container.appendChild(bar);
|
|
53
|
+
container.appendChild(labelEl);
|
|
54
|
+
} else {
|
|
55
|
+
container.appendChild(bar);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
element: container,
|
|
60
|
+
setPercentage: (newPercentage) => {
|
|
61
|
+
const clamped = Math.min(100, Math.max(0, newPercentage));
|
|
62
|
+
fill.style.width = clamped + '%';
|
|
63
|
+
const labelEl = container.querySelector('.linear-progress-label');
|
|
64
|
+
if (labelEl) {
|
|
65
|
+
labelEl.textContent = label || `${clamped}%`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createCircularProgress(options = {}) {
|
|
72
|
+
const {
|
|
73
|
+
percentage = 0,
|
|
74
|
+
radius = 50,
|
|
75
|
+
strokeWidth = 4,
|
|
76
|
+
color = '#007bff',
|
|
77
|
+
backgroundColor = '#e0e0e0',
|
|
78
|
+
showLabel = true
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
const size = radius * 2 + strokeWidth;
|
|
82
|
+
const circumference = 2 * Math.PI * (radius - strokeWidth / 2);
|
|
83
|
+
|
|
84
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
85
|
+
svg.setAttribute('width', size);
|
|
86
|
+
svg.setAttribute('height', size);
|
|
87
|
+
svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
|
|
88
|
+
svg.style.cssText = `
|
|
89
|
+
transform: rotate(-90deg);
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
93
|
+
bgCircle.setAttribute('cx', size / 2);
|
|
94
|
+
bgCircle.setAttribute('cy', size / 2);
|
|
95
|
+
bgCircle.setAttribute('r', radius - strokeWidth / 2);
|
|
96
|
+
bgCircle.setAttribute('fill', 'none');
|
|
97
|
+
bgCircle.setAttribute('stroke', backgroundColor);
|
|
98
|
+
bgCircle.setAttribute('stroke-width', strokeWidth);
|
|
99
|
+
|
|
100
|
+
const progressCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
101
|
+
progressCircle.setAttribute('cx', size / 2);
|
|
102
|
+
progressCircle.setAttribute('cy', size / 2);
|
|
103
|
+
progressCircle.setAttribute('r', radius - strokeWidth / 2);
|
|
104
|
+
progressCircle.setAttribute('fill', 'none');
|
|
105
|
+
progressCircle.setAttribute('stroke', color);
|
|
106
|
+
progressCircle.setAttribute('stroke-width', strokeWidth);
|
|
107
|
+
progressCircle.setAttribute('stroke-dasharray', circumference);
|
|
108
|
+
progressCircle.setAttribute('stroke-linecap', 'round');
|
|
109
|
+
|
|
110
|
+
const offset = circumference - (percentage / 100) * circumference;
|
|
111
|
+
progressCircle.setAttribute('stroke-dashoffset', offset);
|
|
112
|
+
progressCircle.style.cssText = `
|
|
113
|
+
transition: stroke-dashoffset 0.3s ease;
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
svg.appendChild(bgCircle);
|
|
117
|
+
svg.appendChild(progressCircle);
|
|
118
|
+
|
|
119
|
+
const container = document.createElement('div');
|
|
120
|
+
container.className = 'circular-progress-container';
|
|
121
|
+
container.style.cssText = `
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 1rem;
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const svgWrapper = document.createElement('div');
|
|
129
|
+
svgWrapper.style.cssText = `
|
|
130
|
+
position: relative;
|
|
131
|
+
width: ${size}px;
|
|
132
|
+
height: ${size}px;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
svgWrapper.appendChild(svg);
|
|
139
|
+
|
|
140
|
+
if (showLabel) {
|
|
141
|
+
const label = document.createElement('div');
|
|
142
|
+
label.className = 'circular-progress-label';
|
|
143
|
+
label.style.cssText = `
|
|
144
|
+
position: absolute;
|
|
145
|
+
text-align: center;
|
|
146
|
+
font-size: 1.5rem;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
color: ${color};
|
|
149
|
+
`;
|
|
150
|
+
label.textContent = `${percentage}%`;
|
|
151
|
+
svgWrapper.appendChild(label);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
container.appendChild(svgWrapper);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
element: container,
|
|
158
|
+
setPercentage: (newPercentage) => {
|
|
159
|
+
const clamped = Math.min(100, Math.max(0, newPercentage));
|
|
160
|
+
const newOffset = circumference - (clamped / 100) * circumference;
|
|
161
|
+
progressCircle.setAttribute('stroke-dashoffset', newOffset);
|
|
162
|
+
|
|
163
|
+
const labelEl = svgWrapper.querySelector('.circular-progress-label');
|
|
164
|
+
if (labelEl) {
|
|
165
|
+
labelEl.textContent = `${clamped}%`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function renderProgressBar(percentage, label = '') {
|
|
172
|
+
const html = `
|
|
173
|
+
<div style="width: 100%; background: #e0e0e0; height: 8px; border-radius: 4px; overflow: hidden;">
|
|
174
|
+
<div style="height: 100%; width: ${percentage}%; background: #007bff; border-radius: 4px; transition: width 0.3s;"></div>
|
|
175
|
+
</div>
|
|
176
|
+
<div style="text-align: center; margin-top: 0.5rem; font-size: 0.875rem; color: #666;">
|
|
177
|
+
${label || percentage + '%'}
|
|
178
|
+
</div>
|
|
179
|
+
`;
|
|
180
|
+
return html;
|
|
181
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const stmtCache = new Map();
|
|
2
|
+
const resultCache = new Map();
|
|
3
|
+
const queryCache = new Map();
|
|
4
|
+
let cacheHits = 0;
|
|
5
|
+
let cacheMisses = 0;
|
|
6
|
+
let stmtCacheHits = 0;
|
|
7
|
+
let stmtCacheMisses = 0;
|
|
8
|
+
|
|
9
|
+
const MAX_STMT_CACHE = 500;
|
|
10
|
+
const MAX_RESULT_CACHE = 200;
|
|
11
|
+
const MAX_QUERY_CACHE = 100;
|
|
12
|
+
const DEFAULT_TTL = 30000;
|
|
13
|
+
|
|
14
|
+
const TTL_CONFIG = {
|
|
15
|
+
user: 60000,
|
|
16
|
+
users: 60000,
|
|
17
|
+
client: 120000,
|
|
18
|
+
engagement: 10000,
|
|
19
|
+
rfi: 5000,
|
|
20
|
+
audit_logs: 0,
|
|
21
|
+
sessions: 5000,
|
|
22
|
+
email: 1000,
|
|
23
|
+
default: DEFAULT_TTL
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const prepareStmt = (db, sql) => {
|
|
27
|
+
const cached = stmtCache.get(sql);
|
|
28
|
+
if (cached) {
|
|
29
|
+
stmtCacheHits++;
|
|
30
|
+
return cached;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
stmtCacheMisses++;
|
|
34
|
+
if (stmtCache.size >= MAX_STMT_CACHE) {
|
|
35
|
+
const firstKey = stmtCache.keys().next().value;
|
|
36
|
+
stmtCache.delete(firstKey);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const stmt = db.prepare(sql);
|
|
40
|
+
stmtCache.set(sql, stmt);
|
|
41
|
+
return stmt;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const getCached = (key, entity = 'default') => {
|
|
45
|
+
const entry = resultCache.get(key);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
cacheMisses++;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ttl = TTL_CONFIG[entity] || TTL_CONFIG.default;
|
|
52
|
+
if (ttl === 0) {
|
|
53
|
+
cacheMisses++;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Date.now() - entry.timestamp > ttl) {
|
|
58
|
+
resultCache.delete(key);
|
|
59
|
+
cacheMisses++;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
cacheHits++;
|
|
64
|
+
return entry.data;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const setCached = (key, data, entity = 'default') => {
|
|
68
|
+
const ttl = TTL_CONFIG[entity] || TTL_CONFIG.default;
|
|
69
|
+
if (ttl === 0) return;
|
|
70
|
+
|
|
71
|
+
if (resultCache.size >= MAX_RESULT_CACHE) {
|
|
72
|
+
const firstKey = resultCache.keys().next().value;
|
|
73
|
+
resultCache.delete(firstKey);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resultCache.set(key, { data, timestamp: Date.now() });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const invalidate = (pattern) => {
|
|
80
|
+
if (!pattern) {
|
|
81
|
+
resultCache.clear();
|
|
82
|
+
queryCache.clear();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const regex = new RegExp(pattern);
|
|
87
|
+
for (const key of resultCache.keys()) {
|
|
88
|
+
if (regex.test(key)) resultCache.delete(key);
|
|
89
|
+
}
|
|
90
|
+
for (const key of queryCache.keys()) {
|
|
91
|
+
if (regex.test(key)) queryCache.delete(key);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const cacheQuery = (key, fn) => {
|
|
96
|
+
const cached = queryCache.get(key);
|
|
97
|
+
if (cached) return cached;
|
|
98
|
+
|
|
99
|
+
if (queryCache.size >= MAX_QUERY_CACHE) {
|
|
100
|
+
const firstKey = queryCache.keys().next().value;
|
|
101
|
+
queryCache.delete(firstKey);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = fn();
|
|
105
|
+
queryCache.set(key, result);
|
|
106
|
+
return result;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const getStats = () => ({
|
|
110
|
+
cacheHits,
|
|
111
|
+
cacheMisses,
|
|
112
|
+
hitRate: cacheHits + cacheMisses > 0 ? (cacheHits / (cacheHits + cacheMisses) * 100).toFixed(2) + '%' : '0%',
|
|
113
|
+
stmtCacheHits,
|
|
114
|
+
stmtCacheMisses,
|
|
115
|
+
stmtHitRate: stmtCacheHits + stmtCacheMisses > 0 ? (stmtCacheHits / (stmtCacheHits + stmtCacheMisses) * 100).toFixed(2) + '%' : '0%',
|
|
116
|
+
stmtCacheSize: stmtCache.size,
|
|
117
|
+
resultCacheSize: resultCache.size,
|
|
118
|
+
queryCacheSize: queryCache.size
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const clearStats = () => {
|
|
122
|
+
cacheHits = 0;
|
|
123
|
+
cacheMisses = 0;
|
|
124
|
+
stmtCacheHits = 0;
|
|
125
|
+
stmtCacheMisses = 0;
|
|
126
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
}
|