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,445 @@
1
+ /**
2
+ * Config Generator Engine - Resolves entity specs from YAML config
3
+ * Core of Thatcher's configuration-driven architecture
4
+ * Adapted from moonlanding/src/lib/config-generator-engine.js
5
+ */
6
+
7
+ import yaml from 'js-yaml';
8
+ import { LRUCache, deepFreeze, deepClone, recursiveResolve } from './config-helpers.js';
9
+ import { generateFieldsFromOverrides, ensureFieldLabels } from './config-field-helpers.js';
10
+
11
+ export class ConfigGeneratorEngine {
12
+ constructor(masterConfig) {
13
+ if (!masterConfig) throw new Error('[ConfigGeneratorEngine] masterConfig is required');
14
+ this.masterConfig = deepFreeze(masterConfig);
15
+ this.specCache = new LRUCache(100);
16
+ this._plugins = new Map();
17
+ }
18
+
19
+ /**
20
+ * Register a plugin to extend an entity
21
+ */
22
+ registerPlugin(entityName, plugin = {}) {
23
+ if (!entityName || typeof entityName !== 'string') {
24
+ throw new Error('[ConfigGeneratorEngine] registerPlugin: entityName required');
25
+ }
26
+
27
+ const existing = this._plugins.get(entityName) || { fields: {}, hooks: [], validators: [] };
28
+
29
+ this._plugins.set(entityName, {
30
+ fields: { ...existing.fields, ...(plugin.fields || {}) },
31
+ hooks: [...existing.hooks, ...(plugin.hooks || [])],
32
+ validators: [...existing.validators, ...(plugin.validators || [])],
33
+ });
34
+
35
+ this.specCache.clear();
36
+
37
+ // Register hooks with global hook engine if available
38
+ for (const { event, handler } of (plugin.hooks || [])) {
39
+ if (event && handler && globalThis.__hookEngine) {
40
+ globalThis.__hookEngine.register(event, handler);
41
+ }
42
+ }
43
+
44
+ if (globalThis.__debug__) {
45
+ globalThis.__debug__.expose('plugins', Object.fromEntries(this._plugins.entries()), 'Plugin registry');
46
+ }
47
+
48
+ return this;
49
+ }
50
+
51
+ getPlugins(entityName) {
52
+ return this._plugins.get(entityName) || null;
53
+ }
54
+
55
+ getConfig() {
56
+ return deepFreeze(deepClone(this.masterConfig));
57
+ }
58
+
59
+ getRoles() {
60
+ return this._cached('roles:all', () => deepFreeze(deepClone(this.masterConfig.roles || {})));
61
+ }
62
+
63
+ getRole(roleName) {
64
+ return this._cached(`role:${roleName}`, () => deepFreeze(deepClone(this._require('roles', roleName))));
65
+ }
66
+
67
+ getDomains() {
68
+ return deepFreeze(deepClone(this.masterConfig.domains || {}));
69
+ }
70
+
71
+ getHighlightPalette() {
72
+ return deepFreeze(deepClone(this.masterConfig.highlights?.palette || []));
73
+ }
74
+
75
+ getSystemConfig() {
76
+ return deepFreeze(deepClone(this.masterConfig.system || {}));
77
+ }
78
+
79
+ getRepeatIntervals() {
80
+ return deepFreeze(deepClone(this.masterConfig.repeat_intervals || { once: 'once', monthly: 'monthly', yearly: 'yearly' }));
81
+ }
82
+
83
+ getAllEntities() {
84
+ return Object.keys(this.masterConfig.entities || {}).sort();
85
+ }
86
+
87
+ getAllAutomations() {
88
+ return deepClone(this.masterConfig.automation?.schedules || []);
89
+ }
90
+
91
+ getTheme() {
92
+ return this._cached('theme:all', () => deepFreeze(deepClone(this.masterConfig.theme || {})));
93
+ }
94
+
95
+ getIcons() {
96
+ return this._cached('icons:all', () => deepFreeze(deepClone(this.masterConfig.icons || {})));
97
+ }
98
+
99
+ getIcon(iconType, iconName) {
100
+ return this.masterConfig.icons?.[iconType]?.[iconName] || null;
101
+ }
102
+
103
+ getWorkflow(name) {
104
+ return this._resolved('workflow', 'workflows', name);
105
+ }
106
+
107
+ getPermissionTemplate(name) {
108
+ return deepFreeze(deepClone(this._require('permission_templates', name)));
109
+ }
110
+
111
+ getStatusEnum(name) {
112
+ return deepFreeze(deepClone(this._require('status_enums', name)));
113
+ }
114
+
115
+ getStatuses(name) {
116
+ return this._cached(`statuses:${name}`, () => deepFreeze(deepClone(this._require('status_enums', name))));
117
+ }
118
+
119
+ getStatus(entity, status) {
120
+ return this._cached(`status:${entity}:${status}`, () => deepFreeze(deepClone(this._require(`status_enums.${entity}_status`, status))));
121
+ }
122
+
123
+ getEntity(name) {
124
+ return this._cached(`entity:${name}`, () => deepFreeze(deepClone(this._require('entities', name))));
125
+ }
126
+
127
+ getValidationRule(name) {
128
+ return this._resolved('validation rule', 'validation', name);
129
+ }
130
+
131
+ getDocumentTemplate(name) {
132
+ return this._resolved('document template', 'document_generation.templates', name);
133
+ }
134
+
135
+ getIntegration(name) {
136
+ return deepFreeze(deepClone(this._require('integrations', name)));
137
+ }
138
+
139
+ getThreshold(dotPath) {
140
+ const segments = dotPath.split('.');
141
+ let cur = this.masterConfig.thresholds;
142
+ for (const s of segments) {
143
+ if (cur == null) throw new Error(`[config] Threshold "${dotPath}" not found`);
144
+ cur = cur[s];
145
+ }
146
+ if (cur === undefined) throw new Error(`[config] Threshold "${dotPath}" not found`);
147
+ return typeof cur === 'object' ? deepFreeze(deepClone(cur)) : cur;
148
+ }
149
+
150
+ getTimingConfig() {
151
+ return this._cached('thresholds:timing', () => deepFreeze(deepClone(this.getThreshold('timing'))));
152
+ }
153
+
154
+ getSystemLimitsConfig() {
155
+ return this._cached('thresholds:system', () => deepFreeze(deepClone(this.getThreshold('system'))));
156
+ }
157
+
158
+ getUIConfig() {
159
+ return this._cached('thresholds:ui', () => deepFreeze(deepClone(this.getThreshold('ui'))));
160
+ }
161
+
162
+ getPollingConfig() {
163
+ return this._cached('thresholds:polling', () => deepFreeze(deepClone(this.getThreshold('polling'))));
164
+ }
165
+
166
+ getWorkflowStages(wfName) {
167
+ return (this.masterConfig.workflows?.[wfName]?.stages || []).map(s => typeof s === 'string' ? s : s.name);
168
+ }
169
+
170
+ getStageConfig(wfName, stageName) {
171
+ const stage = (this.masterConfig.workflows?.[wfName]?.stages || []).find(
172
+ s => (typeof s === 'string' ? s : s.name) === stageName
173
+ );
174
+ if (!stage) return null;
175
+ return deepFreeze(recursiveResolve(deepClone(typeof stage === 'string' ? { name: stage } : stage), this.masterConfig));
176
+ }
177
+
178
+ getEntitiesForDomain(domain) {
179
+ return [...(this._require('domains', domain).entities || [])];
180
+ }
181
+
182
+ isFeatureEnabled(name, ctx = {}) {
183
+ const f = this.masterConfig.features?.[name];
184
+ if (!f || f.enabled === false || f.deprecated) return false;
185
+ if (f.domain && ctx.domain && f.domain !== ctx.domain) return false;
186
+ if (f.workflow_stage && ctx.stage && f.workflow_stage !== ctx.stage) return false;
187
+ if (f.roles && ctx.role && !f.roles.includes(ctx.role)) return false;
188
+ if (f.requires && ctx.features && f.requires.some(r => !ctx.features.includes(r))) return false;
189
+ return true;
190
+ }
191
+
192
+ getFieldValue(entity, field) {
193
+ const spec = this.generateEntitySpec(entity);
194
+ if (!spec.fields?.[field]) throw new Error(`[config] Field "${field}" not found for entity "${entity}"`);
195
+ return deepFreeze(deepClone(spec.fields[field]));
196
+ }
197
+
198
+ getRolePermissions(role, entity) {
199
+ return [...(this.generateEntitySpec(entity).permissions?.[role] || [])];
200
+ }
201
+
202
+ canRoleDoAction(role, entity, action) {
203
+ const p = this.getRolePermissions(role, entity);
204
+ return p.includes(action) || p.includes('all');
205
+ }
206
+
207
+ generateNotificationHandler(name) {
208
+ return deepFreeze(recursiveResolve(deepClone(this._require('notifications', name)), this.masterConfig));
209
+ }
210
+
211
+ generateAutomationJob(name) {
212
+ const s = (this.masterConfig.automation?.schedules || []).find(s => s.name === name);
213
+ if (!s) throw new Error(`[config] Automation schedule "${name}" not found`);
214
+ return deepFreeze(recursiveResolve(deepClone(s), this.masterConfig));
215
+ }
216
+
217
+ cacheSpec(entityName, spec) {
218
+ if (!entityName || !spec) throw new Error('[config] cacheSpec requires entityName and spec');
219
+ this.specCache.set(`spec:${entityName}`, deepFreeze(spec));
220
+ return true;
221
+ }
222
+
223
+ invalidateCache() {
224
+ this.specCache.clear();
225
+ this.masterConfig = null;
226
+ return true;
227
+ }
228
+
229
+ enableDebug(enabled = true) {
230
+ this.debugMode = enabled;
231
+ return this;
232
+ }
233
+
234
+ /**
235
+ * Generate complete entity specification from config
236
+ * This is the main method that builds the spec used by CRUD, UI, etc.
237
+ */
238
+ generateEntitySpec(entityName) {
239
+ if (!entityName) throw new Error('[config] generateEntitySpec: entityName required');
240
+
241
+ const cacheKey = `spec:${entityName}`;
242
+ const cached = this.specCache.get(cacheKey);
243
+ if (cached) return deepFreeze(deepClone(cached));
244
+
245
+ const config = this.masterConfig;
246
+ let name = entityName;
247
+ let entityDef = config.entities?.[entityName] || (() => {
248
+ const match = Object.keys(config.entities || {}).find(k => {
249
+ const d = config.entities[k];
250
+ return (d.label_plural || d.label || k).toLowerCase() === entityName.toLowerCase();
251
+ });
252
+ if (match) {
253
+ name = match;
254
+ return config.entities[match];
255
+ }
256
+ })();
257
+
258
+ if (!entityDef) throw new Error(`[config] Entity "${entityName}" not found`);
259
+
260
+ entityDef = deepClone(entityDef);
261
+ const childrenObj = {};
262
+ (entityDef.children || []).forEach(c => { childrenObj[c] = { entity: c }; });
263
+
264
+ const spec = {
265
+ name,
266
+ label: entityDef.label || name,
267
+ labelPlural: entityDef.label_plural || entityDef.label || entityName,
268
+ icon: entityDef.icon || 'Circle',
269
+ order: entityDef.order || 999,
270
+ parent: entityDef.parent || null,
271
+ children: childrenObj,
272
+ computed_fields: entityDef.computed_fields || [],
273
+ has_timeline: entityDef.has_timeline || false,
274
+ has_roles: entityDef.has_roles || [],
275
+ has_pdf_viewer: entityDef.has_pdf_viewer || false,
276
+ has_collaboration: entityDef.has_collaboration || false,
277
+ has_tender_tracking: entityDef.has_tender_tracking || false,
278
+ has_notifications: entityDef.has_notifications || false,
279
+ has_authentication: entityDef.has_authentication || false,
280
+ has_google_sync: entityDef.has_google_sync || false,
281
+ recreation_enabled: entityDef.recreation_enabled || false,
282
+ recreation_intervals: entityDef.recreation_intervals || [],
283
+ immutable: entityDef.immutable || false,
284
+ immutable_strategy: entityDef.immutable_strategy || null,
285
+ state_machine: entityDef.state_machine || false,
286
+ row_access: entityDef.row_access || null,
287
+ embedded: entityDef.embedded || false,
288
+ system_entity: entityDef.system_entity || false,
289
+ fields: {},
290
+ options: {},
291
+ };
292
+
293
+ // Merge field overrides
294
+ if (entityDef.field_overrides) {
295
+ spec.field_overrides = recursiveResolve(entityDef.field_overrides, config);
296
+ }
297
+
298
+ // Build fields from definition + overrides + plugin additions
299
+ const baseFields = entityDef.fields || {};
300
+ const plugin = this._plugins.get(entityName);
301
+ const pluginFields = plugin?.fields || {};
302
+
303
+ // Combine and process fields
304
+ const allFields = {
305
+ ...baseFields,
306
+ ...spec.field_overrides,
307
+ ...pluginFields,
308
+ };
309
+
310
+ // Ensure required metadata (label, type defaults)
311
+ for (const [key, field] of Object.entries(allFields)) {
312
+ spec.fields[key] = {
313
+ type: field.type || 'text',
314
+ label: field.label || key,
315
+ required: field.required || false,
316
+ unique: field.unique || false,
317
+ readonly: field.readonly || field.readOnly || false,
318
+ hidden: field.hidden || false,
319
+ searchable: field.search || field.searchable || false,
320
+ sortable: field.sortable || field.sort || false,
321
+ default: field.default !== undefined ? field.default : null,
322
+ options: field.options || [],
323
+ ref: field.ref || null,
324
+ display: field.display || null,
325
+ auto: field.auto || null,
326
+ min: field.min,
327
+ max: field.max,
328
+ ...field,
329
+ };
330
+ }
331
+
332
+ // Permission matrix
333
+ if (entityDef.permission_template) {
334
+ const matrix = this.getPermissionTemplate(entityDef.permission_template);
335
+ const access = {};
336
+ const roleActions = {};
337
+
338
+ for (const [role, actions] of Object.entries(matrix)) {
339
+ if (!Array.isArray(actions)) continue;
340
+ roleActions[role] = [...actions];
341
+ actions.forEach(a => {
342
+ if (!access[a]) access[a] = [];
343
+ access[a].push(role);
344
+ });
345
+ }
346
+
347
+ spec.access = access;
348
+ spec.permissions = roleActions;
349
+ }
350
+
351
+ // Workflow binding
352
+ if (entityDef.workflow) {
353
+ const wf = this.getWorkflow(entityDef.workflow);
354
+ spec.workflow = wf;
355
+ spec.workflowDef = wf;
356
+ if (wf?.stages && Array.isArray(wf.stages)) {
357
+ const stagesObj = {};
358
+ wf.stages.forEach(s => {
359
+ stagesObj[typeof s === 'string' ? s : s.name] = s;
360
+ });
361
+ entityDef.stages = stagesObj;
362
+ }
363
+ }
364
+
365
+ // List options
366
+ if (entityDef.list) {
367
+ spec.list = {
368
+ defaultSort: entityDef.list.defaultSort || { field: 'created_at', dir: 'desc' },
369
+ pageSize: entityDef.list.pageSize || 50,
370
+ filters: entityDef.list.filters || [],
371
+ ...entityDef.list,
372
+ };
373
+ }
374
+
375
+ this.specCache.set(cacheKey, deepFreeze(spec));
376
+ return spec;
377
+ }
378
+
379
+ // Internal helpers
380
+
381
+ _cached(key, fn) {
382
+ const cached = this.specCache.get(key);
383
+ if (cached) return deepFreeze(deepClone(cached));
384
+ const value = fn();
385
+ this.specCache.set(key, value);
386
+ return value;
387
+ }
388
+
389
+ _require(section, key) {
390
+ const sectionData = this.masterConfig[section];
391
+ if (!sectionData) throw new Error(`[config] Missing section: ${section}`);
392
+ const value = sectionData[key];
393
+ if (value === undefined) throw new Error(`[config] Key "${key}" not found in ${section}`);
394
+ return value;
395
+ }
396
+
397
+ _resolved(type, section, key) {
398
+ const value = this._require(section, key);
399
+ return deepFreeze(recursiveResolve(deepClone(value), this.masterConfig));
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Get or create singleton config engine
405
+ */
406
+ let _singleton = null;
407
+
408
+ export function getConfigEngineSync() {
409
+ if (!_singleton) {
410
+ throw new Error('ConfigEngine not initialized. Call initConfig() first.');
411
+ }
412
+ return _singleton;
413
+ }
414
+
415
+ export async function getConfigEngine() {
416
+ return getConfigEngineSync();
417
+ }
418
+
419
+ /**
420
+ * Initialize config engine from config source
421
+ * @param {string|object} configSource
422
+ * @returns {ConfigGeneratorEngine}
423
+ */
424
+ export async function initConfig(configSource) {
425
+ const { loadConfig } = await import('../config/config-loader.js');
426
+ const { validateConfig, getConfigWithDefaults } = await import('../config/config-loader.js');
427
+
428
+ const rawConfig = await loadConfig(configSource);
429
+ const config = getConfigWithDefaults(rawConfig);
430
+ const errors = validateConfig(config);
431
+
432
+ if (errors.length > 0) {
433
+ throw new Error(`Configuration validation failed:\n- ${errors.join('\n- ')}`);
434
+ }
435
+
436
+ _singleton = new ConfigGeneratorEngine(config);
437
+ return _singleton;
438
+ }
439
+
440
+ /**
441
+ * Reset singleton (for hot reload)
442
+ */
443
+ export function resetConfigEngine() {
444
+ _singleton = null;
445
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Configuration Helpers - Utilities for config processing
3
+ * LRUCache, deep cloning, freezing, recursive resolution
4
+ */
5
+
6
+ /**
7
+ * LRU Cache implementation
8
+ */
9
+ export class LRUCache {
10
+ constructor(maxSize = 100) {
11
+ this.maxSize = maxSize;
12
+ this.cache = new Map();
13
+ }
14
+
15
+ get(key) {
16
+ if (!this.cache.has(key)) return undefined;
17
+ const value = this.cache.get(key);
18
+ this.cache.delete(key);
19
+ this.cache.set(key, value);
20
+ return value;
21
+ }
22
+
23
+ set(key, value) {
24
+ if (this.cache.has(key)) {
25
+ this.cache.delete(key);
26
+ } else if (this.cache.size >= this.maxSize) {
27
+ const firstKey = this.cache.keys().next().value;
28
+ this.cache.delete(firstKey);
29
+ }
30
+ this.cache.set(key, value);
31
+ }
32
+
33
+ clear() {
34
+ this.cache.clear();
35
+ }
36
+
37
+ get size() {
38
+ return this.cache.size;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Deep freeze object (immutable)
44
+ */
45
+ export function deepFreeze(obj) {
46
+ if (obj === null || typeof obj !== 'object') return obj;
47
+ if (Object.isFrozen(obj)) return obj;
48
+
49
+ Object.freeze(obj);
50
+ Object.getOwnPropertyNames(obj).forEach(prop => {
51
+ if (obj[prop] && typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
52
+ deepFreeze(obj[prop]);
53
+ }
54
+ });
55
+ return obj;
56
+ }
57
+
58
+ /**
59
+ * Deep clone object
60
+ */
61
+ export function deepClone(obj) {
62
+ if (obj === null || typeof obj !== 'object') return obj;
63
+ if (Array.isArray(obj)) return obj.map(deepClone);
64
+
65
+ const cloned = {};
66
+ for (const key of Object.keys(obj)) {
67
+ cloned[key] = deepClone(obj[key]);
68
+ }
69
+ return cloned;
70
+ }
71
+
72
+ /**
73
+ * Recursively resolve string template references in config
74
+ * Replaces ${ref.path} with value from config
75
+ */
76
+ export function recursiveResolve(value, config) {
77
+ if (typeof value === 'string') {
78
+ return value.replace(/\$\{([^}]+)\}/g, (_, path) => {
79
+ const segments = path.split('.');
80
+ let current = config;
81
+ for (const seg of segments) {
82
+ if (current && typeof current === 'object' && seg in current) {
83
+ current = current[seg];
84
+ } else {
85
+ return _; // Keep original if not found
86
+ }
87
+ }
88
+ return typeof current === 'undefined' ? _ : current;
89
+ });
90
+ }
91
+
92
+ if (Array.isArray(value)) {
93
+ return value.map(item => recursiveResolve(item, config));
94
+ }
95
+
96
+ if (value && typeof value === 'object') {
97
+ const resolved = {};
98
+ for (const [k, v] of Object.entries(value)) {
99
+ resolved[k] = recursiveResolve(v, config);
100
+ }
101
+ return resolved;
102
+ }
103
+
104
+ return value;
105
+ }
106
+
107
+ /**
108
+ * Merge objects deeply
109
+ */
110
+ export function deepMerge(target, source) {
111
+ const output = { ...target };
112
+ for (const key of Object.keys(source)) {
113
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
114
+ output[key] = deepMerge(target[key] || {}, source[key]);
115
+ } else {
116
+ output[key] = source[key];
117
+ }
118
+ }
119
+ return output;
120
+ }
@@ -0,0 +1,79 @@
1
+ class ConnectionGuard {
2
+ constructor(config) {
3
+ this.config = config
4
+ this.ipConnections = new Map()
5
+ this.messageRates = new Map()
6
+ this.pingIntervalId = null
7
+ }
8
+
9
+ checkConnectionLimit(ip) {
10
+ const count = this.ipConnections.get(ip) || 0
11
+ return count < this.config.maxConnectionsPerIP
12
+ }
13
+
14
+ trackIPConnection(ip) {
15
+ this.ipConnections.set(ip, (this.ipConnections.get(ip) || 0) + 1)
16
+ }
17
+
18
+ untrackIPConnection(ip) {
19
+ const count = this.ipConnections.get(ip) || 0
20
+ if (count <= 1) {
21
+ this.ipConnections.delete(ip)
22
+ } else {
23
+ this.ipConnections.set(ip, count - 1)
24
+ }
25
+ }
26
+
27
+ checkRateLimit(clientId) {
28
+ const now = Date.now()
29
+ const rate = this.messageRates.get(clientId) || { count: 0, window: now }
30
+
31
+ if (now - rate.window > 1000) {
32
+ rate.count = 1
33
+ rate.window = now
34
+ } else {
35
+ rate.count++
36
+ }
37
+
38
+ this.messageRates.set(clientId, rate)
39
+ return rate.count <= this.config.messageRateLimit
40
+ }
41
+
42
+ clearRateLimit(clientId) {
43
+ this.messageRates.delete(clientId)
44
+ }
45
+
46
+ startPingInterval(clients, onDead) {
47
+ this.pingIntervalId = setInterval(() => {
48
+ for (const [clientId, client] of clients) {
49
+ if (!client.alive) {
50
+ client.ws.terminate()
51
+ onDead(clientId)
52
+ } else {
53
+ client.alive = false
54
+ client.ws.ping()
55
+ }
56
+ }
57
+ }, this.config.pingInterval)
58
+ }
59
+
60
+ stopPingInterval() {
61
+ if (this.pingIntervalId) {
62
+ clearInterval(this.pingIntervalId)
63
+ this.pingIntervalId = null
64
+ }
65
+ }
66
+
67
+ generateClientId() {
68
+ return `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
69
+ }
70
+
71
+ destroy() {
72
+ this.stopPingInterval()
73
+ this.ipConnections.clear()
74
+ this.messageRates.clear()
75
+ }
76
+ }
77
+
78
+ export { ConnectionGuard }
79
+ export default ConnectionGuard
@@ -0,0 +1,34 @@
1
+ import { AppError } from '@/lib/errors';
2
+ import { HTTP } from '@/config/constants';
3
+ import { now } from '@/lib/database-core';
4
+
5
+ const ACTION_FIELD_MAPS = {
6
+ resolve_highlight: { status: 'resolved', color: 'green' },
7
+ reopen_highlight: { status: 'unresolved', color: 'grey', resolved_at: null, resolved_by: null, resolution_notes: null },
8
+ change_status: (data, user) => ({ status: data.status || data.toStatus, status_changed_at: now(), status_changed_by: user.id }),
9
+ respond: (data, user) => ({ status: 'responded', response: data.response, responded_at: now(), responded_by: user.id }),
10
+ };
11
+
12
+ export function resolveActionUpdate(action, data, user, record) {
13
+ const mapped = ACTION_FIELD_MAPS[action];
14
+ if (typeof mapped === 'function') return mapped(data, user);
15
+ if (mapped) {
16
+ const result = { ...mapped };
17
+ if (action === 'resolve_highlight') {
18
+ result.resolved_at = now();
19
+ result.resolved_by = user.id;
20
+ result.resolution_notes = data.notes || data.resolution_notes || '';
21
+ }
22
+ return result;
23
+ }
24
+ return data;
25
+ }
26
+
27
+ export function validateActionPrecondition(action, record) {
28
+ if (action === 'resolve_highlight' && record.status === 'resolved') {
29
+ throw new AppError('Highlight already resolved', 'BAD_REQUEST', HTTP.BAD_REQUEST);
30
+ }
31
+ if (action === 'reopen_highlight' && record.status !== 'resolved') {
32
+ throw new AppError('Highlight not resolved', 'BAD_REQUEST', HTTP.BAD_REQUEST);
33
+ }
34
+ }