thatcher 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
export class PromiseContainer extends EventEmitter {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this.activePromises = new Set();
|
|
7
|
+
this.rejectionHandlers = new Map();
|
|
8
|
+
this.globalHandler = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
wrap(promise, context = 'anonymous') {
|
|
12
|
+
const tracked = promise
|
|
13
|
+
.catch(err => {
|
|
14
|
+
this.emit('rejection', { error: err, context });
|
|
15
|
+
if (this.globalHandler) {
|
|
16
|
+
this.globalHandler(err, context);
|
|
17
|
+
}
|
|
18
|
+
return Promise.reject(err);
|
|
19
|
+
})
|
|
20
|
+
.finally(() => {
|
|
21
|
+
this.activePromises.delete(tracked);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.activePromises.add(tracked);
|
|
25
|
+
return tracked;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setGlobalHandler(handler) {
|
|
29
|
+
this.globalHandler = handler;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async drainAll(timeout = 5000) {
|
|
33
|
+
if (this.activePromises.size === 0) return;
|
|
34
|
+
|
|
35
|
+
const drainPromise = Promise.allSettled([...this.activePromises]);
|
|
36
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
37
|
+
setTimeout(() => reject(new Error('Drain timeout')), timeout)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await Promise.race([drainPromise, timeoutPromise]);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.warn(`Promise drain timeout: ${this.activePromises.size} promises still active`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getStats() {
|
|
48
|
+
return {
|
|
49
|
+
active: this.activePromises.size,
|
|
50
|
+
hasGlobalHandler: !!this.globalHandler
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const globalContainer = new PromiseContainer();
|
|
56
|
+
|
|
57
|
+
globalContainer.setGlobalHandler((err, context) => {
|
|
58
|
+
console.error(`[PromiseContainer] Unhandled rejection in ${context}:`, err);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
62
|
+
console.error('[Process] Unhandled Promise Rejection:', reason);
|
|
63
|
+
globalContainer.emit('processRejection', { reason, promise });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const contain = globalContainer.wrap.bind(globalContainer);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { safeError } from './safe-error.js';
|
|
2
|
+
import { contain } from './promise-container.js';
|
|
3
|
+
|
|
4
|
+
export function wrapRouteHandler(handler, options = {}) {
|
|
5
|
+
const { logErrors = true, sendErrorResponse = true, context = 'route' } = options;
|
|
6
|
+
|
|
7
|
+
return async function wrappedHandler(req, res) {
|
|
8
|
+
try {
|
|
9
|
+
const result = await contain(
|
|
10
|
+
Promise.resolve(handler(req, res)),
|
|
11
|
+
`${context}:${req.method}:${req.url}`
|
|
12
|
+
);
|
|
13
|
+
return result;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (logErrors) {
|
|
16
|
+
console.error(`[RouteError:${context}] ${req.method} ${req.url}:`, err);
|
|
17
|
+
}
|
|
18
|
+
if (sendErrorResponse && !res.headersSent) {
|
|
19
|
+
const safe = safeError(err);
|
|
20
|
+
const statusCode = err.statusCode || err.status || 500;
|
|
21
|
+
res.statusCode = statusCode;
|
|
22
|
+
res.setHeader('Content-Type', 'application/json');
|
|
23
|
+
const body = JSON.stringify({
|
|
24
|
+
error: safe.message,
|
|
25
|
+
type: safe.type,
|
|
26
|
+
...(process.env.NODE_ENV === 'development' && { stack: safe.stack })
|
|
27
|
+
});
|
|
28
|
+
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf-8'));
|
|
29
|
+
res.end(body);
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function wrapRouteHandlers(handlers) {
|
|
37
|
+
const wrapped = {};
|
|
38
|
+
for (const [method, handler] of Object.entries(handlers)) {
|
|
39
|
+
if (typeof handler === 'function') {
|
|
40
|
+
wrapped[method] = wrapRouteHandler(handler, { context: method });
|
|
41
|
+
} else {
|
|
42
|
+
wrapped[method] = handler;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return wrapped;
|
|
46
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function safeError(err) {
|
|
2
|
+
if (!err) return { message: 'Unknown error', type: 'Error' };
|
|
3
|
+
if (typeof err === 'string') return { message: err, type: 'String' };
|
|
4
|
+
|
|
5
|
+
const safe = {
|
|
6
|
+
message: err.message || String(err),
|
|
7
|
+
type: err.constructor?.name || 'Error',
|
|
8
|
+
code: err.code,
|
|
9
|
+
statusCode: err.statusCode,
|
|
10
|
+
status: err.status
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (err.stack && typeof err.stack === 'string') {
|
|
14
|
+
safe.stack = err.stack;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const key of Object.keys(err)) {
|
|
18
|
+
if (safe[key] !== undefined) continue;
|
|
19
|
+
const value = err[key];
|
|
20
|
+
const type = typeof value;
|
|
21
|
+
if (value === null || type === 'undefined') continue;
|
|
22
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
23
|
+
safe[key] = value;
|
|
24
|
+
} else if (type === 'object' && !Array.isArray(value)) {
|
|
25
|
+
try { safe[key] = safeError(value); } catch { safe[key] = String(value); }
|
|
26
|
+
} else if (Array.isArray(value)) {
|
|
27
|
+
safe[key] = value.map(v => {
|
|
28
|
+
const vType = typeof v;
|
|
29
|
+
if (vType === 'string' || vType === 'number' || vType === 'boolean') return v;
|
|
30
|
+
return String(v);
|
|
31
|
+
});
|
|
32
|
+
} else {
|
|
33
|
+
safe[key] = String(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return safe;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function safeStringify(obj, space = 0) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(obj, (key, value) => {
|
|
42
|
+
if (value instanceof Error) return safeError(value);
|
|
43
|
+
const type = typeof value;
|
|
44
|
+
if (type === 'symbol' || type === 'function' || type === 'undefined') return String(value);
|
|
45
|
+
if (type === 'bigint') return value.toString();
|
|
46
|
+
return value;
|
|
47
|
+
}, space);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return JSON.stringify({ error: 'Serialization failed', message: String(err) }, null, space);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
export class Supervisor extends EventEmitter {
|
|
4
|
+
constructor(name, workerFn, options = {}) {
|
|
5
|
+
super();
|
|
6
|
+
this.name = name;
|
|
7
|
+
this.workerFn = workerFn;
|
|
8
|
+
this.options = {
|
|
9
|
+
maxRestarts: options.maxRestarts || 5,
|
|
10
|
+
restartWindow: options.restartWindow || 60000,
|
|
11
|
+
backoffMs: options.backoffMs || 1000,
|
|
12
|
+
maxBackoffMs: options.maxBackoffMs || 30000,
|
|
13
|
+
onError: options.onError || null,
|
|
14
|
+
...options
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
this.worker = null;
|
|
18
|
+
this.restarts = [];
|
|
19
|
+
this.currentBackoff = this.options.backoffMs;
|
|
20
|
+
this.running = false;
|
|
21
|
+
this.stopping = false;
|
|
22
|
+
|
|
23
|
+
if (this.listenerCount('error') === 0) {
|
|
24
|
+
this.on('error', (data) => {
|
|
25
|
+
console.error(`[Supervisor:${this.name}] Worker error:`, data.error?.message || data.error);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async start() {
|
|
31
|
+
if (this.running) return;
|
|
32
|
+
this.running = true;
|
|
33
|
+
this.stopping = false;
|
|
34
|
+
await this._startWorker();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async stop() {
|
|
38
|
+
this.stopping = true;
|
|
39
|
+
this.running = false;
|
|
40
|
+
if (this.worker && typeof this.worker.stop === 'function') {
|
|
41
|
+
await this.worker.stop();
|
|
42
|
+
}
|
|
43
|
+
this.worker = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async restart() {
|
|
47
|
+
await this.stop();
|
|
48
|
+
this.restarts = [];
|
|
49
|
+
this.currentBackoff = this.options.backoffMs;
|
|
50
|
+
await this.start();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async _startWorker() {
|
|
54
|
+
if (!this.running || this.stopping) return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
this.worker = await this.workerFn();
|
|
58
|
+
this.emit('started', this.name);
|
|
59
|
+
this.currentBackoff = this.options.backoffMs;
|
|
60
|
+
|
|
61
|
+
if (this.worker && typeof this.worker.on === 'function') {
|
|
62
|
+
this.worker.on('error', (err) => this._handleError(err));
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
await this._handleError(err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _handleError(err) {
|
|
70
|
+
this.emit('error', { name: this.name, error: err });
|
|
71
|
+
|
|
72
|
+
if (this.options.onError) {
|
|
73
|
+
try {
|
|
74
|
+
await this.options.onError(err);
|
|
75
|
+
} catch (handlerErr) {
|
|
76
|
+
console.error(`[Supervisor:${this.name}] Error handler failed:`, handlerErr);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this.running || this.stopping) return;
|
|
81
|
+
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
this.restarts = this.restarts.filter(t => now - t < this.options.restartWindow);
|
|
84
|
+
this.restarts.push(now);
|
|
85
|
+
|
|
86
|
+
if (this.restarts.length > this.options.maxRestarts) {
|
|
87
|
+
console.error(`[Supervisor:${this.name}] Max restarts exceeded. Giving up.`);
|
|
88
|
+
this.emit('giveup', this.name);
|
|
89
|
+
this.running = false;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, this.currentBackoff));
|
|
94
|
+
this.currentBackoff = Math.min(this.currentBackoff * 2, this.options.maxBackoffMs);
|
|
95
|
+
|
|
96
|
+
await this._startWorker();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getStats() {
|
|
100
|
+
return {
|
|
101
|
+
name: this.name,
|
|
102
|
+
running: this.running,
|
|
103
|
+
stopping: this.stopping,
|
|
104
|
+
restarts: this.restarts.length,
|
|
105
|
+
maxRestarts: this.options.maxRestarts,
|
|
106
|
+
currentBackoff: this.currentBackoff,
|
|
107
|
+
hasWorker: !!this.worker
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class SupervisorTree extends EventEmitter {
|
|
113
|
+
constructor() {
|
|
114
|
+
super();
|
|
115
|
+
this.supervisors = new Map();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
register(name, workerFn, options) {
|
|
119
|
+
const supervisor = new Supervisor(name, workerFn, options);
|
|
120
|
+
supervisor.on('error', (data) => this.emit('childError', data));
|
|
121
|
+
supervisor.on('giveup', (name) => this.emit('childGiveup', name));
|
|
122
|
+
this.supervisors.set(name, supervisor);
|
|
123
|
+
return supervisor;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async start(name) {
|
|
127
|
+
const supervisor = this.supervisors.get(name);
|
|
128
|
+
if (!supervisor) throw new Error(`Supervisor ${name} not found`);
|
|
129
|
+
await supervisor.start();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async stop(name) {
|
|
133
|
+
const supervisor = this.supervisors.get(name);
|
|
134
|
+
if (!supervisor) throw new Error(`Supervisor ${name} not found`);
|
|
135
|
+
await supervisor.stop();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async startAll() {
|
|
139
|
+
await Promise.all([...this.supervisors.values()].map(s => s.start()));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async stopAll() {
|
|
143
|
+
await Promise.all([...this.supervisors.values()].map(s => s.stop()));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async restart(name) {
|
|
147
|
+
const supervisor = this.supervisors.get(name);
|
|
148
|
+
if (!supervisor) throw new Error(`Supervisor ${name} not found`);
|
|
149
|
+
await supervisor.restart();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getStats() {
|
|
153
|
+
const stats = {};
|
|
154
|
+
for (const [name, supervisor] of this.supervisors.entries()) {
|
|
155
|
+
stats[name] = supervisor.getStats();
|
|
156
|
+
}
|
|
157
|
+
return stats;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const globalTree = new SupervisorTree();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class TimeoutError extends Error {
|
|
2
|
+
constructor(message, operation) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'TimeoutError';
|
|
5
|
+
this.operation = operation;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function withTimeout(promise, ms, operation = 'operation') {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const timer = setTimeout(() => {
|
|
12
|
+
reject(new TimeoutError(`${operation} timed out after ${ms}ms`, operation));
|
|
13
|
+
}, ms);
|
|
14
|
+
promise
|
|
15
|
+
.then(value => { clearTimeout(timer); resolve(value); })
|
|
16
|
+
.catch(err => { clearTimeout(timer); reject(err); });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function withAbortableTimeout(fn, ms, operation = 'operation') {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const { signal } = controller;
|
|
23
|
+
const timeoutId = setTimeout(() => { controller.abort(); }, ms);
|
|
24
|
+
|
|
25
|
+
return fn(signal)
|
|
26
|
+
.then(result => { clearTimeout(timeoutId); return result; })
|
|
27
|
+
.catch(err => {
|
|
28
|
+
clearTimeout(timeoutId);
|
|
29
|
+
if (err.name === 'AbortError') {
|
|
30
|
+
throw new TimeoutError(`${operation} aborted after ${ms}ms`, operation);
|
|
31
|
+
}
|
|
32
|
+
throw err;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function retry(fn, options = {}) {
|
|
37
|
+
const { maxRetries = 3, delayMs = 1000, backoff = true, onRetry = null } = options;
|
|
38
|
+
let lastError;
|
|
39
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
lastError = err;
|
|
44
|
+
if (attempt < maxRetries) {
|
|
45
|
+
const delay = backoff ? delayMs * Math.pow(2, attempt) : delayMs;
|
|
46
|
+
if (onRetry) onRetry(err, attempt + 1, delay);
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw lastError;
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createUniversalHandler } from '@/lib/universal-handler';
|
|
2
|
+
import { setCurrentRequest } from '@/engine.server';
|
|
3
|
+
|
|
4
|
+
function createMethodHandler(entityNameOrGetter) {
|
|
5
|
+
return async function(request, context) {
|
|
6
|
+
setCurrentRequest(request);
|
|
7
|
+
const entityName = typeof entityNameOrGetter === 'function'
|
|
8
|
+
? await entityNameOrGetter(context)
|
|
9
|
+
: entityNameOrGetter;
|
|
10
|
+
|
|
11
|
+
const handler = createUniversalHandler(entityName);
|
|
12
|
+
return await handler(request, context);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createHttpMethods(entityNameOrGetter) {
|
|
17
|
+
const handler = createMethodHandler(entityNameOrGetter);
|
|
18
|
+
return {
|
|
19
|
+
GET: handler,
|
|
20
|
+
POST: handler,
|
|
21
|
+
PUT: handler,
|
|
22
|
+
PATCH: handler,
|
|
23
|
+
DELETE: handler,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getDatabase } from '@/lib/database-core';
|
|
2
|
+
|
|
3
|
+
const COMPOSITE_INDEXES = [
|
|
4
|
+
{ name: 'idx_engagement_client_stage', table: 'engagement', columns: ['client_id', 'stage'], where: "status != 'deleted'" },
|
|
5
|
+
{ name: 'idx_engagement_status_created', table: 'engagement', columns: ['status', 'created_at'] },
|
|
6
|
+
{ name: 'idx_engagement_stage_created', table: 'engagement', columns: ['stage', 'created_at'] },
|
|
7
|
+
{ name: 'idx_rfi_engagement_client_status', table: 'rfi', columns: ['engagement_id', 'client_status'] },
|
|
8
|
+
{ name: 'idx_rfi_engagement_auditor_status', table: 'rfi', columns: ['engagement_id', 'auditor_status'] },
|
|
9
|
+
{ name: 'idx_rfi_engagement_status', table: 'rfi', columns: ['engagement_id', 'status'] },
|
|
10
|
+
{ name: 'idx_users_client_role', table: 'users', columns: ['client_id', 'role', 'status'] },
|
|
11
|
+
{ name: 'idx_users_email_active', table: 'users', columns: ['email'], where: "status = 'active'" },
|
|
12
|
+
{ name: 'idx_users_type_status', table: 'users', columns: ['type', 'status'] },
|
|
13
|
+
{ name: 'idx_sessions_expires', table: 'sessions', columns: ['expires_at'] },
|
|
14
|
+
{ name: 'idx_sessions_user_expires', table: 'sessions', columns: ['user_id', 'expires_at'] },
|
|
15
|
+
{ name: 'idx_audit_composite', table: 'audit_logs', columns: ['entity_type', 'entity_id', 'created_at'] },
|
|
16
|
+
{ name: 'idx_audit_user_created', table: 'audit_logs', columns: ['user_id', 'created_at'] },
|
|
17
|
+
{ name: 'idx_email_status_created', table: 'email', columns: ['status', 'created_at'] },
|
|
18
|
+
{ name: 'idx_email_recipient', table: 'email', columns: ['recipient_email', 'created_at'] },
|
|
19
|
+
{ name: 'idx_rfi_questions_rfi_status', table: 'rfi_questions', columns: ['rfi_id', 'status'] },
|
|
20
|
+
{ name: 'idx_rfi_questions_assigned', table: 'rfi_questions', columns: ['assigned_to', 'status'] },
|
|
21
|
+
{ name: 'idx_chat_messages_rfi_created', table: 'chat_messages', columns: ['rfi_id', 'created_at'] },
|
|
22
|
+
{ name: 'idx_chat_mentions_user_resolved', table: 'chat_mentions', columns: ['user_id', 'resolved'] },
|
|
23
|
+
{ name: 'idx_password_reset_expires', table: 'password_reset_tokens', columns: ['expires_at', 'used'] }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const indexExists = (db, indexName) => {
|
|
27
|
+
try {
|
|
28
|
+
const result = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name=?").get(indexName);
|
|
29
|
+
return !!result;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const createOptimizedIndexes = () => {
|
|
36
|
+
const db = getDatabase();
|
|
37
|
+
const created = [];
|
|
38
|
+
const skipped = [];
|
|
39
|
+
const errors = [];
|
|
40
|
+
|
|
41
|
+
for (const idx of COMPOSITE_INDEXES) {
|
|
42
|
+
if (indexExists(db, idx.name)) {
|
|
43
|
+
skipped.push(idx.name);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(idx.table);
|
|
49
|
+
if (!tableExists) {
|
|
50
|
+
skipped.push(`${idx.name} (table ${idx.table} not found)`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const columns = idx.columns.map(c => `"${c}"`).join(', ');
|
|
55
|
+
const wherePart = idx.where ? ` WHERE ${idx.where}` : '';
|
|
56
|
+
const sql = `CREATE INDEX IF NOT EXISTS "${idx.name}" ON "${idx.table}"(${columns})${wherePart}`;
|
|
57
|
+
|
|
58
|
+
db.exec(sql);
|
|
59
|
+
created.push(idx.name);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
errors.push({ index: idx.name, error: e.message });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { created, skipped, errors, total: COMPOSITE_INDEXES.length };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const analyzeQuery = (sql) => {
|
|
69
|
+
const db = getDatabase();
|
|
70
|
+
try {
|
|
71
|
+
const plan = db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all();
|
|
72
|
+
return plan;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return [{ error: e.message }];
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const getIndexUsage = () => {
|
|
79
|
+
const db = getDatabase();
|
|
80
|
+
try {
|
|
81
|
+
const indexes = db.prepare("SELECT name, tbl_name FROM sqlite_master WHERE type='index' AND sql IS NOT NULL ORDER BY tbl_name, name").all();
|
|
82
|
+
return indexes;
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const optimizeDatabase = () => {
|
|
89
|
+
const db = getDatabase();
|
|
90
|
+
try {
|
|
91
|
+
db.exec('ANALYZE');
|
|
92
|
+
return { status: 'success', message: 'Database statistics updated' };
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { status: 'error', error: e.message };
|
|
95
|
+
}
|
|
96
|
+
};
|
package/src/lib/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export * from '@/lib/query-engine';
|
|
2
|
+
export * from '@/lib/validate';
|
|
3
|
+
export * from '@/lib/field-types';
|
|
4
|
+
export * from '@/lib/field-iterator';
|
|
5
|
+
export * from '@/lib/list-data-transform';
|
|
6
|
+
export * from '@/lib/api-helpers';
|
|
7
|
+
export * from '@/lib/logger';
|
|
8
|
+
export { can, check, canAccessRow } from '@/services/permission.service';
|
|
9
|
+
export * from '@/lib/status-helpers';
|
|
10
|
+
export * from '@/lib/route-helpers';
|
|
11
|
+
export * from '@/lib/utils';
|
|
12
|
+
export {
|
|
13
|
+
AppError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
PermissionError,
|
|
17
|
+
UnauthorizedError,
|
|
18
|
+
ConflictError,
|
|
19
|
+
DatabaseError,
|
|
20
|
+
ExternalAPIError,
|
|
21
|
+
errorHandler,
|
|
22
|
+
apiErrorHandler,
|
|
23
|
+
normalizeError,
|
|
24
|
+
formatErrorResponse,
|
|
25
|
+
createErrorLogger,
|
|
26
|
+
} from '@/lib/error-handler';
|
|
27
|
+
export { createApiHandler } from '@/lib/api';
|
|
28
|
+
export * from '@/lib/errors';
|
|
29
|
+
export { getDatabase, migrate, genId, now } from '@/lib/database-core';
|
|
30
|
+
export { logAction, getAuditHistory, getEntityAuditTrail, getActionStats, getUserStats } from '@/lib/audit-logger';
|
|
31
|
+
export * from '@/lib/realtime-server';
|
|
32
|
+
export * from '@/lib/hook-engine';
|
|
33
|
+
export * from '@/lib/events-engine';
|
|
34
|
+
export * from '@/lib/workflow-engine';
|
|
35
|
+
export * from '@/lib/field-registry';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function filterByQuery(data, query, searchFields = ['name', 'title', 'description']) {
|
|
2
|
+
if (!query) return data;
|
|
3
|
+
const lower = query.toLowerCase();
|
|
4
|
+
return data.filter(row =>
|
|
5
|
+
searchFields.some(field =>
|
|
6
|
+
String(row[field] || '').toLowerCase().includes(lower)
|
|
7
|
+
)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function groupByField(data, field) {
|
|
12
|
+
if (!field) return { '': data };
|
|
13
|
+
return data.reduce((acc, row) => {
|
|
14
|
+
const g = row[field] || 'Other';
|
|
15
|
+
(acc[g] = acc[g] || []).push(row);
|
|
16
|
+
return acc;
|
|
17
|
+
}, {});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compareValues(a, b, dir = 'asc') {
|
|
21
|
+
if (a == null && b == null) return 0;
|
|
22
|
+
if (a == null) return dir === 'asc' ? 1 : -1;
|
|
23
|
+
if (b == null) return dir === 'asc' ? -1 : 1;
|
|
24
|
+
const cmp = a < b ? -1 : a > b ? 1 : 0;
|
|
25
|
+
return dir === 'asc' ? cmp : -cmp;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sortByField(rows, field, dir) {
|
|
29
|
+
if (!field) return rows;
|
|
30
|
+
return [...rows].sort((a, b) => compareValues(a[field], b[field], dir));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sortGroups(grouped, field, dir) {
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const [group, rows] of Object.entries(grouped)) {
|
|
36
|
+
result[group] = sortByField(rows, field, dir);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const logs = []
|
|
2
|
+
const maxLogs = 10000
|
|
3
|
+
|
|
4
|
+
const levels = {
|
|
5
|
+
debug: 0,
|
|
6
|
+
info: 1,
|
|
7
|
+
warn: 2,
|
|
8
|
+
error: 3,
|
|
9
|
+
critical: 4
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let currentLevel = levels.info
|
|
13
|
+
|
|
14
|
+
function log(level, message, metadata = {}) {
|
|
15
|
+
if (levels[level] < currentLevel) return
|
|
16
|
+
|
|
17
|
+
const entry = {
|
|
18
|
+
level,
|
|
19
|
+
message,
|
|
20
|
+
metadata,
|
|
21
|
+
timestamp: Date.now(),
|
|
22
|
+
iso: new Date().toISOString()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
logs.push(entry)
|
|
26
|
+
if (logs.length > maxLogs) logs.shift()
|
|
27
|
+
|
|
28
|
+
const prefix = `[${entry.iso}] [${level.toUpperCase()}]`
|
|
29
|
+
const metaStr = Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : ''
|
|
30
|
+
|
|
31
|
+
if (level === 'error' || level === 'critical') {
|
|
32
|
+
console.error(prefix, message, metaStr)
|
|
33
|
+
} else if (level === 'warn') {
|
|
34
|
+
console.warn(prefix, message, metaStr)
|
|
35
|
+
} else {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function debug(message, metadata) {
|
|
40
|
+
log('debug', message, metadata)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function info(message, metadata) {
|
|
44
|
+
log('info', message, metadata)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function warn(message, metadata) {
|
|
48
|
+
log('warn', message, metadata)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function error(message, metadata) {
|
|
52
|
+
log('error', message, metadata)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function critical(message, metadata) {
|
|
56
|
+
log('critical', message, metadata)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getLogs(filter = {}) {
|
|
60
|
+
let filtered = logs
|
|
61
|
+
|
|
62
|
+
if (filter.level) {
|
|
63
|
+
const minLevel = levels[filter.level]
|
|
64
|
+
filtered = filtered.filter(l => levels[l.level] >= minLevel)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (filter.since) {
|
|
68
|
+
filtered = filtered.filter(l => l.timestamp >= filter.since)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (filter.search) {
|
|
72
|
+
const search = filter.search.toLowerCase()
|
|
73
|
+
filtered = filtered.filter(l =>
|
|
74
|
+
l.message.toLowerCase().includes(search) ||
|
|
75
|
+
JSON.stringify(l.metadata).toLowerCase().includes(search)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const limit = filter.limit || 100
|
|
80
|
+
return filtered.slice(-limit)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clearLogs() {
|
|
84
|
+
logs.length = 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function setLogLevel(level) {
|
|
88
|
+
if (levels[level] !== undefined) {
|
|
89
|
+
currentLevel = levels[level]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
log,
|
|
95
|
+
debug,
|
|
96
|
+
info,
|
|
97
|
+
warn,
|
|
98
|
+
error,
|
|
99
|
+
critical,
|
|
100
|
+
getLogs,
|
|
101
|
+
clearLogs,
|
|
102
|
+
setLogLevel
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof globalThis !== 'undefined') {
|
|
106
|
+
globalThis.__logs = {
|
|
107
|
+
debug,
|
|
108
|
+
info,
|
|
109
|
+
warn,
|
|
110
|
+
error,
|
|
111
|
+
critical,
|
|
112
|
+
getLogs,
|
|
113
|
+
clearLogs,
|
|
114
|
+
setLogLevel
|
|
115
|
+
}
|
|
116
|
+
}
|