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
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
2
|
+
import { retryWithBackoff, withCircuitBreaker, checkpoint, restoreCheckpoint, logRecovery } from '@/lib/error-resilience';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[Recovery]');
|
|
5
|
+
import { normalizeError, AppError } from '@/lib/error-handler';
|
|
3
6
|
import { HTTP } from '@/config/constants';
|
|
4
7
|
|
|
5
8
|
const recoveryState = { supervisors: new Map(), lastHealthCheck: null };
|
|
@@ -37,7 +40,7 @@ export async function supervise(name, fn, options = {}) {
|
|
|
37
40
|
);
|
|
38
41
|
supervisor.state = 'failed';
|
|
39
42
|
supervisor.lastError = error;
|
|
40
|
-
|
|
43
|
+
log.error(`${name} failed permanently after ${supervisor.restarts.length} restarts`);
|
|
41
44
|
throw error;
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -58,7 +61,7 @@ export async function supervise(name, fn, options = {}) {
|
|
|
58
61
|
|
|
59
62
|
if (supervisor.restarts.length < supervisor.maxRestarts) {
|
|
60
63
|
const delay = Math.min(1000 * Math.pow(2, supervisor.restarts.length - 1), 30000);
|
|
61
|
-
|
|
64
|
+
log.warn(`${name} restarting in ${delay}ms (attempt ${supervisor.restarts.length})`);
|
|
62
65
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
63
66
|
return supervise(name, fn, options);
|
|
64
67
|
}
|
|
@@ -79,7 +82,7 @@ export async function degradedMode(fn, fallback, options = {}) {
|
|
|
79
82
|
]);
|
|
80
83
|
return { mode: 'normal', result };
|
|
81
84
|
} catch (error) {
|
|
82
|
-
|
|
85
|
+
log.warn('falling back to degraded mode', { error: error.message });
|
|
83
86
|
const result = await fallback();
|
|
84
87
|
return { mode: 'degraded', result };
|
|
85
88
|
}
|
|
@@ -115,7 +118,7 @@ export function isolateFailure(fn, defaultValue = null) {
|
|
|
115
118
|
try {
|
|
116
119
|
return await fn(...args);
|
|
117
120
|
} catch (error) {
|
|
118
|
-
|
|
121
|
+
log.error('failure contained', {
|
|
119
122
|
function: fn.name,
|
|
120
123
|
error: String(error.message || error)
|
|
121
124
|
});
|
|
@@ -163,7 +166,7 @@ export async function withRecovery(fn, options = {}) {
|
|
|
163
166
|
return result;
|
|
164
167
|
} catch (error) {
|
|
165
168
|
if (fallback) {
|
|
166
|
-
|
|
169
|
+
log.warn('using fallback after error', { error: error.message });
|
|
167
170
|
return fallback();
|
|
168
171
|
}
|
|
169
172
|
throw error;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
1
2
|
import { HTTP } from '@/config/constants';
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[Resilience]');
|
|
5
|
+
import { AppError, normalizeError } from '@/lib/error-handler';
|
|
4
6
|
|
|
5
7
|
const errorState = { errors: [], circuitBreakers: new Map(), checkpoints: new Map() };
|
|
6
8
|
|
|
@@ -13,12 +15,12 @@ export async function retryWithBackoff(fn, options = {}) {
|
|
|
13
15
|
} catch (error) {
|
|
14
16
|
if (attempt === maxAttempts) {
|
|
15
17
|
const normalized = normalizeError(error);
|
|
16
|
-
|
|
18
|
+
log.error('final attempt failed:', { ...context, attempt, error: normalized.toJSON() });
|
|
17
19
|
throw normalized;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const waitTime = delay * Math.pow(backoff, attempt - 1);
|
|
21
|
-
|
|
23
|
+
log.warn(`attempt ${attempt} failed, retrying in ${waitTime}ms`, context);
|
|
22
24
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -66,7 +68,7 @@ export async function withCircuitBreaker(name, fn, options = {}) {
|
|
|
66
68
|
if (breaker.failures >= breaker.threshold) {
|
|
67
69
|
breaker.state = 'open';
|
|
68
70
|
breaker.nextAttempt = Date.now() + breaker.resetTimeout;
|
|
69
|
-
|
|
71
|
+
log.error(`circuit ${name} opened after ${breaker.failures} failures`);
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
throw error;
|
package/src/lib/events-engine.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
1
2
|
import { hookEngine } from '@/lib/hook-engine.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[EventsEngine]');
|
|
2
5
|
import { list, get, update, create, remove } from '@/engine.js';
|
|
3
6
|
import { queueEmail } from '@/engine/notification-engine';
|
|
4
7
|
import { safeJsonParse } from '@/lib/safe-json.js';
|
|
@@ -31,7 +34,7 @@ const activateWorkflowsForStage = async (engagementId, stage, user) => {
|
|
|
31
34
|
hookEngine.execute(`feature:activated:${feature}`, { engagementId, stage, feature, user });
|
|
32
35
|
}
|
|
33
36
|
} catch (error) {
|
|
34
|
-
|
|
37
|
+
log.error(`error activating workflows for stage ${stage}:`, { message: error.message });
|
|
35
38
|
}
|
|
36
39
|
};
|
|
37
40
|
|
|
@@ -47,7 +50,7 @@ export const registerEntityHandlers = () => {
|
|
|
47
50
|
create('activity_log', { entity_type: 'engagement', entity_id: context.id, action: 'stage_transition',
|
|
48
51
|
message: `Stage transitioned: ${prev.stage} -> ${data.stage}`,
|
|
49
52
|
details: JSON.stringify({ from: prev.stage, to: data.stage, user_role: user?.role }), user_email: user?.email }, user);
|
|
50
|
-
} catch (e) {
|
|
53
|
+
} catch (e) { log.error('failed to log stage change:', { message: e.message }); }
|
|
51
54
|
return context;
|
|
52
55
|
}, { priority: 100, name: 'workflow-stage-validator' });
|
|
53
56
|
|
|
@@ -70,7 +73,7 @@ export const registerEntityHandlers = () => {
|
|
|
70
73
|
|
|
71
74
|
hookEngine.on('engagement:afterUpdate', async (engagement, changes, prev, user) => {
|
|
72
75
|
if (changes.stage && changes.stage !== prev.stage) {
|
|
73
|
-
logActivity('engagement', engagement.id, 'stage_change', `Stage: ${prev.stage}
|
|
76
|
+
logActivity('engagement', engagement.id, 'stage_change', `Stage: ${prev.stage} -> ${changes.stage}`, user, { from: prev.stage, to: changes.stage });
|
|
74
77
|
const stageActions = {
|
|
75
78
|
commencement: () => queueEmail('engagement_commencement', { engagement, recipients: 'client_users' }),
|
|
76
79
|
finalization: async () => {
|
|
@@ -85,7 +88,7 @@ export const registerEntityHandlers = () => {
|
|
|
85
88
|
await stageActions[changes.stage]?.();
|
|
86
89
|
}
|
|
87
90
|
if (changes.status && changes.status !== prev.status) {
|
|
88
|
-
logActivity('engagement', engagement.id, 'status_change', `Status: ${prev.status}
|
|
91
|
+
logActivity('engagement', engagement.id, 'status_change', `Status: ${prev.status} -> ${changes.status}`, user);
|
|
89
92
|
}
|
|
90
93
|
if (changes.commencement_date && changes.commencement_date !== prev.commencement_date) {
|
|
91
94
|
await queueEmail('engagement_date_change', { engagement, field: 'commencement_date', recipients: 'team_members' });
|
|
@@ -118,7 +121,7 @@ export const registerEntityHandlers = () => {
|
|
|
118
121
|
if (changes.client_status && changes.client_status !== prev.client_status) {
|
|
119
122
|
await queueEmail('rfi_status_change', { rfi, from: prev.client_status, to: changes.client_status, recipients: 'assigned_users' });
|
|
120
123
|
}
|
|
121
|
-
logActivity('rfi', rfi.id, 'status_change', `Status: ${prev.status}
|
|
124
|
+
logActivity('rfi', rfi.id, 'status_change', `Status: ${prev.status} -> ${changes.status}`, user);
|
|
122
125
|
}
|
|
123
126
|
if (changes.deadline && changes.deadline !== prev.deadline) {
|
|
124
127
|
await queueEmail('rfi_deadline_change', { rfi, newDeadline: changes.deadline, recipients: 'assigned_users' });
|
|
@@ -160,7 +163,7 @@ export const registerEntityHandlers = () => {
|
|
|
160
163
|
hookEngine.on('review:afterUpdate', async (review, changes, prev, user) => {
|
|
161
164
|
if (changes.status && changes.status !== prev.status) {
|
|
162
165
|
await queueEmail('review_status_change', { review, from: prev.status, to: changes.status, recipients: 'team_members' });
|
|
163
|
-
logActivity('review', review.id, 'status_change', `Status: ${prev.status}
|
|
166
|
+
logActivity('review', review.id, 'status_change', `Status: ${prev.status} -> ${changes.status}`, user);
|
|
164
167
|
}
|
|
165
168
|
if (changes.collaborators && changes.collaborators !== prev.collaborators) {
|
|
166
169
|
const added = safeJsonParse(changes.collaborators, []).filter(c => !safeJsonParse(prev.collaborators, []).find(p => p.id === c.id));
|
package/src/lib/export-sink.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* @param {object} spec - Entity specification
|
|
8
|
-
* @param {Function} callback - (key, fieldDef) => void
|
|
9
|
-
* @param {object} options - { includeComputed, includeHidden }
|
|
10
|
-
*/
|
|
1
|
+
/* ------------------------------------------------------------------ *
|
|
2
|
+
* Predicate-based field query helpers (ported from moonlanding).
|
|
3
|
+
* Additive: these do not change thatcher's existing forEachField /
|
|
4
|
+
* getEditableFields / getFieldNames / getFieldsByType signatures.
|
|
5
|
+
* ------------------------------------------------------------------ */
|
|
6
|
+
|
|
11
7
|
export function forEachField(spec, callback, options = {}) {
|
|
12
8
|
const { includeComputed = false, includeHidden = false } = options;
|
|
13
9
|
|
|
@@ -18,50 +14,21 @@ export function forEachField(spec, callback, options = {}) {
|
|
|
18
14
|
}
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
/**
|
|
22
|
-
* Get all field names
|
|
23
|
-
* @param {object} spec
|
|
24
|
-
* @returns {string[]}
|
|
25
|
-
*/
|
|
26
17
|
export function getFieldNames(spec) {
|
|
27
18
|
return Object.keys(spec.fields || {});
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
/**
|
|
31
|
-
* Get only editable fields
|
|
32
|
-
* @param {object} spec
|
|
33
|
-
* @returns {string[]}
|
|
34
|
-
*/
|
|
35
21
|
export function getEditableFields(spec) {
|
|
36
22
|
return Object.entries(spec.fields || {})
|
|
37
23
|
.filter(([key, field]) => !field.readonly && !field.auto && !field.auto_generate)
|
|
38
24
|
.map(([key]) => key);
|
|
39
25
|
}
|
|
40
26
|
|
|
41
|
-
/**
|
|
42
|
-
* Get field by type
|
|
43
|
-
* @param {object} spec
|
|
44
|
-
* @param {string} type
|
|
45
|
-
* @returns {Array<{key: string, field: object}>}
|
|
46
|
-
*/
|
|
47
27
|
export function getFieldsByType(spec, type) {
|
|
48
28
|
return Object.entries(spec.fields || {})
|
|
49
29
|
.filter(([key, field]) => field.type === type);
|
|
50
30
|
}
|
|
51
31
|
|
|
52
|
-
/* ------------------------------------------------------------------ *
|
|
53
|
-
* Predicate-based field query helpers (ported from moonlanding).
|
|
54
|
-
* Additive: these do not change thatcher's existing forEachField /
|
|
55
|
-
* getEditableFields / getFieldNames / getFieldsByType signatures.
|
|
56
|
-
* ------------------------------------------------------------------ */
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Query the fields of a spec with one or more predicates.
|
|
60
|
-
* @param {object} spec
|
|
61
|
-
* @param {Function|Function[]} predicates - (field, key) => boolean
|
|
62
|
-
* @param {object} [options] - { keysOnly }
|
|
63
|
-
* @returns {Array<string|object>} keys when keysOnly, else { key, ...field }
|
|
64
|
-
*/
|
|
65
32
|
export function fieldQuery(spec, predicates, options = {}) {
|
|
66
33
|
const preds = Array.isArray(predicates) ? predicates : [predicates];
|
|
67
34
|
const results = [];
|
|
@@ -73,9 +40,6 @@ export function fieldQuery(spec, predicates, options = {}) {
|
|
|
73
40
|
return results;
|
|
74
41
|
}
|
|
75
42
|
|
|
76
|
-
/**
|
|
77
|
-
* Reusable field predicates for fieldQuery.
|
|
78
|
-
*/
|
|
79
43
|
export const is = {
|
|
80
44
|
notId: f => f.type !== 'id',
|
|
81
45
|
notHidden: f => !f.hidden,
|
|
@@ -90,85 +54,52 @@ export const is = {
|
|
|
90
54
|
hasProperty: prop => f => f[prop] !== undefined,
|
|
91
55
|
};
|
|
92
56
|
|
|
93
|
-
|
|
94
|
-
* Editable fields as full { key, ...field } objects (form rendering).
|
|
95
|
-
* Distinct from thatcher's getEditableFields (which returns names only).
|
|
96
|
-
*/
|
|
57
|
+
// Returns { key, ...field } objects, unlike getEditableFields() which returns names only.
|
|
97
58
|
export function getFormFields(spec) {
|
|
98
59
|
if (spec.system_entity) return [];
|
|
99
60
|
return fieldQuery(spec, is.editable);
|
|
100
61
|
}
|
|
101
62
|
|
|
102
|
-
/**
|
|
103
|
-
* Listable fields as full { key, ...field } objects.
|
|
104
|
-
*/
|
|
105
63
|
export function getListFields(spec) {
|
|
106
64
|
return fieldQuery(spec, is.listable);
|
|
107
65
|
}
|
|
108
66
|
|
|
109
|
-
/**
|
|
110
|
-
* Displayable fields as full { key, ...field } objects.
|
|
111
|
-
*/
|
|
112
67
|
export function getDisplayFields(spec) {
|
|
113
68
|
return fieldQuery(spec, is.displayable);
|
|
114
69
|
}
|
|
115
70
|
|
|
116
|
-
/**
|
|
117
|
-
* Required field names.
|
|
118
|
-
*/
|
|
119
71
|
export function getRequiredFields(spec) {
|
|
120
72
|
return fieldQuery(spec, is.required, { keysOnly: true });
|
|
121
73
|
}
|
|
122
74
|
|
|
123
|
-
/**
|
|
124
|
-
* Searchable field names.
|
|
125
|
-
*/
|
|
126
75
|
export function getSearchFields(spec) {
|
|
127
76
|
return fieldQuery(spec, is.searchable, { keysOnly: true });
|
|
128
77
|
}
|
|
129
78
|
|
|
130
|
-
/**
|
|
131
|
-
* Filterable fields, resolved from list.filters config.
|
|
132
|
-
*/
|
|
133
79
|
export function getFilterableFields(spec) {
|
|
134
80
|
return (spec.list?.filters || [])
|
|
135
81
|
.map(filterKey => spec.fields?.[filterKey])
|
|
136
82
|
.filter(Boolean);
|
|
137
83
|
}
|
|
138
84
|
|
|
139
|
-
/**
|
|
140
|
-
* Reference (ref) fields as full { key, ...field } objects.
|
|
141
|
-
*/
|
|
142
85
|
export function getRefFields(spec) {
|
|
143
86
|
return fieldQuery(spec, is.ref);
|
|
144
87
|
}
|
|
145
88
|
|
|
146
|
-
/**
|
|
147
|
-
* Get a single field definition by key.
|
|
148
|
-
*/
|
|
149
89
|
export function getField(spec, fieldKey) {
|
|
150
90
|
return spec.fields?.[fieldKey];
|
|
151
91
|
}
|
|
152
92
|
|
|
153
|
-
/**
|
|
154
|
-
* Get a single field's type by key.
|
|
155
|
-
*/
|
|
156
93
|
export function getFieldType(spec, fieldKey) {
|
|
157
94
|
return spec.fields?.[fieldKey]?.type;
|
|
158
95
|
}
|
|
159
96
|
|
|
160
|
-
/**
|
|
161
|
-
* Iterate editable fields on create. callback = (key, field) => void
|
|
162
|
-
*/
|
|
163
97
|
export function iterateCreateFields(spec, callback) {
|
|
164
98
|
for (const { key, ...field } of fieldQuery(spec, is.editable)) {
|
|
165
99
|
callback(key, field);
|
|
166
100
|
}
|
|
167
101
|
}
|
|
168
102
|
|
|
169
|
-
/**
|
|
170
|
-
* Iterate editable fields on update. callback = (key, field) => void
|
|
171
|
-
*/
|
|
172
103
|
export function iterateUpdateFields(spec, callback) {
|
|
173
104
|
for (const { key, ...field } of fieldQuery(spec, is.editable)) {
|
|
174
105
|
callback(key, field);
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Field Registry - Type coercion and field utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Coerce value to field type
|
|
7
|
-
* @param {any} value
|
|
8
|
-
* @param {string} type
|
|
9
|
-
* @returns {any}
|
|
10
|
-
*/
|
|
11
1
|
export function coerceFieldValue(value, type) {
|
|
12
2
|
if (value === null || value === undefined) return value;
|
|
13
3
|
|
|
@@ -31,22 +21,10 @@ export function coerceFieldValue(value, type) {
|
|
|
31
21
|
}
|
|
32
22
|
}
|
|
33
23
|
|
|
34
|
-
/**
|
|
35
|
-
* Serialize field value for database
|
|
36
|
-
* @param {any} value
|
|
37
|
-
* @param {string} type
|
|
38
|
-
* @returns {any}
|
|
39
|
-
*/
|
|
40
24
|
export function serializeField(value, type) {
|
|
41
25
|
return coerceFieldValue(value, type);
|
|
42
26
|
}
|
|
43
27
|
|
|
44
|
-
/**
|
|
45
|
-
* Deserialize field value from database
|
|
46
|
-
* @param {any} value
|
|
47
|
-
* @param {string} type
|
|
48
|
-
* @returns {any}
|
|
49
|
-
*/
|
|
50
28
|
export function deserializeField(value, type) {
|
|
51
29
|
if (value === null) return null;
|
|
52
30
|
|
|
@@ -1,26 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic CRUD Handler - Catch-all API handler for any entity
|
|
3
|
-
* This provides out-of-the-box CRUD for all entities defined in config
|
|
4
|
-
* Users can override by creating their own route files
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import { createCrudHandlers } from '../lib/crud-factory.js';
|
|
8
2
|
import { getConfigEngineSync } from '../lib/config-generator-engine.js';
|
|
9
3
|
|
|
10
|
-
/**
|
|
11
|
-
* Get handler for an entity
|
|
12
|
-
* @param {string} entityName
|
|
13
|
-
* @returns {object} {GET, POST, PUT, DELETE}
|
|
14
|
-
*/
|
|
15
4
|
export function getEntityHandlers(entityName) {
|
|
16
5
|
return createCrudHandlers(entityName);
|
|
17
6
|
}
|
|
18
7
|
|
|
19
|
-
/**
|
|
20
|
-
* Check if entity exists and is accessible
|
|
21
|
-
* @param {string} entityName
|
|
22
|
-
* @returns {boolean}
|
|
23
|
-
*/
|
|
24
8
|
export function hasEntity(entityName) {
|
|
25
9
|
try {
|
|
26
10
|
const engine = getConfigEngineSync();
|
package/src/lib/hook-engine.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
2
|
+
|
|
3
|
+
const log = createLogger('[HookEngine]');
|
|
5
4
|
|
|
6
5
|
export class HookEngine {
|
|
7
6
|
constructor() {
|
|
8
7
|
this.hooks = new Map();
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
/**
|
|
12
|
-
* Register a hook handler
|
|
13
|
-
* @param {string} name - Event name
|
|
14
|
-
* @param {Function} callback - Handler function
|
|
15
|
-
* @param {object} options - { priority, once }
|
|
16
|
-
*/
|
|
17
10
|
register(name, callback, options = {}) {
|
|
18
11
|
const { priority = 0, once = false } = options;
|
|
19
12
|
if (!this.hooks.has(name)) this.hooks.set(name, []);
|
|
@@ -23,16 +16,10 @@ export class HookEngine {
|
|
|
23
16
|
return this;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
/**
|
|
27
|
-
* Alias for register
|
|
28
|
-
*/
|
|
29
19
|
on(name, callback, options = {}) {
|
|
30
20
|
return this.register(name, callback, options);
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
/**
|
|
34
|
-
* Unregister a hook handler
|
|
35
|
-
*/
|
|
36
23
|
off(name, callback) {
|
|
37
24
|
const list = this.hooks.get(name);
|
|
38
25
|
if (!list) return this;
|
|
@@ -42,13 +29,6 @@ export class HookEngine {
|
|
|
42
29
|
return this;
|
|
43
30
|
}
|
|
44
31
|
|
|
45
|
-
/**
|
|
46
|
-
* Execute all handlers for an event (fire-and-forget style)
|
|
47
|
-
* @param {string} name
|
|
48
|
-
* @param {object} data
|
|
49
|
-
* @param {object} options - { fallthrough: true to continue on error }
|
|
50
|
-
* @returns {Promise<{success: boolean, data, errors}>}
|
|
51
|
-
*/
|
|
52
32
|
async execute(name, data = {}, options = {}) {
|
|
53
33
|
const { fallthrough = true } = options;
|
|
54
34
|
const hooks = this.hooks.get(name);
|
|
@@ -60,7 +40,7 @@ export class HookEngine {
|
|
|
60
40
|
await hook.callback(data);
|
|
61
41
|
if (hook.once) this.off(name, hook.callback);
|
|
62
42
|
} catch (error) {
|
|
63
|
-
|
|
43
|
+
log.error(`hook "${name}" error:`, { message: error.message });
|
|
64
44
|
errors.push(error);
|
|
65
45
|
if (!fallthrough) throw error;
|
|
66
46
|
}
|
|
@@ -69,12 +49,6 @@ export class HookEngine {
|
|
|
69
49
|
return { success: errors.length === 0, data, errors };
|
|
70
50
|
}
|
|
71
51
|
|
|
72
|
-
/**
|
|
73
|
-
* Pipe data through transformation hooks
|
|
74
|
-
* @param {string} name
|
|
75
|
-
* @param {any} data
|
|
76
|
-
* @returns {Promise<any>}
|
|
77
|
-
*/
|
|
78
52
|
async pipe(name, data = {}) {
|
|
79
53
|
const hooks = this.hooks.get(name);
|
|
80
54
|
if (!hooks || hooks.length === 0) return data;
|
|
@@ -86,25 +60,16 @@ export class HookEngine {
|
|
|
86
60
|
if (result !== undefined) current = result;
|
|
87
61
|
if (hook.once) this.off(name, hook.callback);
|
|
88
62
|
} catch (error) {
|
|
89
|
-
|
|
63
|
+
log.error(`pipe hook "${name}" error:`, { message: error.message });
|
|
90
64
|
}
|
|
91
65
|
}
|
|
92
66
|
return current;
|
|
93
67
|
}
|
|
94
68
|
|
|
95
|
-
/**
|
|
96
|
-
* Get all listeners for an event
|
|
97
|
-
* @param {string} name
|
|
98
|
-
* @returns {Array}
|
|
99
|
-
*/
|
|
100
69
|
listeners(name) {
|
|
101
70
|
return this.hooks.has(name) ? Array.from(this.hooks.get(name)) : [];
|
|
102
71
|
}
|
|
103
72
|
|
|
104
|
-
/**
|
|
105
|
-
* Get statistics
|
|
106
|
-
* @returns {object} Map of event -> count
|
|
107
|
-
*/
|
|
108
73
|
stats() {
|
|
109
74
|
const result = {};
|
|
110
75
|
for (const [name, list] of this.hooks.entries()) {
|
|
@@ -113,34 +78,21 @@ export class HookEngine {
|
|
|
113
78
|
return result;
|
|
114
79
|
}
|
|
115
80
|
|
|
116
|
-
/**
|
|
117
|
-
* Clear all hooks
|
|
118
|
-
*/
|
|
119
81
|
clear() {
|
|
120
82
|
this.hooks.clear();
|
|
121
83
|
}
|
|
122
84
|
}
|
|
123
85
|
|
|
124
|
-
// Global singleton
|
|
125
86
|
export const hookEngine = new HookEngine();
|
|
126
87
|
|
|
127
|
-
/**
|
|
128
|
-
* Execute hook (convenience function)
|
|
129
|
-
*/
|
|
130
88
|
export async function executeHook(name, data = {}, options = {}) {
|
|
131
89
|
return hookEngine.execute(name, data, options);
|
|
132
90
|
}
|
|
133
91
|
|
|
134
|
-
/**
|
|
135
|
-
* Pipe through hooks
|
|
136
|
-
*/
|
|
137
92
|
export async function pipeHook(name, data = {}) {
|
|
138
93
|
return hookEngine.pipe(name, data);
|
|
139
94
|
}
|
|
140
95
|
|
|
141
|
-
/**
|
|
142
|
-
* Register hook debug exposure
|
|
143
|
-
*/
|
|
144
96
|
function registerHookDebug() {
|
|
145
97
|
if (globalThis.__debug__) {
|
|
146
98
|
globalThis.__debug__.expose('hooks', {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js';
|
|
1
2
|
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
|
|
5
|
+
const log = createLogger('[Checkpoint]');
|
|
6
|
+
|
|
4
7
|
export class CheckpointManager {
|
|
5
8
|
constructor(options = {}) {
|
|
6
9
|
this.checkpointDir = options.checkpointDir || path.join(process.cwd(), 'data', 'checkpoints');
|
|
@@ -23,7 +26,7 @@ export class CheckpointManager {
|
|
|
23
26
|
await this._pruneOldCheckpoints(key);
|
|
24
27
|
return true;
|
|
25
28
|
} catch (err) {
|
|
26
|
-
|
|
29
|
+
log.error(`failed to save ${key}:`, { message: err?.message || String(err) });
|
|
27
30
|
return false;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -42,7 +45,7 @@ export class CheckpointManager {
|
|
|
42
45
|
this.checkpoints.set(key, checkpoint);
|
|
43
46
|
return checkpoint.state;
|
|
44
47
|
} catch (err) {
|
|
45
|
-
|
|
48
|
+
log.error(`failed to load ${key}:`, { message: err?.message || String(err) });
|
|
46
49
|
return null;
|
|
47
50
|
}
|
|
48
51
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
|
|
3
|
+
import { createLogger } from '../logger.js';
|
|
4
|
+
|
|
5
|
+
const log = createLogger('[PromiseContainer]');
|
|
6
|
+
|
|
3
7
|
export class PromiseContainer extends EventEmitter {
|
|
4
8
|
constructor() {
|
|
5
9
|
super();
|
|
@@ -40,7 +44,7 @@ export class PromiseContainer extends EventEmitter {
|
|
|
40
44
|
try {
|
|
41
45
|
await Promise.race([drainPromise, timeoutPromise]);
|
|
42
46
|
} catch (err) {
|
|
43
|
-
|
|
47
|
+
log.warn(`promise drain timeout: ${this.activePromises.size} promises still active`);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -55,11 +59,11 @@ export class PromiseContainer extends EventEmitter {
|
|
|
55
59
|
export const globalContainer = new PromiseContainer();
|
|
56
60
|
|
|
57
61
|
globalContainer.setGlobalHandler((err, context) => {
|
|
58
|
-
|
|
62
|
+
log.error(`unhandled rejection in ${context}:`, { message: err?.message || String(err) });
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
process.on('unhandledRejection', (reason, promise) => {
|
|
62
|
-
|
|
66
|
+
log.error('unhandled promise rejection:', { message: reason?.message || String(reason) });
|
|
63
67
|
globalContainer.emit('processRejection', { reason, promise });
|
|
64
68
|
});
|
|
65
69
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js';
|
|
1
2
|
import { safeError } from './safe-error.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[RouteWrapper]');
|
|
2
5
|
import { contain } from './promise-container.js';
|
|
3
6
|
|
|
4
7
|
export function wrapRouteHandler(handler, options = {}) {
|
|
@@ -13,7 +16,7 @@ export function wrapRouteHandler(handler, options = {}) {
|
|
|
13
16
|
return result;
|
|
14
17
|
} catch (err) {
|
|
15
18
|
if (logErrors) {
|
|
16
|
-
|
|
19
|
+
log.error(`${context} ${req.method} ${req.url}:`, { message: err?.message || String(err) });
|
|
17
20
|
}
|
|
18
21
|
if (sendErrorResponse && !res.headersSent) {
|
|
19
22
|
const safe = safeError(err);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js';
|
|
1
2
|
import { EventEmitter } from 'events';
|
|
2
3
|
|
|
4
|
+
const log = createLogger('[Supervisor]');
|
|
5
|
+
|
|
3
6
|
export class Supervisor extends EventEmitter {
|
|
4
7
|
constructor(name, workerFn, options = {}) {
|
|
5
8
|
super();
|
|
@@ -22,7 +25,7 @@ export class Supervisor extends EventEmitter {
|
|
|
22
25
|
|
|
23
26
|
if (this.listenerCount('error') === 0) {
|
|
24
27
|
this.on('error', (data) => {
|
|
25
|
-
|
|
28
|
+
log.error(`[${this.name}] worker error:`, { message: data.error?.message || String(data.error) });
|
|
26
29
|
});
|
|
27
30
|
}
|
|
28
31
|
}
|
|
@@ -73,7 +76,7 @@ export class Supervisor extends EventEmitter {
|
|
|
73
76
|
try {
|
|
74
77
|
await this.options.onError(err);
|
|
75
78
|
} catch (handlerErr) {
|
|
76
|
-
|
|
79
|
+
log.error(`[${this.name}] error handler failed:`, { message: handlerErr?.message || String(handlerErr) });
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -84,7 +87,7 @@ export class Supervisor extends EventEmitter {
|
|
|
84
87
|
this.restarts.push(now);
|
|
85
88
|
|
|
86
89
|
if (this.restarts.length > this.options.maxRestarts) {
|
|
87
|
-
|
|
90
|
+
log.error(`[${this.name}] max restarts exceeded, giving up`);
|
|
88
91
|
this.emit('giveup', this.name);
|
|
89
92
|
this.running = false;
|
|
90
93
|
return;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Extracted from database-core.js so modules that need genId()/now() (e.g. the
|
|
2
|
+
// busybase store) don't have to import database-core, which pulls in better-sqlite3.
|
|
3
|
+
|
|
4
|
+
export function genId() {
|
|
5
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function now() {
|
|
9
|
+
return Math.floor(Date.now() / 1000);
|
|
10
|
+
}
|