thatcher 1.0.7 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +4 -16
  2. package/package.json +12 -18
  3. package/src/adapters/google-auth.js +4 -43
  4. package/src/adapters/google-drive.js +12 -67
  5. package/src/app/api/audit/dashboard/route.js +14 -11
  6. package/src/app/api/audit/logs/route.js +2 -2
  7. package/src/app/api/audit/permissions/[id]/route.js +6 -3
  8. package/src/app/api/audit/permissions/route.js +10 -7
  9. package/src/app/api/audit/permissions/stats/route.js +8 -5
  10. package/src/app/api/audit/route.js +4 -4
  11. package/src/app/api/audit/stats/route.js +1 -1
  12. package/src/app/api/auth/google/callback/route.js +5 -2
  13. package/src/app/api/auth/google/route.js +4 -1
  14. package/src/app/api/auth/login/route.js +5 -2
  15. package/src/app/api/auth/mwr-bridge/route.js +11 -21
  16. package/src/app/api/auth/password-reset/route.js +15 -14
  17. package/src/app/api/cron/trigger/route.js +4 -1
  18. package/src/app/api/debug/[[...path]]/route.js +6 -5
  19. package/src/app/api/debug/sqlite/route.js +7 -12
  20. package/src/app/api/debug/sync/route.js +5 -5
  21. package/src/app/api/domains/route.js +4 -1
  22. package/src/app/api/email/allocate/batch/route.js +19 -29
  23. package/src/app/api/email/allocate/route.js +16 -17
  24. package/src/app/api/email/receive/route.js +31 -42
  25. package/src/app/api/email/send/route.js +28 -16
  26. package/src/app/api/email/unallocated/route.js +10 -13
  27. package/src/app/api/files/[id]/route.js +4 -1
  28. package/src/app/api/formula/[[...path]]/route.js +0 -4
  29. package/src/app/api/health/route.js +10 -9
  30. package/src/app/api/metrics/route.js +1 -1
  31. package/src/cli.js +4 -4
  32. package/src/config/config-loader.js +1 -21
  33. package/src/config/constants.js +0 -35
  34. package/src/config/env.js +0 -30
  35. package/src/config/spec-helpers.js +6 -95
  36. package/src/engine.js +23 -0
  37. package/src/engine.server.js +8 -78
  38. package/src/index.js +42 -67
  39. package/src/lib/alert-manager.js +7 -3
  40. package/src/lib/api-helpers.js +4 -3
  41. package/src/lib/api.js +10 -10
  42. package/src/lib/audit-logger-enhanced.js +77 -83
  43. package/src/lib/auth-middleware.js +1 -32
  44. package/src/lib/auth-route-helpers.js +5 -0
  45. package/src/lib/busybase-adapter.js +0 -5
  46. package/src/lib/busybase-audit-reads.js +142 -0
  47. package/src/lib/busybase-audit.js +41 -0
  48. package/src/lib/busybase-lucia-adapter.js +72 -0
  49. package/src/lib/busybase-store.js +338 -0
  50. package/src/lib/config-field-helpers.js +0 -17
  51. package/src/lib/config-generator-engine.js +19 -36
  52. package/src/lib/config-helpers.js +0 -21
  53. package/src/lib/crud-action-helpers.js +2 -2
  54. package/src/lib/crud-factory.js +1 -16
  55. package/src/lib/crud-handlers.js +17 -52
  56. package/src/lib/csrf-protection.js +5 -2
  57. package/src/lib/date-utils.js +3 -127
  58. package/src/lib/debug-registry.js +0 -5
  59. package/src/lib/domain-loader.js +4 -1
  60. package/src/lib/email-sender.js +25 -23
  61. package/src/lib/error-handler.js +5 -41
  62. package/src/lib/error-recovery.js +10 -7
  63. package/src/lib/error-resilience.js +7 -5
  64. package/src/lib/events-engine.js +9 -6
  65. package/src/lib/export-sink.js +0 -5
  66. package/src/lib/field-iterator.js +7 -76
  67. package/src/lib/field-registry.js +0 -22
  68. package/src/lib/generic-crud-handler.js +0 -16
  69. package/src/lib/hook-engine.js +5 -53
  70. package/src/lib/hot-reload/checkpoint.js +5 -2
  71. package/src/lib/hot-reload/promise-container.js +7 -3
  72. package/src/lib/hot-reload/route-wrapper.js +4 -1
  73. package/src/lib/hot-reload/supervisor.js +6 -3
  74. package/src/lib/hyperformula-service.js +0 -5
  75. package/src/lib/id-helpers.js +10 -0
  76. package/src/lib/index.js +4 -7
  77. package/src/lib/log-aggregator.js +0 -1
  78. package/src/lib/logger.js +0 -15
  79. package/src/lib/metrics-collector.js +2 -46
  80. package/src/lib/next-compat.js +15 -1
  81. package/src/lib/observability-bootstrap.js +0 -5
  82. package/src/lib/perf-monitor.js +4 -1
  83. package/src/lib/perf-profiler.js +0 -5
  84. package/src/lib/query-string-adapter.js +5 -47
  85. package/src/lib/realtime-server.js +4 -26
  86. package/src/lib/request-tracing.js +0 -5
  87. package/src/lib/resource-monitor.js +4 -1
  88. package/src/lib/response-formatter.js +0 -38
  89. package/src/lib/route-helpers.js +3 -1
  90. package/src/lib/safe-json.js +5 -1
  91. package/src/lib/server-bootstrap.js +10 -9
  92. package/src/lib/stage-pipeline.js +2 -2
  93. package/src/lib/state-transport-client.js +4 -1
  94. package/src/lib/static-server.js +2 -2
  95. package/src/lib/status-helpers.js +2 -51
  96. package/src/lib/tracing.js +0 -5
  97. package/src/lib/utils.js +6 -67
  98. package/src/lib/validate.js +5 -88
  99. package/src/lib/validators.js +0 -38
  100. package/src/lib/with-audit-logging.js +1 -1
  101. package/src/lib/workflow-engine.js +10 -77
  102. package/src/lib/xstate-workflow-engine.js +3 -9
  103. package/src/plugins/index.js +5 -3
  104. package/src/server/server.js +26 -27
  105. package/src/services/collaborator-role.service.js +21 -77
  106. package/src/services/email-sender.js +7 -34
  107. package/src/services/notification-engine.js +12 -33
  108. package/src/services/permission.service.js +10 -73
  109. package/src/ui/advanced-search-renderer.js +3 -2
  110. package/src/ui/advanced-widgets.js +1 -1
  111. package/src/ui/auth-pages.js +10 -9
  112. package/src/ui/auth-styles.js +1 -1
  113. package/src/ui/client.js +1 -1
  114. package/src/ui/component-engine.js +1 -1
  115. package/src/ui/dashboard-renderer.js +5 -5
  116. package/src/ui/dialog-engine.js +6 -9
  117. package/src/ui/dialog-factory.js +2 -2
  118. package/src/ui/engagement-cards.js +7 -7
  119. package/src/ui/engagement-dialogs.js +2 -3
  120. package/src/ui/engagement-grid-renderer.js +20 -19
  121. package/src/ui/entity-renderer.js +56 -42
  122. package/src/ui/file-dialogs.js +25 -9
  123. package/src/ui/flexup-report-renderer.js +21 -18
  124. package/src/ui/format-helpers.js +27 -28
  125. package/src/ui/highlight-threading-renderer.js +9 -9
  126. package/src/ui/job-management-renderer.js +18 -16
  127. package/src/ui/layout.js +51 -25
  128. package/src/ui/ml-console-renderer.js +15 -16
  129. package/src/ui/monitoring-dashboard-client.js +9 -8
  130. package/src/ui/notifications-renderer.js +11 -10
  131. package/src/ui/page-handler-admin.js +32 -23
  132. package/src/ui/page-handler-helpers.js +27 -23
  133. package/src/ui/page-handler-reviews.js +71 -35
  134. package/src/ui/page-handler-rfi.js +28 -14
  135. package/src/ui/page-handler.js +65 -48
  136. package/src/ui/picker-dialogs.js +26 -19
  137. package/src/ui/render-helpers.js +84 -46
  138. package/src/ui/review-comparison-renderer.js +8 -7
  139. package/src/ui/review-detail-panels.js +8 -9
  140. package/src/ui/review-detail-renderer.js +3 -3
  141. package/src/ui/review-detail-script.js +13 -12
  142. package/src/ui/review-mwr-renderer.js +6 -5
  143. package/src/ui/review-renderer.js +7 -6
  144. package/src/ui/review-widgets.js +3 -3
  145. package/src/ui/rfi-detail-renderer.js +37 -20
  146. package/src/ui/rfi-report-renderer.js +14 -9
  147. package/src/ui/settings-renderer-advanced.js +21 -18
  148. package/src/ui/settings-renderer-advanced2.js +20 -19
  149. package/src/ui/settings-renderer-teams.js +4 -4
  150. package/src/ui/settings-renderer.js +4 -4
  151. package/src/ui/spacing-system.js +5 -2
  152. package/src/ui/standalone-login.js +2 -2
  153. package/src/ui/styles.css +2 -1
  154. package/src/ui/styles2.css +75 -5
  155. package/src/ui/test-page.js +4 -4
  156. package/src/ui/widgets.js +20 -15
  157. package/src/lib/audit-logger.js +0 -193
  158. package/src/lib/database-core.js +0 -255
  159. package/src/lib/database-migrations.js +0 -350
  160. package/src/lib/db-backup.js +0 -97
  161. package/src/lib/error-boundary.js +0 -134
  162. package/src/lib/errors.js +0 -69
  163. package/src/lib/health-monitor.js +0 -134
  164. package/src/lib/index-optimizer.js +0 -96
  165. package/src/lib/query-engine-write.js +0 -221
  166. package/src/lib/query-engine.js +0 -446
  167. package/src/ui/collaboration-dialogs.js +0 -31
  168. package/src/ui/global-tags.js +0 -144
  169. package/src/ui/idle-logout.js +0 -156
  170. package/src/ui/letter-dialogs.js +0 -37
  171. package/src/ui/password-reset-page.js +0 -146
  172. package/src/ui/perf-renderer.js +0 -71
  173. package/src/ui/rfi-renderer.js +0 -194
  174. package/src/ui/validation-ui.js +0 -147
