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.
Files changed (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. 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
+ }