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
@@ -18,7 +18,7 @@ export function serveStatic(pathname, req, res, compress, getCacheHeaders, loadM
18
18
  }
19
19
 
20
20
  if (pathname === '/manifest.json') {
21
- const manifest = JSON.stringify({ name: 'MOONLANDING', short_name: 'Moonlanding', start_url: '/', display: 'standalone', background_color: '#f1f5f9', theme_color: '#04141f' });
21
+ const manifest = JSON.stringify({ name: 'Thatcher', short_name: 'Thatcher', start_url: '/', display: 'standalone', background_color: '#f1f5f9', theme_color: '#04141f' });
22
22
  res.setHeader('Content-Type', 'application/json');
23
23
  res.setHeader('Cache-Control', 'public, max-age=86400');
24
24
  res.setHeader('Content-Length', Buffer.byteLength(manifest, 'utf-8'));
@@ -93,5 +93,5 @@ export function serveStatic(pathname, req, res, compress, getCacheHeaders, loadM
93
93
  }
94
94
 
95
95
  export function html404() {
96
- return `<!DOCTYPE html><html lang="en" data-theme="light"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>404 - Page Not Found | MOONLANDING</title><link href="/ui/rippleui.css" rel="stylesheet"><link href="/ui/styles2.css" rel="stylesheet"><style>body{margin:0;background:var(--color-bg,#f1f5f9);font-family:system-ui,sans-serif}.nav-shell{background:#04141f;padding:0 2rem;height:56px;display:flex;align-items:center}a.logo-link{color:#fff;text-decoration:none;font-weight:700;font-size:1.1rem}.error-shell{min-height:calc(100vh - 56px);display:flex;align-items:center;justify-content:center}.error-card{background:#fff;border-radius:12px;padding:3rem 4rem;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1)}.error-code{font-size:4rem;font-weight:900;color:#04141f;line-height:1}.error-msg{font-size:1.2rem;color:#64748b;margin:0.5rem 0 2rem}.home-btn{display:inline-block;padding:0.75rem 2rem;background:#04141f;color:#fff;border-radius:8px;text-decoration:none;font-weight:600;font-size:0.95rem}</style></head><body><nav class="nav-shell"><a href="/" class="logo-link">MOONLANDING</a></nav><div class="error-shell"><div class="error-card"><div class="error-code">404</div><p class="error-msg">Page not found</p><a href="/" class="home-btn">Go to Dashboard</a></div></div></body></html>`;
96
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><script>(function(){try{var t=localStorage.getItem('thatcher-theme')||((window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light');document.documentElement.setAttribute('data-theme',t)}catch(e){document.documentElement.setAttribute('data-theme','light')}})();</script><meta name="viewport" content="width=device-width,initial-scale=1"><title>404 - Page Not Found | Thatcher</title><link href="/ui/rippleui.css" rel="stylesheet"><link href="/ui/styles2.css" rel="stylesheet"><style>body{margin:0;background:var(--color-bg,#f1f5f9);font-family:system-ui,sans-serif}.nav-shell{background:#04141f;padding:0 2rem;height:56px;display:flex;align-items:center}a.logo-link{color:#fff;text-decoration:none;font-weight:700;font-size:1.1rem}.error-shell{min-height:calc(100vh - 56px);display:flex;align-items:center;justify-content:center}.error-card{background:#fff;border-radius:12px;padding:3rem 4rem;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1)}.error-code{font-size:4rem;font-weight:900;color:#04141f;line-height:1}.error-msg{font-size:1.2rem;color:#64748b;margin:0.5rem 0 2rem}.home-btn{display:inline-block;padding:0.75rem 2rem;background:#04141f;color:#fff;border-radius:8px;text-decoration:none;font-weight:600;font-size:0.95rem}</style></head><body><nav class="nav-shell"><a href="/" class="logo-link">Thatcher</a></nav><div class="error-shell"><div class="error-card"><div class="error-code">404</div><p class="error-msg">Page not found</p><a href="/" class="home-btn">Go to Dashboard</a></div></div></body></html>`;
97
97
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * Status Helpers - Status enumerations and transition maps
3
- * These are the standard status values used by moonlanding parity entities
4
- */
5
-
6
1
  import { getConfigEngineSync } from './config-generator-engine.js';
7
2
 
8
3
  // Engagement statuses
@@ -73,20 +68,10 @@ export const STAGE_TRANSITIONS = {
73
68
  closeout: 'closed',
74
69
  };
75
70
 
76
- /**
77
- * Get next stage in standard lifecycle
78
- * @param {string} currentStage
79
- * @returns {string|null}
80
- */
81
71
  export function getNextStage(currentStage) {
82
72
  return STAGE_TRANSITIONS[currentStage] || null;
83
73
  }
84
74
 
85
- /**
86
- * Get all valid transitions for an engagement
87
- * @param {string} currentStage
88
- * @returns {Array<string>}
89
- */
90
75
  export function getValidTransitions(currentStage) {
91
76
  const transitions = [];
92
77
  for (const [from, to] of Object.entries(STAGE_TRANSITIONS)) {
@@ -131,14 +116,11 @@ function buildEnumFromWorkflow(workflowName) {
131
116
  return result;
132
117
  }
133
118
 
134
- /**
135
- * Human-readable labels for status/stage values.
136
- * Built from thatcher's own enums so labels track thatcher's enum model.
137
- */
138
119
  function titleCase(v) {
139
120
  return String(v).replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
140
121
  }
141
122
 
123
+ // Built from thatcher's own enums so labels track thatcher's enum model.
142
124
  export const STATUS_LABELS = {
143
125
  ...Object.fromEntries(Object.values(ENGAGEMENT_STATUS).map((v) => [v, titleCase(v)])),
144
126
  ...Object.fromEntries(Object.values(ENGAGEMENT_STAGE).map((v) => [v, titleCase(v)])),
@@ -149,44 +131,22 @@ export const STATUS_LABELS = {
149
131
  ...Object.fromEntries(Object.values(HIGHLIGHT_STATUS).map((v) => [v, titleCase(v)])),
150
132
  };
151
133
 
152
- /**
153
- * Get the display label for a status/stage value.
154
- * @param {string} value
155
- * @returns {string}
156
- */
157
134
  export function getStatusLabel(value) {
158
135
  return STATUS_LABELS[value] || value;
159
136
  }
160
137
 
161
- /**
162
- * Resolve engagement stages from config workflow, falling back to ENGAGEMENT_STAGE.
163
- * @returns {Object}
164
- */
165
138
  export function getEngagementStages() {
166
139
  return buildEnumFromWorkflow('engagement_lifecycle') || ENGAGEMENT_STAGE;
167
140
  }
168
141
 
169
- /**
170
- * Resolve RFI states from config workflow, falling back to RFI_STATUS.
171
- * @returns {Object}
172
- */
173
142
  export function getRfiStates() {
174
143
  return buildEnumFromWorkflow('rfi_type_standard') || RFI_STATUS;
175
144
  }
176
145
 
177
- /**
178
- * Resolve review stages from config workflow, falling back to REVIEW_STATUS.
179
- * @returns {Object}
180
- */
181
146
  export function getReviewStages() {
182
147
  return buildEnumFromWorkflow('review_lifecycle') || REVIEW_STATUS;
183
148
  }
184
149
 
185
- /**
186
- * Resolve the engagement stage transition map from config, falling back to
187
- * thatcher's static STAGE_TRANSITIONS.
188
- * @returns {Object<string,string>}
189
- */
190
150
  export function getStageTransitions() {
191
151
  const stages = getCachedConfig()?.workflows?.engagement_lifecycle?.stages;
192
152
  if (!stages) return STAGE_TRANSITIONS;
@@ -199,13 +159,6 @@ export function getStageTransitions() {
199
159
  return result;
200
160
  }
201
161
 
202
- /**
203
- * Check whether a transition between two states is valid for an entity type.
204
- * @param {string} entityType
205
- * @param {string} from
206
- * @param {string} to
207
- * @returns {boolean}
208
- */
209
162
  export function isValidTransition(entityType, from, to) {
210
163
  if (entityType === 'engagement') {
211
164
  const transitions = getStageTransitions();
@@ -218,9 +171,7 @@ export function isValidTransition(entityType, from, to) {
218
171
  return false;
219
172
  }
220
173
 
221
- /**
222
- * Clear the cached config snapshot (call after a config hot-reload).
223
- */
174
+ // Call after a config hot-reload to force re-read.
224
175
  export function clearCachedConfig() {
225
176
  _cachedConfig = null;
226
177
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * Observability Tracing Core - OpenTelemetry-style distributed tracing for Thatcher
3
- * Provides trace/span lifecycle, async context propagation, and trace export
4
- */
5
-
6
1
  import { AsyncLocalStorage } from 'async_hooks';
7
2
  import { createLogger } from './logger.js';
8
3
 
package/src/lib/utils.js CHANGED
@@ -1,24 +1,13 @@
1
- /**
2
- * Utility Functions - Common helpers
3
- */
4
-
5
- import { list } from './query-engine.js';
1
+ import { list } from './busybase-store.js';
6
2
  import { getSpec } from '../config/spec-helpers.js';
7
3
 
8
- /**
9
- * Load ref-field options for a spec's form (ported from moonlanding).
10
- * For each `ref` field, fetch the referenced entity's rows and build
11
- * `{ value, label }` option lists. `engagement` refs get a richer label
12
- * and skip archived rows. Failures degrade to an empty option list.
13
- * @param {object} spec - Entity spec with a `fields` map
14
- * @returns {Promise<Record<string, Array<{value:any,label:string}>>>}
15
- */
4
+ // Ported from moonlanding. engagement refs get a richer label and skip archived rows; failures degrade to [].
16
5
  export async function loadFormOptions(spec) {
17
6
  const options = {};
18
7
  for (const [key, field] of Object.entries(spec.fields || {})) {
19
8
  if (field.type === 'ref' && field.ref) {
20
9
  try {
21
- const data = list(field.ref);
10
+ const data = await list(field.ref);
22
11
  if (field.ref === 'engagement') {
23
12
  options[key] = data
24
13
  .filter(r => r.status !== 'archived')
@@ -40,9 +29,7 @@ export async function loadFormOptions(spec) {
40
29
  return options;
41
30
  }
42
31
 
43
- /**
44
- * Error thrown when an entity spec cannot be resolved (ported from moonlanding).
45
- */
32
+ // Ported from moonlanding.
46
33
  export class SpecError extends Error {
47
34
  constructor(entity) {
48
35
  super(`Unknown entity: ${entity}`);
@@ -51,12 +38,7 @@ export class SpecError extends Error {
51
38
  }
52
39
  }
53
40
 
54
- /**
55
- * Resolve an entity spec, throwing a typed SpecError on failure (ported from
56
- * moonlanding). Uses thatcher's single-arg getSpec convention.
57
- * @param {string} entity
58
- * @returns {Promise<object>}
59
- */
41
+ // Ported from moonlanding. Uses thatcher's single-arg getSpec convention.
60
42
  export async function resolveSpec(entity) {
61
43
  try {
62
44
  const spec = getSpec(entity);
@@ -68,21 +50,11 @@ export async function resolveSpec(entity) {
68
50
  }
69
51
  }
70
52
 
71
- /**
72
- * Get display name for user
73
- * @param {object} user - User with name or email
74
- * @returns {string}
75
- */
76
53
  export function getDisplayName(user) {
77
54
  if (!user) return 'Unknown';
78
55
  return user.name || user.email || user.id || 'Unknown';
79
56
  }
80
57
 
81
- /**
82
- * Get initials from name or email
83
- * @param {string|object} userOrName
84
- * @returns {string}
85
- */
86
58
  export function getInitials(userOrName) {
87
59
  let name = typeof userOrName === 'string' ? userOrName : getDisplayName(userOrName);
88
60
  const parts = name.trim().split(/\s+/);
@@ -92,20 +64,10 @@ export function getInitials(userOrName) {
92
64
  return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
93
65
  }
94
66
 
95
- /**
96
- * Get user role
97
- * @param {object} user
98
- * @returns {string}
99
- */
100
67
  export function getUserRole(user) {
101
68
  return user?.role || 'user';
102
69
  }
103
70
 
104
- /**
105
- * Slugify string (URL-safe)
106
- * @param {string} str
107
- * @returns {string}
108
- */
109
71
  export function slugify(str) {
110
72
  return str
111
73
  .toLowerCase()
@@ -115,25 +77,12 @@ export function slugify(str) {
115
77
  .replace(/^-+|-+$/g, '');
116
78
  }
117
79
 
118
- /**
119
- * Truncate text
120
- * @param {string} text
121
- * @param {number} maxLength
122
- * @param {string} suffix
123
- * @returns {string}
124
- */
125
80
  export function truncate(text, maxLength = 50, suffix = '...') {
126
81
  if (!text) return '';
127
82
  if (text.length <= maxLength) return text;
128
83
  return text.slice(0, maxLength - suffix.length) + suffix;
129
84
  }
130
85
 
131
- /**
132
- * Deep merge objects
133
- * @param {object} target
134
- * @param {object} source
135
- * @returns {object}
136
- */
137
86
  export function deepMerge(target, source) {
138
87
  const output = { ...target };
139
88
  for (const key of Object.keys(source)) {
@@ -146,10 +95,6 @@ export function deepMerge(target, source) {
146
95
  return output;
147
96
  }
148
97
 
149
- /**
150
- * Generate a random color
151
- * @returns {string} Hex color
152
- */
153
98
  export function randomColor() {
154
99
  const colors = [
155
100
  '#228be6', '#40c057', '#fab005', '#fa5252', '#15aabf',
@@ -158,13 +103,7 @@ export function randomColor() {
158
103
  return colors[Math.floor(Math.random() * colors.length)];
159
104
  }
160
105
 
161
- /**
162
- * Determine whether a user record is active (ported from moonlanding).
163
- * Treats explicit inactive/disabled/suspended status, falsy `is_active`,
164
- * or a set `deleted_at` as inactive.
165
- * @param {object} user
166
- * @returns {boolean}
167
- */
106
+ // Ported from moonlanding. inactive/disabled/suspended status, falsy is_active, or set deleted_at all count as inactive.
168
107
  export function isUserActive(user) {
169
108
  if (!user) return false;
170
109
  if (user.is_active === false || user.is_active === 0) return false;
@@ -1,7 +1,4 @@
1
- /**
2
- * Validation System - Field and entity validation
3
- * Adapted from moonlanding/src/lib/validate.js
4
- */
1
+ // Adapted from moonlanding/src/lib/validate.js
5
2
 
6
3
  import { getSpec } from '../config/spec-helpers.js';
7
4
  import { isValidEmail as checkEmailFormat, isValidEmail } from './validators.js';
@@ -13,22 +10,10 @@ export { isValidEmail };
13
10
 
14
11
  const HTML_ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
15
12
 
16
- /**
17
- * Sanitize HTML
18
- * @param {string} str
19
- * @returns {string}
20
- */
21
13
  function sanitizeHtml(str) {
22
14
  return typeof str === 'string' ? str.replace(/[&<>"']/g, c => HTML_ESC[c]) : str;
23
15
  }
24
16
 
25
- /**
26
- * Validate a single field
27
- * @param {object} fieldDef
28
- * @param {any} value
29
- * @param {object} options
30
- * @returns {Promise<{valid: boolean, error?: string}>}
31
- */
32
17
  export async function validateField(fieldDef, value, options = {}) {
33
18
  const { fieldName, entityName, existingValue } = options;
34
19
 
@@ -65,9 +50,9 @@ export async function validateField(fieldDef, value, options = {}) {
65
50
  return { valid: true };
66
51
  }
67
52
  try {
68
- const { get } = await import('./query-engine.js');
53
+ const { getBy } = await import('./busybase-store.js');
69
54
  const refTable = fieldDef.ref === 'user' ? 'users' : fieldDef.ref;
70
- if (!get(refTable, value)) {
55
+ if (!(await getBy(refTable, 'id', value))) {
71
56
  return {
72
57
  valid: false,
73
58
  error: `${fieldDef.ref.charAt(0).toUpperCase() + fieldDef.ref.slice(1)} with id '${value}' not found`,
@@ -81,13 +66,6 @@ export async function validateField(fieldDef, value, options = {}) {
81
66
  return { valid: true };
82
67
  }
83
68
 
84
- /**
85
- * Validate type of value
86
- * @param {object} fieldDef
87
- * @param {any} value
88
- * @param {string} fieldName
89
- * @returns {string|null}
90
- */
91
69
  function validateType(fieldDef, value, fieldName) {
92
70
  const { type, min, max } = fieldDef;
93
71
 
@@ -112,12 +90,6 @@ function validateType(fieldDef, value, fieldName) {
112
90
  return null;
113
91
  }
114
92
 
115
- /**
116
- * Resolve enum options from field definition
117
- * @param {object} fieldDef
118
- * @param {string} entityName
119
- * @returns {Array<string>}
120
- */
121
93
  function resolveEnumOptions(fieldDef, entityName) {
122
94
  if (Array.isArray(fieldDef.options)) {
123
95
  return fieldDef.options.map(o => typeof o === 'object' ? o.value : o);
@@ -136,13 +108,6 @@ function resolveEnumOptions(fieldDef, entityName) {
136
108
  return [];
137
109
  }
138
110
 
139
- /**
140
- * Validate all fields for an entity
141
- * @param {string} entityName
142
- * @param {object} data
143
- * @param {object} existingRecord
144
- * @returns {Promise<object>} Errors object keyed by field name
145
- */
146
111
  export async function validateEntity(entityName, data, existingRecord = null) {
147
112
  const spec = getSpec(entityName);
148
113
  const errors = {};
@@ -170,23 +135,15 @@ export async function validateEntity(entityName, data, existingRecord = null) {
170
135
  return errors;
171
136
  }
172
137
 
173
- /**
174
- * Enforce a field's `unique` constraint against the datastore.
175
- * Returns an error string if a duplicate exists, otherwise null.
176
- * @param {object} fieldDef
177
- * @param {any} value
178
- * @param {object} options - { fieldName, entityName, existingRecord }
179
- * @returns {Promise<string|null>}
180
- */
181
138
  async function checkUnique(fieldDef, value, { fieldName, entityName, existingRecord }) {
182
139
  if (!fieldDef.unique) return null;
183
140
  if (value == null || value === '') return null;
184
141
  const existingValue = existingRecord?.[fieldName];
185
142
  if (value === existingValue) return null;
186
143
  try {
187
- const { get } = await import('./query-engine.js');
144
+ const { getBy } = await import('./busybase-store.js');
188
145
  const table = entityName === 'user' ? 'users' : entityName;
189
- const dup = get(table, undefined, { [fieldName]: value });
146
+ const dup = await getBy(table, fieldName, value);
190
147
  if (dup && (!existingRecord || dup.id !== existingRecord.id)) {
191
148
  return `Field '${fieldName}' must be unique`;
192
149
  }
@@ -196,13 +153,6 @@ async function checkUnique(fieldDef, value, { fieldName, entityName, existingRec
196
153
  return null;
197
154
  }
198
155
 
199
- /**
200
- * Validate update (only changed fields)
201
- * @param {string} entityName
202
- * @param {object} changes
203
- * @param {object} existingRecord
204
- * @returns {Promise<object>}
205
- */
206
156
  export async function validateUpdate(entityName, changes, existingRecord) {
207
157
  const spec = getSpec(entityName);
208
158
  const errors = {};
@@ -232,23 +182,10 @@ export async function validateUpdate(entityName, changes, existingRecord) {
232
182
  return errors;
233
183
  }
234
184
 
235
- /**
236
- * Check if errors object has any errors
237
- * @param {object} errors
238
- * @returns {boolean}
239
- */
240
185
  export function hasErrors(errors) {
241
186
  return errors && Object.keys(errors).length > 0;
242
187
  }
243
188
 
244
- /**
245
- * Validate a status/stage transition is permitted.
246
- * Uses thatcher's STAGE_TRANSITIONS graph via getValidTransitions.
247
- * @param {string} entityType
248
- * @param {string} currentStatus
249
- * @param {string} newStatus
250
- * @returns {{valid: boolean, reason?: string}}
251
- */
252
189
  export function validateStatusTransition(entityType, currentStatus, newStatus) {
253
190
  if (!currentStatus || !newStatus) {
254
191
  return { valid: false, reason: 'Status values required' };
@@ -264,13 +201,6 @@ export function validateStatusTransition(entityType, currentStatus, newStatus) {
264
201
  return { valid: true };
265
202
  }
266
203
 
267
- /**
268
- * Validate that an end date is not before a start date.
269
- * @param {number} startSeconds - Unix timestamp (seconds)
270
- * @param {number} endSeconds - Unix timestamp (seconds)
271
- * @param {string} label
272
- * @returns {{valid: boolean, reason?: string}}
273
- */
274
204
  export function validateDateRange(startSeconds, endSeconds, label = 'date') {
275
205
  if (!startSeconds || !endSeconds) return { valid: true };
276
206
  if (isBeforeDate(endSeconds, startSeconds)) {
@@ -279,13 +209,6 @@ export function validateDateRange(startSeconds, endSeconds, label = 'date') {
279
209
  return { valid: true };
280
210
  }
281
211
 
282
- /**
283
- * Validate a deadline: required, not before a reference date, within maxYears.
284
- * @param {number} deadlineSeconds - Unix timestamp (seconds)
285
- * @param {number} [referenceSeconds] - Unix timestamp (seconds)
286
- * @param {number} [maxYears=2]
287
- * @returns {{valid: boolean, reason?: string}}
288
- */
289
212
  export function validateDeadline(deadlineSeconds, referenceSeconds, maxYears = 2) {
290
213
  if (!deadlineSeconds) return { valid: false, reason: 'Deadline is required' };
291
214
  if (referenceSeconds && isBeforeDate(deadlineSeconds, referenceSeconds)) {
@@ -302,12 +225,6 @@ export function validateDeadline(deadlineSeconds, referenceSeconds, maxYears = 2
302
225
  return { valid: true };
303
226
  }
304
227
 
305
- /**
306
- * Sanitize string/text fields of a record using the entity spec.
307
- * @param {object} data
308
- * @param {object} spec
309
- * @returns {object} Sanitized shallow copy
310
- */
311
228
  export function sanitizeData(data, spec) {
312
229
  const sanitized = { ...data };
313
230
  for (const [fieldName, value] of Object.entries(sanitized)) {
@@ -1,12 +1,3 @@
1
- /**
2
- * Validation Utilities - Reusable validation functions
3
- */
4
-
5
- /**
6
- * Validate email address
7
- * @param {string} email
8
- * @returns {{valid: boolean, email?: string, domain?: string, reason?: string}}
9
- */
10
1
  export function isValidEmail(email) {
11
2
  if (!email || typeof email !== 'string') {
12
3
  return { valid: false, reason: 'Email is required' };
@@ -37,11 +28,6 @@ export function isValidEmail(email) {
37
28
  return { valid: true, email: trimmed, domain };
38
29
  }
39
30
 
40
- /**
41
- * Validate URL
42
- * @param {string} url
43
- * @returns {boolean}
44
- */
45
31
  export function isValidUrl(url) {
46
32
  try {
47
33
  new URL(url);
@@ -51,13 +37,6 @@ export function isValidUrl(url) {
51
37
  }
52
38
  }
53
39
 
54
- /**
55
- * Validate date is within range
56
- * @param {number} timestamp - Unix timestamp (seconds)
57
- * @param {number} minYearsAgo
58
- * @param {number} maxYearsAhead
59
- * @returns {boolean}
60
- */
61
40
  export function isWithinYears(timestamp, minYearsAgo = 10, maxYearsAhead = 5) {
62
41
  const now = Date.now() / 1000;
63
42
  const secondsInYear = 365.25 * 24 * 60 * 60;
@@ -67,21 +46,10 @@ export function isWithinYears(timestamp, minYearsAgo = 10, maxYearsAhead = 5) {
67
46
  );
68
47
  }
69
48
 
70
- /**
71
- * Check if date1 is before date2
72
- * @param {number} date1 - Unix timestamp
73
- * @param {number} date2 - Unix timestamp
74
- * @returns {boolean}
75
- */
76
49
  export function isBeforeDate(date1, date2) {
77
50
  return date1 < date2;
78
51
  }
79
52
 
80
- /**
81
- * Slugify string
82
- * @param {string} str
83
- * @returns {string}
84
- */
85
53
  export function slugify(str) {
86
54
  return str
87
55
  .toLowerCase()
@@ -91,12 +59,6 @@ export function slugify(str) {
91
59
  .replace(/^-+|-+$/g, '');
92
60
  }
93
61
 
94
- /**
95
- * Validate required fields object
96
- * @param {object} data
97
- * @param {Array<string>} requiredFields
98
- * @returns {Array<string>} Missing field names
99
- */
100
62
  export function getMissingFields(data, requiredFields) {
101
63
  return requiredFields.filter(field => {
102
64
  const val = data[field];
@@ -4,7 +4,7 @@ const extractEntityInfo = (request, params = {}) => {
4
4
  const url = new URL(request.url);
5
5
  const parts = url.pathname.split('/').filter(Boolean);
6
6
  const entityType = parts[parts.length - 2] || 'unknown';
7
- const entityId = params.id || params.engagementId || params.reviewId || params.highlightId || params.permissionId || parts[parts.length - 1];
7
+ const entityId = params.id || parts[parts.length - 1];
8
8
  return { entityType, entityId };
9
9
  };
10
10