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
package/src/index.js ADDED
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Thatcher SDK - Configuration-Driven Application Framework
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { fileURLToPath, pathToFileURL } from 'url';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ // Forward-declare so we can set later
12
+ let _configEngine = null;
13
+ let _databaseInitialized = false;
14
+ let _pluginsLoaded = false;
15
+ let _server = null;
16
+ let _hotReloadWatchers = [];
17
+
18
+ /**
19
+ * Resolve a path relative to this package's src directory as an import URL
20
+ * @param {string} relative - Path like './src/lib/database-core.js'
21
+ * @returns {string} file:// URL
22
+ */
23
+ function resolveModule(relative) {
24
+ const abs = path.resolve(__dirname, relative);
25
+ return pathToFileURL(abs).href;
26
+ }
27
+
28
+ /**
29
+ * Thatcher class - main API
30
+ */
31
+ export class Thatcher {
32
+ constructor(options = {}) {
33
+ this.options = this.normalizeOptions(options);
34
+ this.config = null;
35
+ this.initialized = false;
36
+ this.started = false;
37
+ }
38
+
39
+ normalizeOptions(options) {
40
+ return {
41
+ config: options.config || null,
42
+ databasePath: options.databasePath || path.resolve(process.cwd(), 'data', 'app.db'),
43
+ env: options.env || {},
44
+ plugins: options.plugins || [],
45
+ server: {
46
+ port: options.server?.port || parseInt(process.env.PORT || '3000', 10),
47
+ host: options.server?.host || '0.0.0.0',
48
+ hotReload: options.server?.hotReload !== false,
49
+ },
50
+ ui: options.ui !== false,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Initialize the system (config, DB, plugins)
56
+ */
57
+ async init() {
58
+ if (this.initialized) return this;
59
+
60
+ // Apply env overrides
61
+ Object.assign(process.env, this.options.env);
62
+
63
+ // Load configuration
64
+ await this.loadConfig();
65
+
66
+ // Initialize database
67
+ await this.initDatabase();
68
+
69
+ // Load plugins
70
+ await this.loadPlugins();
71
+
72
+ // Hot reload
73
+ if (this.options.server.hotReload) {
74
+ this.setupHotReload();
75
+ }
76
+
77
+ this.initialized = true;
78
+ return this;
79
+ }
80
+
81
+ /**
82
+ * Load master configuration
83
+ */
84
+ async loadConfig() {
85
+ const { config } = this.options;
86
+ let masterConfig;
87
+
88
+ if (typeof config === 'string') {
89
+ const configPath = path.resolve(process.cwd(), config);
90
+ if (!fs.existsSync(configPath)) {
91
+ throw new Error(`Configuration file not found: ${configPath}`);
92
+ }
93
+ const content = fs.readFileSync(configPath, 'utf-8');
94
+ const { default: yaml } = await import('js-yaml');
95
+ masterConfig = yaml.load(content);
96
+ } else if (typeof config === 'object') {
97
+ masterConfig = config;
98
+ } else {
99
+ // Auto-discover
100
+ const defaultPaths = [
101
+ path.resolve(process.cwd(), 'master-config.yml'),
102
+ path.resolve(process.cwd(), 'thatcher.config.yml'),
103
+ ];
104
+ for (const p of defaultPaths) {
105
+ if (fs.existsSync(p)) {
106
+ this.options.config = p;
107
+ const content = fs.readFileSync(p, 'utf-8');
108
+ const { default: yaml } = await import('js-yaml');
109
+ masterConfig = yaml.load(content);
110
+ break;
111
+ }
112
+ }
113
+ }
114
+
115
+ if (!masterConfig) {
116
+ throw new Error('No configuration provided. Supply config path or object.');
117
+ }
118
+
119
+ // Init config engine
120
+ const { ConfigGeneratorEngine } = await import(resolveModule('./src/lib/config-generator-engine.js'));
121
+ _configEngine = new ConfigGeneratorEngine(masterConfig);
122
+
123
+ // Debug exposure
124
+ if (globalThis.__debug__) {
125
+ globalThis.__debug__.expose('configEngine', _configEngine, 'Config Engine');
126
+ }
127
+
128
+ this.config = masterConfig;
129
+ }
130
+
131
+ /**
132
+ * Initialize database with schema from config
133
+ */
134
+ async initDatabase() {
135
+ if (_databaseInitialized) return;
136
+ const { migrate } = await import(resolveModule('./lib/database-core.js'));
137
+ migrate(_configEngine);
138
+ const { getDatabase } = await import(resolveModule('./lib/database-core.js'));
139
+ getDatabase(); // prime the connection
140
+ _databaseInitialized = true;
141
+ }
142
+
143
+ /**
144
+ * Load .plugin.js files
145
+ */
146
+ async loadPlugins() {
147
+ if (_pluginsLoaded) return;
148
+
149
+ // User-supplied plugins
150
+ for (const plugin of this.options.plugins) {
151
+ if (_configEngine && plugin.entityName) {
152
+ _configEngine.registerPlugin(plugin.entityName, plugin);
153
+ }
154
+ }
155
+
156
+ // Auto-discover in cwd/plugins and cwd/src/plugins
157
+ const pluginDirs = [
158
+ path.resolve(process.cwd(), 'plugins'),
159
+ path.resolve(process.cwd(), 'src/plugins'),
160
+ ];
161
+ for (const dir of pluginDirs) {
162
+ if (fs.existsSync(dir)) {
163
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.plugin.js'));
164
+ for (const file of files) {
165
+ try {
166
+ const mod = await import(`file://${path.join(dir, file)}?t=${Date.now()}`);
167
+ const plugin = mod.default || mod;
168
+ if (plugin.entityName) {
169
+ _configEngine.registerPlugin(plugin.entityName, plugin);
170
+ }
171
+ } catch (e) {
172
+ console.error(`[plugins] Failed to load ${file}:`, e.message);
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ _pluginsLoaded = true;
179
+ }
180
+
181
+ /**
182
+ * Setup hot reload watchers
183
+ */
184
+ setupHotReload() {
185
+ const dirs = [
186
+ path.resolve(process.cwd(), 'config'),
187
+ path.resolve(process.cwd(), 'api'),
188
+ path.resolve(process.cwd(), 'ui'),
189
+ path.resolve(process.cwd(), 'plugins'),
190
+ ];
191
+
192
+ for (const dir of dirs) {
193
+ if (!fs.existsSync(dir)) continue;
194
+ try {
195
+ const watcher = fs.watch(dir, { recursive: true }, (_, filename) => {
196
+ if (filename && (filename.endsWith('.js') || filename.endsWith('.jsx') || filename.endsWith('.yml'))) {
197
+ console.log(`[HotReload] ${filename} changed`);
198
+ this.invalidateCache();
199
+ }
200
+ });
201
+ _hotReloadWatchers.push(watcher);
202
+ } catch (err) {
203
+ console.warn(`[HotReload] Failed to watch ${dir}:`, err.message);
204
+ }
205
+ }
206
+ }
207
+
208
+ invalidateCache() {
209
+ if (_configEngine) {
210
+ _configEngine.invalidateCache();
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Start HTTP server
216
+ */
217
+ async startServer(opts = {}) {
218
+ if (!this.initialized) throw new Error('Call init() first');
219
+
220
+ const port = opts.port || this.options.server.port;
221
+ const host = opts.host || this.options.server.host;
222
+
223
+ const { createServer } = await import(resolveModule('./src/server/server.js'));
224
+ _server = createServer({
225
+ thatcher: this,
226
+ config: this.config,
227
+ configEngine: _configEngine,
228
+ port,
229
+ host,
230
+ });
231
+
232
+ return new Promise((resolve) => {
233
+ _server.listen(port, host, () => {
234
+ console.log(`\n▲ Thatcher Server\n- Local: http://localhost:${port}\n✓ Ready\n`);
235
+ resolve(_server);
236
+ });
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Stop server
242
+ */
243
+ async stop() {
244
+ if (_server) {
245
+ _server.close();
246
+ _server = null;
247
+ }
248
+ for (const w of _hotReloadWatchers) w.close();
249
+ _hotReloadWatchers = [];
250
+ }
251
+
252
+ /**
253
+ * Get config engine
254
+ */
255
+ getConfigEngine() {
256
+ return _configEngine;
257
+ }
258
+
259
+ /**
260
+ * Get entity spec
261
+ */
262
+ getEntitySpec(name) {
263
+ return _configEngine?.generateEntitySpec(name);
264
+ }
265
+
266
+ /**
267
+ * List all entities
268
+ */
269
+ getAllEntities() {
270
+ return _configEngine?.getAllEntities() || [];
271
+ }
272
+
273
+ /**
274
+ * Get workflow definition
275
+ */
276
+ getWorkflow(name) {
277
+ return _configEngine?.getWorkflow(name);
278
+ }
279
+
280
+ // === CRUD operations ===
281
+
282
+ async list(entity, where = {}, opts = {}) {
283
+ const { list } = await import(resolveModule('./src/lib/query-engine.js'));
284
+ return list(entity, where, opts);
285
+ }
286
+
287
+ async get(entity, id) {
288
+ const { get } = await import(resolveModule('./src/lib/query-engine.js'));
289
+ return get(entity, id);
290
+ }
291
+
292
+ async create(entity, data, user) {
293
+ const { create } = await import(resolveModule('./src/lib/query-engine-write.js'));
294
+ return create(entity, data, user);
295
+ }
296
+
297
+ async update(entity, id, data, user) {
298
+ const { update } = await import(resolveModule('./src/lib/query-engine-write.js'));
299
+ return update(entity, id, data, user);
300
+ }
301
+
302
+ async delete(entity, id) {
303
+ const { remove } = await import(resolveModule('./src/lib/query-engine-write.js'));
304
+ return remove(entity, id);
305
+ }
306
+
307
+ async search(entity, query, where = {}, opts = {}) {
308
+ const { search } = await import(resolveModule('./src/lib/query-engine.js'));
309
+ return search(entity, query, where, opts);
310
+ }
311
+
312
+ // === Workflow ===
313
+
314
+ async transition(entityType, entityId, workflowName, toState, user, reason = '') {
315
+ const { transition } = await import(resolveModule('./src/lib/workflow-engine.js'));
316
+ return transition(entityType, entityId, workflowName, toState, user, reason);
317
+ }
318
+
319
+ async getAvailableTransitions(workflowName, currentState, user, record = null) {
320
+ const { getAvailableTransitions } = await import(resolveModule('./src/lib/workflow-engine.js'));
321
+ return getAvailableTransitions(workflowName, currentState, user, record);
322
+ }
323
+
324
+ // === AuthZ ===
325
+
326
+ async can(user, spec, action) {
327
+ const { can } = await import(resolveModule('./services/permission.service.js'));
328
+ return can(user, spec, action);
329
+ }
330
+
331
+ async requirePermission(user, spec, action) {
332
+ const { require } = await import(resolveModule('./services/permission.service.js'));
333
+ return require(user, spec, action);
334
+ }
335
+
336
+ // === Hooks ===
337
+
338
+ async executeHook(event, context) {
339
+ const { executeHook } = await import(resolveModule('./lib/hook-engine.js'));
340
+ return executeHook(event, context);
341
+ }
342
+
343
+ // === Database transaction ===
344
+
345
+ async withTransaction(callback) {
346
+ const { withTransaction } = await import(resolveModule('./lib/query-engine.js'));
347
+ return withTransaction(callback);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Create thatcher instance
353
+ */
354
+ export function createThatcher(options) {
355
+ return new Thatcher(options);
356
+ }
357
+
358
+ /**
359
+ * Quick-start helper
360
+ */
361
+ export async function startThatcher(options = {}) {
362
+ const t = new Thatcher(options);
363
+ await t.init();
364
+ await t.startServer();
365
+ return t;
366
+ }
367
+
368
+ export default Thatcher;
@@ -0,0 +1,162 @@
1
+ export const aria = {
2
+ label: (text) => `aria-label="${escapeHtml(text)}"`,
3
+ labelledBy: (id) => `aria-labelledby="${id}"`,
4
+ describedBy: (id) => `aria-describedby="${id}"`,
5
+ live: (politeness = 'polite') => `aria-live="${politeness}" aria-atomic="true"`,
6
+ hidden: (hidden = true) => hidden ? 'aria-hidden="true"' : '',
7
+ expanded: (expanded) => `aria-expanded="${expanded}"`,
8
+ pressed: (pressed) => `aria-pressed="${pressed}"`,
9
+ checked: (checked) => `aria-checked="${checked}"`,
10
+ disabled: (disabled) => disabled ? 'aria-disabled="true"' : '',
11
+ current: (type = 'page') => `aria-current="${type}"`,
12
+ haspopup: (type = 'dialog') => `aria-haspopup="${type}"`,
13
+ controls: (id) => `aria-controls="${id}"`,
14
+ owns: (id) => `aria-owns="${id}"`,
15
+ invalid: (invalid) => invalid ? 'aria-invalid="true"' : '',
16
+ required: () => 'aria-required="true"',
17
+ };
18
+
19
+ export const role = {
20
+ button: 'role="button"',
21
+ dialog: 'role="dialog"',
22
+ alertdialog: 'role="alertdialog"',
23
+ navigation: 'role="navigation"',
24
+ main: 'role="main"',
25
+ search: 'role="search"',
26
+ form: 'role="form"',
27
+ region: 'role="region"',
28
+ banner: 'role="banner"',
29
+ contentinfo: 'role="contentinfo"',
30
+ complementary: 'role="complementary"',
31
+ list: 'role="list"',
32
+ listitem: 'role="listitem"',
33
+ menu: 'role="menu"',
34
+ menuitem: 'role="menuitem"',
35
+ status: 'role="status"',
36
+ alert: 'role="alert"',
37
+ progressbar: 'role="progressbar"',
38
+ tablist: 'role="tablist"',
39
+ tab: 'role="tab"',
40
+ tabpanel: 'role="tabpanel"',
41
+ };
42
+
43
+ function escapeHtml(text) {
44
+ return String(text).replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
45
+ }
46
+
47
+ export function skipLink(href, text) {
48
+ return `<a href="${href}" class="skip-link" ${aria.label('Skip to ' + text)}>${text}</a>`;
49
+ }
50
+
51
+ export function landmark(type, label, content) {
52
+ const r = role[type] || `role="${type}"`;
53
+ const a = label ? aria.label(label) : '';
54
+ return `<div ${r} ${a}>${content}</div>`;
55
+ }
56
+
57
+ export function semanticButton(text, attrs = {}) {
58
+ const label = attrs.label || text;
59
+ const disabled = attrs.disabled ? 'disabled' : '';
60
+ const pressed = attrs.pressed !== undefined ? aria.pressed(attrs.pressed) : '';
61
+ const expanded = attrs.expanded !== undefined ? aria.expanded(attrs.expanded) : '';
62
+ const controls = attrs.controls ? aria.controls(attrs.controls) : '';
63
+ const onClick = attrs.onclick ? `onclick="${attrs.onclick}"` : '';
64
+ const className = attrs.class || 'btn';
65
+ return `<button type="button" class="${className}" ${aria.label(label)} ${disabled} ${pressed} ${expanded} ${controls} ${onClick}>${text}</button>`;
66
+ }
67
+
68
+ export function semanticLink(href, text, attrs = {}) {
69
+ const label = attrs.label || text;
70
+ const current = attrs.current ? aria.current(attrs.current) : '';
71
+ const className = attrs.class || '';
72
+ return `<a href="${href}" class="${className}" ${aria.label(label)} ${current}>${text}</a>`;
73
+ }
74
+
75
+ export function formField(type, name, attrs = {}) {
76
+ const id = attrs.id || `field-${name}`;
77
+ const label = attrs.label || name;
78
+ const required = attrs.required ? aria.required() : '';
79
+ const invalid = attrs.invalid ? aria.invalid(true) : '';
80
+ const describedBy = attrs.description ? aria.describedBy(`${id}-desc`) : '';
81
+ const value = attrs.value !== undefined ? `value="${escapeHtml(attrs.value)}"` : '';
82
+ const placeholder = attrs.placeholder ? `placeholder="${escapeHtml(attrs.placeholder)}"` : '';
83
+ const className = attrs.class || 'input input-bordered';
84
+
85
+ let field;
86
+ if (type === 'textarea') {
87
+ field = `<textarea id="${id}" name="${name}" class="${className}" ${required} ${invalid} ${describedBy} ${placeholder}>${attrs.value || ''}</textarea>`;
88
+ } else if (type === 'select') {
89
+ const options = (attrs.options || []).map(opt =>
90
+ `<option value="${opt.value}"${opt.selected ? ' selected' : ''}>${opt.label}</option>`
91
+ ).join('');
92
+ field = `<select id="${id}" name="${name}" class="${className}" ${required} ${invalid} ${describedBy}>${options}</select>`;
93
+ } else {
94
+ field = `<input type="${type}" id="${id}" name="${name}" class="${className}" ${value} ${placeholder} ${required} ${invalid} ${describedBy}/>`;
95
+ }
96
+
97
+ const labelEl = `<label for="${id}" class="label"><span class="label-text">${label}${attrs.required ? ' <span aria-hidden="true">*</span>' : ''}</span></label>`;
98
+ const descEl = attrs.description ? `<p id="${id}-desc" class="text-xs text-gray-500 mt-1">${attrs.description}</p>` : '';
99
+
100
+ return `<div class="form-control">${labelEl}${field}${descEl}</div>`;
101
+ }
102
+
103
+ export function liveRegion(id, politeness = 'polite') {
104
+ return `<div id="${id}" ${aria.live(politeness)} class="sr-only"></div>`;
105
+ }
106
+
107
+ export function announceToScreenReader(regionId, message) {
108
+ return `document.getElementById('${regionId}').textContent = '${escapeHtml(message).replace(/'/g, "\\'")}';`;
109
+ }
110
+
111
+ export const focusTrap = {
112
+ activate: (containerId) => `
113
+ (function() {
114
+ const container = document.getElementById('${containerId}');
115
+ if (!container) return;
116
+ const focusable = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
117
+ const first = focusable[0];
118
+ const last = focusable[focusable.length - 1];
119
+ container._trapHandler = (e) => {
120
+ if (e.key !== 'Tab') return;
121
+ if (e.shiftKey && document.activeElement === first) {
122
+ e.preventDefault();
123
+ last.focus();
124
+ } else if (!e.shiftKey && document.activeElement === last) {
125
+ e.preventDefault();
126
+ first.focus();
127
+ }
128
+ };
129
+ container.addEventListener('keydown', container._trapHandler);
130
+ first?.focus();
131
+ })();
132
+ `,
133
+ deactivate: (containerId) => `
134
+ (function() {
135
+ const container = document.getElementById('${containerId}');
136
+ if (container?._trapHandler) {
137
+ container.removeEventListener('keydown', container._trapHandler);
138
+ delete container._trapHandler;
139
+ }
140
+ })();
141
+ `
142
+ };
143
+
144
+ export const keyboard = {
145
+ escape: (callback) => `if (event.key === 'Escape') { ${callback} }`,
146
+ enter: (callback) => `if (event.key === 'Enter') { ${callback} }`,
147
+ space: (callback) => `if (event.key === ' ' || event.key === 'Spacebar') { event.preventDefault(); ${callback} }`,
148
+ arrowNav: (upCallback, downCallback) => `
149
+ if (event.key === 'ArrowUp') { event.preventDefault(); ${upCallback} }
150
+ if (event.key === 'ArrowDown') { event.preventDefault(); ${downCallback} }
151
+ `,
152
+ };
153
+
154
+ export function visuallyHidden(content) {
155
+ return `<span class="sr-only">${content}</span>`;
156
+ }
157
+
158
+ export function accessibleImage(src, alt, attrs = {}) {
159
+ const className = attrs.class || '';
160
+ const decorative = attrs.decorative ? 'alt="" role="presentation"' : `alt="${escapeHtml(alt)}"`;
161
+ return `<img src="${src}" ${decorative} class="${className}"/>`;
162
+ }
@@ -0,0 +1,34 @@
1
+ 'use server';
2
+
3
+ import { create, update, remove } from '@/engine';
4
+ import { requireUser, check } from '@/engine.server';
5
+ import { getSpec } from '@/config/spec-helpers';
6
+ import { revalidatePath } from '@/lib/next-polyfills';
7
+
8
+ export async function serverCreateEntity(entityName, data) {
9
+ const user = await requireUser();
10
+ const spec = getSpec(entityName);
11
+ await check(user, spec, 'create');
12
+ const result = create(entityName, data, user);
13
+ revalidatePath(`/${entityName}`);
14
+ return result;
15
+ }
16
+
17
+ export async function serverUpdateEntity(entityName, id, data) {
18
+ const user = await requireUser();
19
+ const spec = getSpec(entityName);
20
+ await check(user, spec, 'edit');
21
+ const result = update(entityName, id, data, user);
22
+ revalidatePath(`/${entityName}/${id}`);
23
+ revalidatePath(`/${entityName}`);
24
+ return result;
25
+ }
26
+
27
+ export async function serverDeleteEntity(entityName, id) {
28
+ const user = await requireUser();
29
+ const spec = getSpec(entityName);
30
+ await check(user, spec, 'delete');
31
+ const result = remove(entityName, id);
32
+ revalidatePath(`/${entityName}`);
33
+ return result;
34
+ }
@@ -0,0 +1,21 @@
1
+ import { serverCreateEntity, serverUpdateEntity, serverDeleteEntity } from '@/lib/action-factory';
2
+
3
+ export function createCRUDActions(entityName) {
4
+ return {
5
+ create: (data) => serverCreateEntity(entityName, data),
6
+ update: (id, data) => serverUpdateEntity(entityName, id, data),
7
+ delete: (id) => serverDeleteEntity(entityName, id),
8
+ };
9
+ }
10
+
11
+ export function createEntityAction(entityName, actionName, permission, handler) {
12
+ return async (...args) => {
13
+ 'use server';
14
+ const { requireUser, check } = await import('@/engine.server');
15
+ const { getSpec } = await import('@/config');
16
+ const user = await requireUser();
17
+ const spec = getSpec(entityName);
18
+ await check(user, spec, permission);
19
+ return handler(user, spec, ...args);
20
+ };
21
+ }