@@ -1,446 +0,0 @@
1
- /**
2
- * Query Engine - Read operations
3
- * Adapted from moonlanding/src/lib/query-engine.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
-
10
- const db = getDatabase();
11
- const logger = createLogger('[QueryEngine]');
12
-
13
- /**
14
- * Execute a query with error handling
15
- * @param {string} sql
16
- * @param {Array} params
17
- * @param {object} context
18
- * @returns {Array}
19
- */
20
- function execQuery(sql, params = [], context = {}) {
21
- try {
22
- return db.prepare(sql).all(...params);
23
- } catch (e) {
24
- logger.error(`${context.operation || 'Query'} ${context.entity || ''}`, { sql, error: e.message });
25
- throw new Error(`Database query failed: ${e.message}`);
26
- }
27
- }
28
-
29
- /**
30
- * Execute a single-row query
31
- * @param {string} sql
32
- * @param {Array} params
33
- * @param {object} context
34
- * @returns {object|null}
35
- */
36
- function execGet(sql, params = [], context = {}) {
37
- try {
38
- return db.prepare(sql).get(...params);
39
- } catch (e) {
40
- logger.error(`${context.operation || 'Get'} ${context.entity || ''}`, { sql, error: e.message });
41
- throw new Error(`Database get failed: ${e.message}`);
42
- }
43
- }
44
-
45
- /**
46
- * Execute a write statement (INSERT/UPDATE/DELETE) with error handling
47
- * @param {string} sql
48
- * @param {Array} params
49
- * @param {object} context
50
- * @returns {object} better-sqlite3 RunResult
51
- */
52
- function execRun(sql, params = [], context = {}) {
53
- try {
54
- return db.prepare(sql).run(...params);
55
- } catch (e) {
56
- logger.error(`${context.operation || 'Run'} ${context.entity || ''}`, { sql, error: e.message });
57
- throw new Error(`Database run failed: ${e.message}`);
58
- }
59
- }
60
-
61
- /**
62
- * Get table name for entity (users vs user)
63
- * @param {object} spec
64
- * @returns {string}
65
- */
66
- function tableName(spec) {
67
- return spec.name === 'user' ? 'users' : spec.name;
68
- }
69
-
70
- /**
71
- * Build SELECT query with joins, where, sorting, pagination
72
- * @param {object} spec - Entity specification
73
- * @param {object} where - Where conditions
74
- * @param {object} options - Query options
75
- * @returns {{sql: string, params: Array}}
76
- */
77
- function buildSpecQuery(spec, where = {}, options = {}) {
78
- const tbl = tableName(spec);
79
- const table = `"${tbl}"`;
80
- const selects = [`${table}.*`];
81
- const joins = [];
82
-
83
- // Add computed fields
84
- if (spec.computed) {
85
- Object.entries(spec.computed).forEach(([k, c]) => {
86
- selects.push(`${c.sql} as "${k}"`);
87
- });
88
- }
89
-
90
- // Add joins for ref fields with display
91
- Object.entries(spec.fields || {}).forEach(([k, f]) => {
92
- if (f.type === 'ref' && f.display) {
93
- const refTbl = f.ref === 'user' ? 'users' : f.ref;
94
- const alias = `"${refTbl}_${k}"`;
95
- joins.push(`LEFT JOIN "${refTbl}" ${alias} ON ${table}."${k}" = ${alias}.id`);
96
-
97
- const displayField = f.display.split('.')[1] || 'name';
98
- selects.push(`${alias}."${displayField}" as "${k}_display"`);
99
- }
100
- });
101
-
102
- const wc = [];
103
- const p = [];
104
-
105
- // Where conditions
106
- Object.entries(where).forEach(([k, v]) => {
107
- if (v !== undefined && v !== null) {
108
- wc.push(`${table}."${k}" = ?`);
109
- p.push(v);
110
- }
111
- });
112
-
113
- // Exclude soft-deleted by default
114
- if (spec.fields?.status && !where.status && !options.includeDeleted) {
115
- wc.push(`${table}."status" != '${RECORD_STATUS.DELETED}'`);
116
- }
117
- if (spec.fields?.archived && !where.archived && !options.includeArchived) {
118
- wc.push(`${table}."archived" = 0`);
119
- }
120
-
121
- let sql = `SELECT ${selects.join(', ')} FROM ${table}`;
122
- if (joins.length) {
123
- sql += ' ' + joins.join(' ');
124
- }
125
- if (wc.length) {
126
- sql += ` WHERE ` + wc.join(` AND `);
127
- }
128
-
129
- // Sorting
130
- const sort = options.sort || spec.list?.defaultSort;
131
- if (sort && sort.field && spec.fields?.[sort.field]) {
132
- const dir = (sort.dir || 'ASC').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
133
- sql += ` ORDER BY ${table}."${sort.field}" ${dir}`;
134
- }
135
-
136
- // Limit/offset
137
- if (options.limit) {
138
- sql += ` LIMIT ${parseInt(options.limit, 10)}`;
139
- if (options.offset) {
140
- sql += ` OFFSET ${parseInt(options.offset, 10)}`;
141
- }
142
- }
143
-
144
- return { sql, params: p };
145
- }
146
-
147
- /**
148
- * Get total count for entity (optionally with where)
149
- * @param {string} entity
150
- * @param {object} where
151
- * @param {object} options
152
- * @returns {number}
153
- */
154
- export function count(entity, where = {}, options = {}) {
155
- const spec = getSpec(entity);
156
- const tbl = tableName(spec);
157
- const table = `"${tbl}"`;
158
- const wc = [];
159
- const p = [];
160
-
161
- Object.entries(where).forEach(([k, v]) => {
162
- if (v !== undefined && v !== null) {
163
- wc.push(`${table}."${k}" = ?`);
164
- p.push(v);
165
- }
166
- });
167
-
168
- if (spec.fields?.status && !where.status && !options.includeDeleted) {
169
- wc.push(`${table}."status" != '${RECORD_STATUS.DELETED}'`);
170
- }
171
-
172
- const whereClause = wc.length ? `WHERE ${wc.join(' AND ')}` : '';
173
- const sql = `SELECT COUNT(*) as cnt FROM ${table} ${whereClause}`;
174
-
175
- const result = execGet(sql, p, { entity, operation: 'Count' });
176
- return result?.cnt || 0;
177
- }
178
-
179
- /**
180
- * List all records (with optional where)
181
- * @param {string} entity
182
- * @param {object} where
183
- * @param {object} options
184
- * @returns {Array}
185
- */
186
- export function list(entity, where = {}, options = {}) {
187
- const spec = getSpec(entity);
188
- const { sql, params } = buildSpecQuery(spec, where, options);
189
- return execQuery(sql, params, { entity, operation: 'List' });
190
- }
191
-
192
- /**
193
- * List with pagination
194
- * @param {string} entity
195
- * @param {object} where
196
- * @param {number} page
197
- * @param {number|null} pageSize
198
- * @returns {{items: Array, pagination: object}}
199
- */
200
- export async function listWithPagination(entity, where = {}, page = 1, pageSize = null) {
201
- const spec = getSpec(entity);
202
- const tbl = tableName(spec);
203
-
204
- const paginationCfg = getPaginationConfig(spec);
205
- const defaultPageSize = spec.list?.pageSize || paginationCfg.default_page_size;
206
- const finalPageSize = pageSize || defaultPageSize;
207
- const finalPage = Math.max(1, page);
208
-
209
- const offset = (finalPage - 1) * finalPageSize;
210
-
211
- const { sql, params } = buildSpecQuery(spec, where, {
212
- ...options,
213
- limit: finalPageSize,
214
- offset,
215
- });
216
-
217
- const items = execQuery(sql, params, { entity, operation: 'ListWithPagination' });
218
-
219
- const total = count(entity, where, options);
220
-
221
- return {
222
- items,
223
- pagination: {
224
- page: finalPage,
225
- pageSize: finalPageSize,
226
- total,
227
- totalPages: Math.ceil(total / finalPageSize),
228
- },
229
- };
230
- }
231
-
232
- /**
233
- * Get single record by ID
234
- * @param {string} entity
235
- * @param {string|number} id
236
- * @returns {object|null}
237
- */
238
- export function get(entity, id) {
239
- const spec = getSpec(entity);
240
- const tbl = tableName(spec);
241
- const sql = `SELECT * FROM "${tbl}" WHERE id = ?`;
242
- return execGet(sql, [id], { entity, operation: 'Get' });
243
- }
244
-
245
- /**
246
- * Get single record by field value
247
- * @param {string} entity
248
- * @param {string} field
249
- * @param {any} value
250
- * @returns {object|null}
251
- */
252
- export function getBy(entity, field, value) {
253
- const spec = getSpec(entity);
254
- const tbl = tableName(spec);
255
- const sql = `SELECT * FROM "${tbl}" WHERE "${field}" = ? LIMIT 1`;
256
- return execGet(sql, [value], { entity, operation: 'GetBy' });
257
- }
258
-
259
- /**
260
- * Search using FTS
261
- * @param {string} entity
262
- * @param {string} query
263
- * @param {object} where
264
- * @param {object} options
265
- * @returns {Array}
266
- */
267
- export function search(entity, query, where = {}, options = {}) {
268
- const spec = getSpec(entity);
269
- const tbl = tableName(spec);
270
- const ftsTable = `${tbl}_fts`;
271
-
272
- // Check if FTS table exists
273
- const ftsExists = db.prepare(`
274
- SELECT name FROM sqlite_master WHERE type='table' AND name=?
275
- `).get(ftsTable);
276
-
277
- if (!ftsExists) {
278
- // Fallback to simple LIKE search
279
- const { sql, params } = buildSpecQuery(spec, where, options);
280
- const baseSql = sql.replace('SELECT *', `SELECT *`);
281
- const searchTerm = `%${query}%`;
282
- const searchFields = Object.keys(spec.fields || {}).filter(
283
- f => ['text', 'textarea', 'email'].includes(spec.fields[f].type)
284
- );
285
-
286
- if (searchFields.length > 0) {
287
- const conditions = searchFields.map(f => `"${f}" LIKE ?`).join(' OR ');
288
- const whereAnd = sql.includes('WHERE') ? ' AND ' : ' WHERE ';
289
- const fullSql = baseSql + whereAnd + `(${conditions})`;
290
- const searchParams = [...params, ...searchFields.map(() => searchTerm)];
291
- return execQuery(fullSql, searchParams, { entity, operation: 'SearchFallback' });
292
- }
293
-
294
- return [];
295
- }
296
-
297
- // Use FTS
298
- const ftsResults = db.prepare(`
299
- SELECT rowid as id FROM ${ftsTable}
300
- WHERE ${ftsTable} MATCH ?
301
- `).all(query);
302
-
303
- if (!ftsResults.length) return [];
304
-
305
- const ids = ftsResults.map(r => r.id);
306
- const idPlaceholders = ids.map(() => '?').join(',');
307
- const { sql, params } = buildSpecQuery(spec, { ...where, id: { $in: ids } }, options);
308
- const finalSql = sql.replace('WHERE', `WHERE "${tbl}"."id" IN (${idPlaceholders}) AND `);
309
-
310
- return execQuery(finalSql, [...ids, ...params], { entity, operation: 'Search' });
311
- }
312
-
313
- /**
314
- * Search with pagination
315
- * @param {string} entity
316
- * @param {string} query
317
- * @param {object} where
318
- * @param {number} page
319
- * @param {number} pageSize
320
- * @returns {{items: Array, pagination: object}}
321
- */
322
- export async function searchWithPagination(entity, query, where = {}, page = 1, pageSize = null) {
323
- const spec = getSpec(entity);
324
- const paginationCfg = getPaginationConfig(spec);
325
- const defaultPageSize = spec.list?.pageSize || paginationCfg.default_page_size;
326
- const finalPageSize = pageSize || defaultPageSize;
327
- const finalPage = Math.max(1, page);
328
-
329
- const items = search(entity, query, where, {
330
- limit: finalPageSize,
331
- offset: (finalPage - 1) * finalPageSize,
332
- });
333
-
334
- // For total count, we'd need a separate FTS count query - simplified here
335
- const total = items.length; // Approximate
336
-
337
- return {
338
- items,
339
- pagination: {
340
- page: finalPage,
341
- pageSize: finalPageSize,
342
- total,
343
- totalPages: Math.ceil(total / finalPageSize),
344
- },
345
- };
346
- }
347
-
348
- /**
349
- * Get children of a parent entity
350
- * @param {string} parentEntity
351
- * @param {string|number} parentId
352
- * @param {object} childDef
353
- * @returns {Array}
354
- */
355
- export function getChildren(parentEntity, parentId, childDef) {
356
- const childSpec = getSpec(childDef.entity);
357
- const childTbl = tableName(childSpec);
358
- const fk = childDef.fk || `${parentEntity}_id`;
359
-
360
- const sql = `SELECT * FROM "${childTbl}" WHERE "${fk}" = ? AND status != 'deleted'`;
361
- return execQuery(sql, [parentId], { entity: childDef.entity, operation: 'GetChildren' });
362
- }
363
-
364
- /**
365
- * Batch-fetch children of a parent for several child definitions at once.
366
- * Ported from moonlanding; uses thatcher's `list` + getChildren fk convention
367
- * (`fk`, falling back to moon's `foreignKey`, then `${parentEntity}_id`).
368
- * @param {string} parentEntity
369
- * @param {string|number} parentId
370
- * @param {object|Array} childSpecs - map of key->def, or array of entity names
371
- * @returns {Promise<object>} map of key -> rows
372
- */
373
- export function batchGetChildren(parentEntity, parentId, childSpecs) {
374
- const childEntries = Array.isArray(childSpecs)
375
- ? childSpecs.map(s => [s, { entity: s }])
376
- : Object.entries(childSpecs);
377
- const queries = childEntries.map(async ([key, def]) => {
378
- const entity = def.entity || def;
379
- const foreignKey = def.fk || def.foreignKey || `${parentEntity}_id`;
380
- const results = list(entity, { [foreignKey]: parentId });
381
- return [key, results];
382
- });
383
- return Promise.all(queries).then(results => Object.fromEntries(results));
384
- }
385
-
386
- /**
387
- * Get pagination config from system settings
388
- * @param {object} spec - Entity spec
389
- * @returns {{default_page_size: number, max_page_size: number}}
390
- */
391
- function getPaginationConfig(spec) {
392
- // Try to get from config engine, fallback to defaults
393
- try {
394
- // Will be provided by caller or fetched from global config
395
- return {
396
- default_page_size: spec.list?.pageSize || 50,
397
- max_page_size: 500,
398
- };
399
- } catch {
400
- return { default_page_size: 50, max_page_size: 500 };
401
- }
402
- }
403
-
404
- /**
405
- * Simple logger
406
- */
407
- function createLogger(prefix) {
408
- return {
409
- error: (msg, meta = {}) => {
410
- console.error(`${prefix} ${msg}`, meta);
411
- },
412
- warn: (msg, meta = {}) => {
413
- console.warn(`${prefix} ${msg}`, meta);
414
- },
415
- info: (msg, meta = {}) => {
416
- console.info(`${prefix} ${msg}`, meta);
417
- },
418
- };
419
- }
420
-
421
- /**
422
- * Run in transaction
423
- * @param {Function} callback
424
- * @returns {Promise<any>}
425
- */
426
- export async function withTransaction(callback) {
427
- const dbInstance = getDatabase();
428
- try {
429
- dbInstance.prepare('BEGIN').run();
430
- const result = await callback();
431
- dbInstance.prepare('COMMIT').run();
432
- return result;
433
- } catch (e) {
434
- dbInstance.prepare('ROLLBACK').run();
435
- throw e;
436
- }
437
- }
438
-
439
- // Re-export write operations so consumers can import all CRUD from a single
440
- // module (ported from moonlanding). Several thatcher modules already import
441
- // `create`/`update`/`remove` from './query-engine.js'; this wires them through
442
- // to the existing write implementation without changing those callers.
443
- export { create, update, remove } from './query-engine-write.js';
444
-
445
- // Expose low-level executors for callers that need raw SQL access.
446
- export { execQuery, execGet, execRun };
@@ -1,31 +0,0 @@
1
- export function collaboratorManagementDialog(entityType, entityId) {
2
- return `<div id="collab-mgmt-dialog" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="collab-mgmt-dialog-title" aria-hidden="true">
3
- <div class="dialog-panel" style="max-width:640px">
4
- <div class="dialog-header"><span class="dialog-title" id="collab-mgmt-dialog-title">Manage Collaborators</span><button class="dialog-close" data-dialog-close="collab-mgmt-dialog" aria-label="Close dialog">&times;</button></div>
5
- <div class="dialog-body">
6
- <div id="cmd-list" class="flex flex-col gap-2" style="max-height:300px;overflow:auto"><div class="text-gray-500 text-sm text-center py-4">Loading...</div></div>
7
- <div class="inline-form" style="margin-top:1rem">
8
- <div class="inline-form-row">
9
- <div class="inline-form-field" style="flex:1"><label for="cmd-email">Email</label><input type="email" id="cmd-email" class="input input-bordered input-sm w-full" placeholder="collaborator@example.com"/></div>
10
- <div class="inline-form-field"><label for="cmd-role">Role</label><select id="cmd-role" class="select select-bordered select-sm"><option value="viewer">Viewer</option><option value="commenter">Commenter</option><option value="reviewer" selected>Reviewer</option><option value="manager">Manager</option></select></div>
11
- <div class="inline-form-field"><label>&nbsp;</label><button class="btn btn-primary btn-sm" data-action="cmdAdd">Add</button></div>
12
- </div>
13
- </div>
14
- </div>
15
- <div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="collab-mgmt-dialog">Close</button></div>
16
- </div></div>
17
- <script>
18
- window.openCollabMgmt=function(){document.getElementById('collab-mgmt-dialog').style.display='flex';cmdLoad()};
19
- async function cmdLoad(){var c=document.getElementById('cmd-list');c.innerHTML='<div class="text-gray-500 text-sm text-center py-2">Loading...</div>';try{var res=await fetch('/api/collaborator?${entityType}_id=${entityId}');var d=await res.json();var items=d.data||d||[];c.innerHTML='';if(!items.length){c.innerHTML='<div class="text-gray-500 text-sm text-center py-2">No collaborators</div>';return}items.forEach(function(item){var div=document.createElement('div');div.className='flex items-center justify-between gap-2';div.style.padding='0.5rem 0';div.style.borderBottom='1px solid #f3f4f6';var expired=item.expires_at&&item.expires_at<=Math.floor(Date.now()/1000);var expiryText=item.expires_at?'Expires: '+new Date(item.expires_at*1000).toLocaleDateString():'Permanent';if(expired)expiryText='<span style="color:#ef4444">Expired</span>';div.innerHTML='<div><div class="text-sm font-medium">'+(item.email||item.name||'Unknown')+'</div><div class="text-xs text-gray-500">'+(item.role_type||item.access_type||'viewer')+' &middot; '+expiryText+'</div></div><div class="flex gap-1"><button class="btn btn-xs btn-ghost" data-action="cmdExtend" data-args='["'+item.id+'"]'>Extend</button><button class="btn btn-xs btn-error btn-outline" data-action="cmdRevoke" data-args='["'+item.id+'"]'>Revoke</button></div>';c.appendChild(div)})}catch(e){c.innerHTML='<div class="text-red-500 text-sm text-center">Error loading</div>'}}
20
- window.cmdAdd=async function(){var email=document.getElementById('cmd-email').value.trim();var role=document.getElementById('cmd-role').value;if(!email){showToast('Email required','error');return}try{var r=await fetch('/api/collaborator',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:email,role_type:role,${entityType}_id:'${entityId}'})});if(r.ok){showToast('Collaborator added','success');document.getElementById('cmd-email').value='';cmdLoad()}else{var d=await r.json().catch(function(){return{}});showToast(d.error||'Failed','error')}}catch(e){showToast('Error','error')}};
21
- window.cmdRevoke=async function(id){if(!confirm('Revoke access?'))return;try{var r=await fetch('/api/collaborator/'+id,{method:'DELETE'});if(r.ok){showToast('Revoked','success');cmdLoad()}else showToast('Failed','error')}catch(e){showToast('Error','error')}};
22
- window.cmdExtend=async function(id){var days=prompt('Extend by how many days?','7');if(!days)return;var ts=Math.floor(Date.now()/1000)+Number(days)*86400;try{var r=await fetch('/api/collaborator/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({expires_at:ts})});if(r.ok){showToast('Extended','success');cmdLoad()}else showToast('Failed','error')}catch(e){showToast('Error','error')}};
23
- </script>`;
24
- }
25
-
26
- export function collaboratorExpiryUI(entityType, entityId) {
27
- return `<div class="card-clean" style="margin-bottom:1rem"><div class="card-clean-body"><div class="flex justify-between items-center"><h3 style="font-size:0.875rem;font-weight:600">Collaborator Access</h3><button class="btn btn-outline btn-sm" data-action="openCollabMgmt">Manage</button></div><div id="collab-expiry-list" class="mt-3 flex flex-col gap-1"></div></div></div>
28
- <script>
29
- (function(){fetch('/api/collaborator?${entityType}_id=${entityId}').then(function(r){return r.json()}).then(function(d){var items=d.data||d||[];var el=document.getElementById('collab-expiry-list');var now=Math.floor(Date.now()/1000);var expiring=items.filter(function(c){return c.expires_at&&c.expires_at>now&&c.expires_at-now<3*86400});var expired=items.filter(function(c){return c.expires_at&&c.expires_at<=now});if(!expiring.length&&!expired.length){el.innerHTML='<div class="text-xs text-gray-500">All access current</div>';return}var html='';expired.forEach(function(c){html+='<div class="text-xs" style="color:#ef4444">'+c.email+' - Expired</div>'});expiring.forEach(function(c){var days=Math.ceil((c.expires_at-now)/86400);html+='<div class="text-xs" style="color:#f59e0b">'+c.email+' - Expires in '+days+'d</div>'});el.innerHTML=html}).catch(function(){})})();
30
- </script>`;
31
- }
@@ -1,144 +0,0 @@
1
- let tagsStore = {};
2
-
3
- async function apiCall(endpoint, options = {}) {
4
- try {
5
- const res = await fetch(`/api/tag${endpoint}`, { headers: { 'Content-Type': 'application/json' }, ...options });
6
- if (!res.ok) throw new Error('API error');
7
- return await res.json();
8
- } catch (e) {
9
- throw e;
10
- }
11
- }
12
-
13
- async function createTag(name, color = '#808080') {
14
- const data = await apiCall('', { method: 'POST', body: JSON.stringify({ name: name.trim(), color }) });
15
- tagsStore[data.id] = { id: data.id, name: data.name, color: data.color, usage: 0 };
16
- showToast('Tag created', 'success');
17
- return data;
18
- }
19
-
20
- async function updateTag(id, name, color) {
21
- const data = await apiCall(`/${id}`, { method: 'PUT', body: JSON.stringify({ name: name.trim(), color }) });
22
- tagsStore[id] = { ...tagsStore[id], name: data.name, color: data.color };
23
- showToast('Tag updated', 'success');
24
- return data;
25
- }
26
-
27
- async function deleteTag(id) {
28
- await apiCall(`/${id}`, { method: 'DELETE' });
29
- delete tagsStore[id];
30
- showToast('Tag deleted', 'success');
31
- }
32
-
33
- async function listTags() {
34
- try {
35
- const data = await apiCall('');
36
- const tags = Array.isArray(data) ? data : data.data || [];
37
- tagsStore = tags.reduce((acc, tag) => { acc[tag.id] = tag; return acc; }, {});
38
- return tags;
39
- } catch (e) {
40
- console.error('Error loading tags:', e);
41
- return [];
42
- }
43
- }
44
-
45
- function esc(text) {
46
- const m = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
47
- return text.replace(/[&<>"']/g, c => m[c]);
48
- }
49
-
50
- function renderTagsList() {
51
- const tags = Object.values(tagsStore).sort((a, b) => (b.usage || 0) - (a.usage || 0));
52
- return `<div class="tags-list">${tags.map(t => `
53
- <div class="tag-item" data-tag-id="${t.id}">
54
- <div class="tag-dot" style="background-color: ${t.color}"></div>
55
- <div class="tag-info"><span class="tag-name">${esc(t.name)}</span><span class="tag-usage">${t.usage || 0} items</span></div>
56
- <div class="tag-actions">
57
- <button class="btn btn-xs btn-ghost" data-action="gtsEditTag" data-args='["${t.id}"]'>Edit</button>
58
- <button class="btn btn-xs btn-ghost text-error" data-action="gtsDeleteTag" data-args='["${t.id}"]'>Delete</button>
59
- </div>
60
- </div>
61
- `).join('') || '<p class="text-xs text-gray-500">No tags created</p>'}</div>`;
62
- }
63
-
64
- function showDialog(mode = 'create', tagId = null) {
65
- const tag = mode === 'edit' ? tagsStore[tagId] : null;
66
- const html = `
67
- <div class="dialog-overlay" id="tag-dialog-overlay" data-action="gtsCloseDialog" onkeydown="if(event.key==='Escape')gtsCloseDialog()" role="dialog" aria-modal="true" aria-labelledby="tag-dialog-heading" aria-hidden="true">
68
- <div class="dialog-content" data-stop-propagation="true">
69
- <div class="dialog-header"><h2 id="tag-dialog-heading">${mode === 'create' ? 'Create New Tag' : 'Edit Tag'}</h2><button class="btn btn-sm btn-ghost" data-action="gtsCloseDialog" aria-label="Close dialog">&times;</button></div>
70
- <div class="dialog-body">
71
- <div class="form-group"><label class="label" for="tag-name-input">Tag Name</label><input type="text" class="input input-bordered w-full" id="tag-name-input" placeholder="e.g., Priority" value="${tag ? esc(tag.name) : ''}" maxlength="50" /></div>
72
- <div class="form-group"><label class="label" for="tag-color-input">Color</label><div class="flex gap-2"><input type="color" id="tag-color-input" value="${tag ? tag.color : '#808080'}" aria-label="Tag color" /><span id="color-preview" class="w-8 h-8 rounded border" style="background-color: ${tag ? tag.color : '#808080'}" aria-hidden="true"></span></div></div>
73
- </div>
74
- <div class="dialog-footer"><button class="btn btn-ghost" data-action="gtsCloseDialog">Cancel</button><button class="btn btn-primary" data-action="gtsSaveTag" data-args='["${mode}","${tagId || ''}"]'>${mode === 'create' ? 'Create' : 'Update'}</button></div>
75
- </div>
76
- </div>`;
77
- const container = document.getElementById('tag-dialog-container') || (c => (document.body.appendChild(c), c))(Object.assign(document.createElement('div'), { id: 'tag-dialog-container' }));
78
- container.innerHTML = html;
79
- document.getElementById('tag-color-input').addEventListener('input', (e) => {
80
- document.getElementById('color-preview').style.backgroundColor = e.target.value;
81
- });
82
- }
83
-
84
- function showToast(msg, type = 'info') {
85
- let c = document.getElementById('toast-container');
86
- if (!c) {
87
- c = document.createElement('div');
88
- c.id = 'toast-container';
89
- c.className = 'fixed bottom-4 right-4 z-50 flex flex-col gap-2';
90
- document.body.appendChild(c);
91
- }
92
- const t = document.createElement('div');
93
- t.className = `alert alert-${type === 'error' ? 'error' : type === 'success' ? 'success' : 'info'} shadow-lg max-w-sm`;
94
- t.textContent = msg;
95
- c.appendChild(t);
96
- setTimeout(() => t.remove(), 3000);
97
- }
98
-
99
- window.gtsOpenDialog = (mode = 'create', tagId = null) => showDialog(mode, tagId);
100
- window.gtsEditTag = (tagId) => gtsOpenDialog('edit', tagId);
101
- window.gtsDeleteTag = async (tagId) => {
102
- if (confirm(`Delete "${esc(tagsStore[tagId].name)}"?`)) {
103
- await deleteTag(tagId);
104
- refreshTagsList();
105
- }
106
- };
107
- window.gtsSaveTag = async (mode, tagId) => {
108
- const name = document.getElementById('tag-name-input').value.trim();
109
- const color = document.getElementById('tag-color-input').value;
110
- if (!name) { showToast('Tag name required', 'error'); return; }
111
- try {
112
- mode === 'create' ? await createTag(name, color) : await updateTag(tagId, name, color);
113
- gtsCloseDialog();
114
- refreshTagsList();
115
- } catch (e) {
116
- showToast('Error: ' + e.message, 'error');
117
- }
118
- };
119
- window.gtsCloseDialog = () => {
120
- const c = document.getElementById('tag-dialog-container');
121
- if (c) c.innerHTML = '';
122
- };
123
-
124
- async function refreshTagsList() {
125
- await listTags();
126
- const c = document.getElementById('tags-list-container');
127
- if (c) c.innerHTML = renderTagsList();
128
- }
129
-
130
- export async function initGlobalTags() {
131
- await listTags();
132
- return { createTag, updateTag, deleteTag, listTags, renderTagsList, refreshTagsList };
133
- }
134
-
135
- export function renderTagsUI() {
136
- return `
137
- <div class="card-clean"><div class="card-clean-body">
138
- <div class="flex justify-between items-center mb-4"><h2 class="card-title text-lg">Global Tags</h2><button class="btn btn-primary btn-sm" data-action="gtsOpenDialog" data-args='["create"]'>New Tag</button></div>
139
- <div id="tags-list-container">${renderTagsList()}</div>
140
- </div></div>
141
- <div id="tag-dialog-container"></div>
142
- <style>.tags-list{display:flex;flex-direction:column;gap:0.75rem}.tag-item{display:flex;align-items:center;gap:1rem;padding:0.75rem;border:1px solid var(--color-border,#e5e7eb);border-radius:0.5rem}.tag-dot{width:20px;height:20px;border-radius:50%;flex-shrink:0}.tag-info{flex:1}.tag-name{font-weight:500;display:block}.tag-usage{font-size:0.75rem;color:var(--color-text-muted,#6b7280);display:block}.tag-actions{display:flex;gap:0.5rem}.dialog-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000}.dialog-content{background:var(--color-surface,white);border-radius:0.5rem;box-shadow:0 10px 25px rgba(0,0,0,0.2);max-width:500px;width:90%;color:var(--color-text,#111)}.dialog-header{padding:1.5rem;border-bottom:1px solid var(--color-border,#e5e7eb);display:flex;justify-content:space-between;align-items:center}.dialog-body{padding:1.5rem}.dialog-footer{padding:1rem 1.5rem;border-top:1px solid var(--color-border,#e5e7eb);display:flex;justify-content:flex-end;gap:0.75rem}.form-group{margin-bottom:1rem}.label{display:block;margin-bottom:0.5rem;font-weight:500}</style>
143
- `;
144
- }