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.
- package/README.md +4 -16
- package/package.json +12 -18
- package/src/adapters/google-auth.js +4 -43
- package/src/adapters/google-drive.js +12 -67
- package/src/app/api/audit/dashboard/route.js +14 -11
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +6 -3
- package/src/app/api/audit/permissions/route.js +10 -7
- package/src/app/api/audit/permissions/stats/route.js +8 -5
- package/src/app/api/audit/route.js +4 -4
- package/src/app/api/audit/stats/route.js +1 -1
- package/src/app/api/auth/google/callback/route.js +5 -2
- package/src/app/api/auth/google/route.js +4 -1
- package/src/app/api/auth/login/route.js +5 -2
- package/src/app/api/auth/mwr-bridge/route.js +11 -21
- package/src/app/api/auth/password-reset/route.js +15 -14
- package/src/app/api/cron/trigger/route.js +4 -1
- package/src/app/api/debug/[[...path]]/route.js +6 -5
- package/src/app/api/debug/sqlite/route.js +7 -12
- package/src/app/api/debug/sync/route.js +5 -5
- package/src/app/api/domains/route.js +4 -1
- package/src/app/api/email/allocate/batch/route.js +19 -29
- package/src/app/api/email/allocate/route.js +16 -17
- package/src/app/api/email/receive/route.js +31 -42
- package/src/app/api/email/send/route.js +28 -16
- package/src/app/api/email/unallocated/route.js +10 -13
- package/src/app/api/files/[id]/route.js +4 -1
- package/src/app/api/formula/[[...path]]/route.js +0 -4
- package/src/app/api/health/route.js +10 -9
- package/src/app/api/metrics/route.js +1 -1
- package/src/cli.js +4 -4
- package/src/config/config-loader.js +1 -21
- package/src/config/constants.js +0 -35
- package/src/config/env.js +0 -30
- package/src/config/spec-helpers.js +6 -95
- package/src/engine.js +23 -0
- package/src/engine.server.js +8 -78
- package/src/index.js +42 -67
- package/src/lib/alert-manager.js +7 -3
- package/src/lib/api-helpers.js +4 -3
- package/src/lib/api.js +10 -10
- package/src/lib/audit-logger-enhanced.js +77 -83
- package/src/lib/auth-middleware.js +1 -32
- package/src/lib/auth-route-helpers.js +5 -0
- package/src/lib/busybase-adapter.js +0 -5
- package/src/lib/busybase-audit-reads.js +142 -0
- package/src/lib/busybase-audit.js +41 -0
- package/src/lib/busybase-lucia-adapter.js +72 -0
- package/src/lib/busybase-store.js +338 -0
- package/src/lib/config-field-helpers.js +0 -17
- package/src/lib/config-generator-engine.js +19 -36
- package/src/lib/config-helpers.js +0 -21
- package/src/lib/crud-action-helpers.js +2 -2
- package/src/lib/crud-factory.js +1 -16
- package/src/lib/crud-handlers.js +17 -52
- package/src/lib/csrf-protection.js +5 -2
- package/src/lib/date-utils.js +3 -127
- package/src/lib/debug-registry.js +0 -5
- package/src/lib/domain-loader.js +4 -1
- package/src/lib/email-sender.js +25 -23
- package/src/lib/error-handler.js +5 -41
- package/src/lib/error-recovery.js +10 -7
- package/src/lib/error-resilience.js +7 -5
- package/src/lib/events-engine.js +9 -6
- package/src/lib/export-sink.js +0 -5
- package/src/lib/field-iterator.js +7 -76
- package/src/lib/field-registry.js +0 -22
- package/src/lib/generic-crud-handler.js +0 -16
- package/src/lib/hook-engine.js +5 -53
- package/src/lib/hot-reload/checkpoint.js +5 -2
- package/src/lib/hot-reload/promise-container.js +7 -3
- package/src/lib/hot-reload/route-wrapper.js +4 -1
- package/src/lib/hot-reload/supervisor.js +6 -3
- package/src/lib/hyperformula-service.js +0 -5
- package/src/lib/id-helpers.js +10 -0
- package/src/lib/index.js +4 -7
- package/src/lib/log-aggregator.js +0 -1
- package/src/lib/logger.js +0 -15
- package/src/lib/metrics-collector.js +2 -46
- package/src/lib/next-compat.js +15 -1
- package/src/lib/observability-bootstrap.js +0 -5
- package/src/lib/perf-monitor.js +4 -1
- package/src/lib/perf-profiler.js +0 -5
- package/src/lib/query-string-adapter.js +5 -47
- package/src/lib/realtime-server.js +4 -26
- package/src/lib/request-tracing.js +0 -5
- package/src/lib/resource-monitor.js +4 -1
- package/src/lib/response-formatter.js +0 -38
- package/src/lib/route-helpers.js +3 -1
- package/src/lib/safe-json.js +5 -1
- package/src/lib/server-bootstrap.js +10 -9
- package/src/lib/stage-pipeline.js +2 -2
- package/src/lib/state-transport-client.js +4 -1
- package/src/lib/static-server.js +2 -2
- package/src/lib/status-helpers.js +2 -51
- package/src/lib/tracing.js +0 -5
- package/src/lib/utils.js +6 -67
- package/src/lib/validate.js +5 -88
- package/src/lib/validators.js +0 -38
- package/src/lib/with-audit-logging.js +1 -1
- package/src/lib/workflow-engine.js +10 -77
- package/src/lib/xstate-workflow-engine.js +3 -9
- package/src/plugins/index.js +5 -3
- package/src/server/server.js +26 -27
- package/src/services/collaborator-role.service.js +21 -77
- package/src/services/email-sender.js +7 -34
- package/src/services/notification-engine.js +12 -33
- package/src/services/permission.service.js +10 -73
- package/src/ui/advanced-search-renderer.js +3 -2
- package/src/ui/advanced-widgets.js +1 -1
- package/src/ui/auth-pages.js +10 -9
- package/src/ui/auth-styles.js +1 -1
- package/src/ui/client.js +1 -1
- package/src/ui/component-engine.js +1 -1
- package/src/ui/dashboard-renderer.js +5 -5
- package/src/ui/dialog-engine.js +6 -9
- package/src/ui/dialog-factory.js +2 -2
- package/src/ui/engagement-cards.js +7 -7
- package/src/ui/engagement-dialogs.js +2 -3
- package/src/ui/engagement-grid-renderer.js +20 -19
- package/src/ui/entity-renderer.js +56 -42
- package/src/ui/file-dialogs.js +25 -9
- package/src/ui/flexup-report-renderer.js +21 -18
- package/src/ui/format-helpers.js +27 -28
- package/src/ui/highlight-threading-renderer.js +9 -9
- package/src/ui/job-management-renderer.js +18 -16
- package/src/ui/layout.js +51 -25
- package/src/ui/ml-console-renderer.js +15 -16
- package/src/ui/monitoring-dashboard-client.js +9 -8
- package/src/ui/notifications-renderer.js +11 -10
- package/src/ui/page-handler-admin.js +32 -23
- package/src/ui/page-handler-helpers.js +27 -23
- package/src/ui/page-handler-reviews.js +71 -35
- package/src/ui/page-handler-rfi.js +28 -14
- package/src/ui/page-handler.js +65 -48
- package/src/ui/picker-dialogs.js +26 -19
- package/src/ui/render-helpers.js +84 -46
- package/src/ui/review-comparison-renderer.js +8 -7
- package/src/ui/review-detail-panels.js +8 -9
- package/src/ui/review-detail-renderer.js +3 -3
- package/src/ui/review-detail-script.js +13 -12
- package/src/ui/review-mwr-renderer.js +6 -5
- package/src/ui/review-renderer.js +7 -6
- package/src/ui/review-widgets.js +3 -3
- package/src/ui/rfi-detail-renderer.js +37 -20
- package/src/ui/rfi-report-renderer.js +14 -9
- package/src/ui/settings-renderer-advanced.js +21 -18
- package/src/ui/settings-renderer-advanced2.js +20 -19
- package/src/ui/settings-renderer-teams.js +4 -4
- package/src/ui/settings-renderer.js +4 -4
- package/src/ui/spacing-system.js +5 -2
- package/src/ui/standalone-login.js +2 -2
- package/src/ui/styles.css +2 -1
- package/src/ui/styles2.css +75 -5
- package/src/ui/test-page.js +4 -4
- package/src/ui/widgets.js +20 -15
- package/src/lib/audit-logger.js +0 -193
- package/src/lib/database-core.js +0 -255
- package/src/lib/database-migrations.js +0 -350
- package/src/lib/db-backup.js +0 -97
- package/src/lib/error-boundary.js +0 -134
- package/src/lib/errors.js +0 -69
- package/src/lib/health-monitor.js +0 -134
- package/src/lib/index-optimizer.js +0 -96
- package/src/lib/query-engine-write.js +0 -221
- package/src/lib/query-engine.js +0 -446
- package/src/ui/collaboration-dialogs.js +0 -31
- package/src/ui/global-tags.js +0 -144
- package/src/ui/idle-logout.js +0 -156
- package/src/ui/letter-dialogs.js +0 -37
- package/src/ui/password-reset-page.js +0 -146
- package/src/ui/perf-renderer.js +0 -71
- package/src/ui/rfi-renderer.js +0 -194
- package/src/ui/validation-ui.js +0 -147
package/src/lib/static-server.js
CHANGED
|
@@ -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: '
|
|
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"
|
|
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
|
}
|
package/src/lib/tracing.js
CHANGED
|
@@ -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;
|
package/src/lib/validate.js
CHANGED
|
@@ -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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
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 {
|
|
53
|
+
const { getBy } = await import('./busybase-store.js');
|
|
69
54
|
const refTable = fieldDef.ref === 'user' ? 'users' : fieldDef.ref;
|
|
70
|
-
if (!
|
|
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 {
|
|
144
|
+
const { getBy } = await import('./busybase-store.js');
|
|
188
145
|
const table = entityName === 'user' ? 'users' : entityName;
|
|
189
|
-
const dup =
|
|
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)) {
|
package/src/lib/validators.js
CHANGED
|
@@ -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 ||
|
|
7
|
+
const entityId = params.id || parts[parts.length - 1];
|
|
8
8
|
return { entityType, entityId };
|
|
9
9
|
};
|
|
10
10
|
|