webspresso 0.0.106 → 0.0.108
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/.agents/AGENTS.md +4 -1
- package/.agents/CACHE.md +1 -0
- package/.agents/CONTEXT.md +152 -0
- package/.agents/ORM.md +74 -0
- package/.agents/SHUTDOWN.md +8 -1
- package/.agents/skills/webspresso-framework/SKILL.md +2 -1
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
- package/.agents/skills/webspresso-services/SKILL.md +36 -3
- package/AGENTS.md +6 -3
- package/bin/webspresso.js +17 -7
- package/core/app.js +45 -11
- package/core/applySchema.js +3 -72
- package/core/auth/index.js +4 -1
- package/core/auth/jwt.js +85 -13
- package/core/auth/magic-link.js +23 -5
- package/core/auth/manager.js +7 -1
- package/core/auth/redis-session.js +62 -25
- package/core/auth/session-manager.js +28 -2
- package/core/auth/sessions.js +182 -52
- package/core/cache/lru.js +10 -0
- package/core/cache/tagged-lru.js +4 -0
- package/core/compileSchema.js +3 -68
- package/core/compression/compressible.js +53 -6
- package/core/compression/negotiate.js +31 -12
- package/core/console/eval.js +4 -1
- package/core/content/service.js +1 -0
- package/core/context/app-context.js +201 -0
- package/core/context/index.js +55 -0
- package/core/context/request.js +241 -0
- package/core/errors/domain.js +20 -2
- package/core/errors/http.js +1 -0
- package/core/events/dispatcher.js +144 -15
- package/core/events/signal.js +9 -0
- package/core/health/probes.js +12 -2
- package/core/http/hono-adapter.js +1 -1
- package/core/http/index.js +10 -1
- package/core/http/pipeline.js +519 -334
- package/core/index.js +36 -2
- package/core/mail/mailer.js +1 -1
- package/core/orm/eager-loader.js +170 -55
- package/core/orm/index.js +25 -47
- package/core/orm/json-fields.js +44 -13
- package/core/orm/model.js +18 -1
- package/core/orm/query-builder.js +33 -15
- package/core/orm/repository.js +59 -12
- package/core/orm/schema-helpers.js +59 -0
- package/core/orm/scopes.js +54 -4
- package/core/orm/transaction.js +109 -14
- package/core/plugins/index.js +8 -0
- package/core/plugins/plugin-manager.js +1098 -0
- package/core/queue/manager.js +55 -27
- package/core/rate-limit/stores/memory.js +23 -10
- package/core/resilience/circuit-breaker.js +34 -14
- package/core/schema-cache.js +2 -64
- package/core/services/builtins/auth.js +273 -0
- package/core/services/builtins/exchange.js +229 -0
- package/core/services/builtins/file-manager.js +178 -0
- package/core/services/builtins/mail.js +134 -0
- package/core/services/builtins/media.js +196 -0
- package/core/services/builtins/system.js +219 -0
- package/core/services/discovery.js +133 -0
- package/core/services/executor.js +629 -0
- package/core/services/index.js +114 -0
- package/core/services/loader.js +161 -0
- package/core/services/memoize.js +408 -0
- package/core/services/registry.js +731 -0
- package/core/services/validator.js +424 -0
- package/core/shutdown/shutdown-manager.js +194 -114
- package/core/ssr/stream.js +109 -16
- package/core/stream/text-stream.js +45 -4
- package/core/studio/inspector.js +9 -1
- package/core/studio/server.js +24 -8
- package/core/utils/index.js +110 -0
- package/core/utils/string.js +77 -16
- package/core/utils/timeout.js +4 -0
- package/core/validation/apply.js +74 -0
- package/core/validation/cache.js +66 -0
- package/core/validation/compiler.js +77 -0
- package/core/validation/index.js +40 -1
- package/index.d.ts +249 -12
- package/index.js +68 -20
- package/package.json +18 -8
- package/plugins/admin-panel/client/parts/07-model-list.js +5 -2
- package/plugins/admin-panel/index.js +24 -11
- package/plugins/admin-panel/modules/user-management.js +3 -2
- package/plugins/csrf/index.js +2 -1
- package/plugins/dashboard/index.js +27 -7
- package/src/app-context.js +4 -188
- package/src/dev/typegen.js +1 -1
- package/src/file-router.js +12 -3
- package/src/plugin-manager.js +5 -861
- package/src/server.js +30 -34
- package/src/services/builtins/auth.js +2 -273
- package/src/services/builtins/exchange.js +2 -229
- package/src/services/builtins/file-manager.js +2 -178
- package/src/services/builtins/mail.js +2 -134
- package/src/services/builtins/media.js +2 -196
- package/src/services/builtins/system.js +2 -219
- package/src/services/discovery.js +2 -125
- package/src/services/executor.js +2 -362
- package/src/services/index.js +4 -60
- package/src/services/loader.js +2 -0
- package/src/services/memoize.js +2 -234
- package/src/services/registry.js +2 -314
- package/src/services/validator.js +2 -273
- package/core/utils/array.js +0 -12
- package/core/utils/base64url.js +0 -8
- package/core/utils/ensure-leading-slash.js +0 -6
- package/core/utils/escape-html.js +0 -7
- package/core/utils/escape-regexp.js +0 -7
- package/core/utils/get-client-ip.js +0 -6
- package/core/utils/hmac.js +0 -6
- package/core/utils/random.js +0 -9
- package/core/utils/safe-compare.js +0 -3
- package/core/utils/sha256.js +0 -6
- package/core/utils/strip-trailing-slash.js +0 -6
- package/core/utils/walk.js +0 -7
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide app context set by createApp() — use from API handlers, jobs, etc.
|
|
3
|
+
* @module src/app-context
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { circuitBreakerRegistry, retry } = require('../resilience');
|
|
7
|
+
|
|
8
|
+
/** @type {{ db: object|null, shutdownManager: object|null, serviceRegistry: object|null, events: object|null, mailer: object|null, cache: object|null, breakers: object|null }} */
|
|
9
|
+
let context = {
|
|
10
|
+
db: null,
|
|
11
|
+
shutdownManager: null,
|
|
12
|
+
serviceRegistry: null,
|
|
13
|
+
events: null,
|
|
14
|
+
mailer: null,
|
|
15
|
+
cache: null,
|
|
16
|
+
breakers: circuitBreakerRegistry,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Merge into the current context (typically called once from createApp).
|
|
21
|
+
* @param {{ db?: object|null, shutdownManager?: object|null, serviceRegistry?: object|null, events?: object|null, mailer?: object|null, cache?: object|null }} partial
|
|
22
|
+
*/
|
|
23
|
+
function setAppContext(partial) {
|
|
24
|
+
context = { ...context, ...partial };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @returns {{ db: object|null, shutdownManager: object|null, serviceRegistry: object|null, events: object|null, mailer: object|null }}
|
|
29
|
+
*/
|
|
30
|
+
function getAppContext() {
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Database instance passed to createApp({ db }) (Knex + ORM helpers).
|
|
36
|
+
* @returns {object}
|
|
37
|
+
* @throws {Error} If createApp was not given a db instance
|
|
38
|
+
*/
|
|
39
|
+
function getDb() {
|
|
40
|
+
if (!context.db) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'No database registered. Create a DB with createDatabase(), then pass it to createApp({ db }). ' +
|
|
43
|
+
'Or use hasDb() before calling getDb().'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return context.db;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function hasDb() {
|
|
53
|
+
return context.db != null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {object|null}
|
|
58
|
+
*/
|
|
59
|
+
function getShutdownManager() {
|
|
60
|
+
return context.shutdownManager;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
function hasShutdownManager() {
|
|
67
|
+
return context.shutdownManager != null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the registered ServiceRegistry instance
|
|
72
|
+
* @returns {object|null}
|
|
73
|
+
*/
|
|
74
|
+
function getServiceRegistry() {
|
|
75
|
+
return context.serviceRegistry;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a ServiceRegistry is registered
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function hasServiceRegistry() {
|
|
83
|
+
return context.serviceRegistry != null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the registered EventDispatcher instance
|
|
88
|
+
* @returns {object|null}
|
|
89
|
+
*/
|
|
90
|
+
function getEvents() {
|
|
91
|
+
return context.events;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if an EventDispatcher is registered
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
function hasEvents() {
|
|
99
|
+
return context.events != null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clear context (e.g. between tests).
|
|
104
|
+
*/
|
|
105
|
+
function resetAppContext() {
|
|
106
|
+
context = {
|
|
107
|
+
db: null,
|
|
108
|
+
shutdownManager: null,
|
|
109
|
+
serviceRegistry: null,
|
|
110
|
+
events: null,
|
|
111
|
+
mailer: null,
|
|
112
|
+
cache: null,
|
|
113
|
+
breakers: circuitBreakerRegistry,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @returns {object|null}
|
|
119
|
+
*/
|
|
120
|
+
function getMailer() {
|
|
121
|
+
return context.mailer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
function hasMailer() {
|
|
128
|
+
return context.mailer != null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @returns {object|null}
|
|
133
|
+
*/
|
|
134
|
+
function getCache() {
|
|
135
|
+
return context.cache;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
function hasCache() {
|
|
142
|
+
return context.cache != null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Express middleware: sets `req.db`, `req.service`, `req.events`, `req.mailer`, and `req.cache` from registered app context.
|
|
147
|
+
* File-based `pages/api/*` routes attach this automatically; use in `setupRoutes`
|
|
148
|
+
* for manually registered handlers that need `req.db` and `req.service`.
|
|
149
|
+
* @type {import('express').RequestHandler}
|
|
150
|
+
*/
|
|
151
|
+
function attachDbMiddleware(req, res, next) {
|
|
152
|
+
if (context.db != null) {
|
|
153
|
+
req.db = context.db;
|
|
154
|
+
}
|
|
155
|
+
if (context.events != null) {
|
|
156
|
+
req.events = context.events;
|
|
157
|
+
}
|
|
158
|
+
if (context.mailer != null) {
|
|
159
|
+
req.mailer = context.mailer;
|
|
160
|
+
}
|
|
161
|
+
if (context.cache != null) {
|
|
162
|
+
req.cache = context.cache;
|
|
163
|
+
}
|
|
164
|
+
req.breakers = context.breakers || circuitBreakerRegistry;
|
|
165
|
+
req.breaker = (name, opts) => (context.breakers || circuitBreakerRegistry).getBreaker(name, opts);
|
|
166
|
+
req.retry = retry;
|
|
167
|
+
if (context.serviceRegistry != null) {
|
|
168
|
+
req.service = (name, input, opts) =>
|
|
169
|
+
context.serviceRegistry.call(name, input, req.context || {
|
|
170
|
+
req,
|
|
171
|
+
res,
|
|
172
|
+
db: context.db,
|
|
173
|
+
events: context.events,
|
|
174
|
+
mailer: context.mailer,
|
|
175
|
+
cache: context.cache,
|
|
176
|
+
breakers: req.breakers,
|
|
177
|
+
breaker: req.breaker,
|
|
178
|
+
retry,
|
|
179
|
+
}, opts);
|
|
180
|
+
}
|
|
181
|
+
next();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
setAppContext,
|
|
186
|
+
getAppContext,
|
|
187
|
+
getDb,
|
|
188
|
+
hasDb,
|
|
189
|
+
getShutdownManager,
|
|
190
|
+
hasShutdownManager,
|
|
191
|
+
getServiceRegistry,
|
|
192
|
+
hasServiceRegistry,
|
|
193
|
+
getEvents,
|
|
194
|
+
hasEvents,
|
|
195
|
+
getMailer,
|
|
196
|
+
hasMailer,
|
|
197
|
+
getCache,
|
|
198
|
+
hasCache,
|
|
199
|
+
resetAppContext,
|
|
200
|
+
attachDbMiddleware,
|
|
201
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Context Management Layer
|
|
5
|
+
* Process-wide application context and request-scoped AsyncLocalStorage ambient context.
|
|
6
|
+
*
|
|
7
|
+
* @module core/context
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const appContext = require('./app-context');
|
|
11
|
+
const requestContext = require('./request');
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
// App context (process-wide)
|
|
15
|
+
setAppContext: appContext.setAppContext,
|
|
16
|
+
getAppContext: appContext.getAppContext,
|
|
17
|
+
getDb: appContext.getDb,
|
|
18
|
+
hasDb: appContext.hasDb,
|
|
19
|
+
getShutdownManager: appContext.getShutdownManager,
|
|
20
|
+
hasShutdownManager: appContext.hasShutdownManager,
|
|
21
|
+
getServiceRegistry: appContext.getServiceRegistry,
|
|
22
|
+
hasServiceRegistry: appContext.hasServiceRegistry,
|
|
23
|
+
getEvents: appContext.getEvents,
|
|
24
|
+
hasEvents: appContext.hasEvents,
|
|
25
|
+
getMailer: appContext.getMailer,
|
|
26
|
+
hasMailer: appContext.hasMailer,
|
|
27
|
+
getCache: appContext.getCache,
|
|
28
|
+
hasCache: appContext.hasCache,
|
|
29
|
+
resetAppContext: appContext.resetAppContext,
|
|
30
|
+
attachDbMiddleware: appContext.attachDbMiddleware,
|
|
31
|
+
|
|
32
|
+
// Request & ambient execution context (AsyncLocalStorage)
|
|
33
|
+
requestContextStorage: requestContext.requestContextStorage,
|
|
34
|
+
runWithContext: requestContext.runWithContext,
|
|
35
|
+
withContext: requestContext.withContext,
|
|
36
|
+
getContext: requestContext.getContext,
|
|
37
|
+
useContext: requestContext.useContext,
|
|
38
|
+
useDb: requestContext.useDb,
|
|
39
|
+
useAuth: requestContext.useAuth,
|
|
40
|
+
useUser: requestContext.useUser,
|
|
41
|
+
useLogger: requestContext.useLogger,
|
|
42
|
+
useReq: requestContext.useReq,
|
|
43
|
+
useRes: requestContext.useRes,
|
|
44
|
+
useParams: requestContext.useParams,
|
|
45
|
+
useQuery: requestContext.useQuery,
|
|
46
|
+
useService: requestContext.useService,
|
|
47
|
+
useQueue: requestContext.useQueue,
|
|
48
|
+
useCache: requestContext.useCache,
|
|
49
|
+
useBreaker: requestContext.useBreaker,
|
|
50
|
+
useTransaction: requestContext.useTransaction,
|
|
51
|
+
|
|
52
|
+
// Namespace accessors
|
|
53
|
+
appContext,
|
|
54
|
+
requestContext,
|
|
55
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Ambient Request & Execution Context
|
|
5
|
+
* Zero-dependency execution context powered by Node.js AsyncLocalStorage.
|
|
6
|
+
* Provides composable hooks (useContext, useDb, useAuth, useUser, useLogger, useService,
|
|
7
|
+
* useReq, useRes, useParams, useQuery, useQueue, useCache, useBreaker, useTransaction, withContext)
|
|
8
|
+
* across loaders, services, models, and background workers without prop drilling.
|
|
9
|
+
*
|
|
10
|
+
* @module core/context/request
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
14
|
+
|
|
15
|
+
const requestContextStorage = new AsyncLocalStorage();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs a callback within an ambient request or execution context
|
|
19
|
+
* @param {Object} context - Execution context payload
|
|
20
|
+
* @param {Function} fn - Function to execute within context
|
|
21
|
+
* @returns {*} Return value of fn
|
|
22
|
+
*/
|
|
23
|
+
function runWithContext(context, fn) {
|
|
24
|
+
return requestContextStorage.run(context || {}, fn);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run a callback with shallow-merged overrides on top of the current ambient context
|
|
29
|
+
* @param {Object} overrides - Object containing fields to override in current context
|
|
30
|
+
* @param {Function} fn - Function to execute
|
|
31
|
+
* @returns {*} Return value of fn
|
|
32
|
+
*/
|
|
33
|
+
function withContext(overrides, fn) {
|
|
34
|
+
const current = requestContextStorage.getStore() || {};
|
|
35
|
+
const merged = { ...current, ...(overrides || {}) };
|
|
36
|
+
return requestContextStorage.run(merged, fn);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the current active ambient context (returns null if outside context)
|
|
41
|
+
* @returns {Object|null}
|
|
42
|
+
*/
|
|
43
|
+
function getContext() {
|
|
44
|
+
return requestContextStorage.getStore() || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook to access the ambient execution context
|
|
49
|
+
* @returns {Object} Active context object
|
|
50
|
+
* @throws {Error} If called outside of an active execution context
|
|
51
|
+
*/
|
|
52
|
+
function useContext() {
|
|
53
|
+
const store = requestContextStorage.getStore();
|
|
54
|
+
if (!store) {
|
|
55
|
+
throw new Error('useContext() must be called within an active request, job, or execution context.');
|
|
56
|
+
}
|
|
57
|
+
return store;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook to access the ambient database instance
|
|
62
|
+
* @returns {Object|null} Database / ORM instance or null
|
|
63
|
+
*/
|
|
64
|
+
function useDb() {
|
|
65
|
+
const store = requestContextStorage.getStore();
|
|
66
|
+
return store?.db || store?.req?.db || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to access the ambient authenticated user / principal
|
|
71
|
+
* @returns {Object|null} User / Auth object or null
|
|
72
|
+
*/
|
|
73
|
+
function useAuth() {
|
|
74
|
+
const store = requestContextStorage.getStore();
|
|
75
|
+
return (
|
|
76
|
+
store?.user ||
|
|
77
|
+
store?.auth?.user ||
|
|
78
|
+
store?.auth ||
|
|
79
|
+
store?.req?.user ||
|
|
80
|
+
store?.req?.auth?.user ||
|
|
81
|
+
store?.req?.auth ||
|
|
82
|
+
null
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hook to access the ambient authenticated user
|
|
88
|
+
* Alias / dedicated helper for useAuth()
|
|
89
|
+
* @returns {Object|null} User object or null
|
|
90
|
+
*/
|
|
91
|
+
function useUser() {
|
|
92
|
+
return useAuth();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Hook to access the ambient logger
|
|
97
|
+
* @returns {Object} Logger instance
|
|
98
|
+
*/
|
|
99
|
+
function useLogger() {
|
|
100
|
+
const store = requestContextStorage.getStore();
|
|
101
|
+
return store?.logger || store?.req?.logger || console;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hook to access the ambient HTTP request object
|
|
106
|
+
* @returns {Object|null} Express/Hono request object or null if outside HTTP request
|
|
107
|
+
*/
|
|
108
|
+
function useReq() {
|
|
109
|
+
const store = requestContextStorage.getStore();
|
|
110
|
+
return store?.req || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Hook to access the ambient HTTP response object
|
|
115
|
+
* @returns {Object|null} Express/Hono response object or null if outside HTTP request
|
|
116
|
+
*/
|
|
117
|
+
function useRes() {
|
|
118
|
+
const store = requestContextStorage.getStore();
|
|
119
|
+
return store?.res || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Hook to access the ambient route parameters
|
|
124
|
+
* @returns {Object} Route parameters map (e.g. { id: '123' })
|
|
125
|
+
*/
|
|
126
|
+
function useParams() {
|
|
127
|
+
const store = requestContextStorage.getStore();
|
|
128
|
+
return store?.params || store?.req?.params || {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Hook to access the ambient URL query parameters
|
|
133
|
+
* @returns {Object} URL query parameters map
|
|
134
|
+
*/
|
|
135
|
+
function useQuery() {
|
|
136
|
+
const store = requestContextStorage.getStore();
|
|
137
|
+
return store?.query || store?.req?.query || {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Hook to invoke a registered service using the ambient context
|
|
142
|
+
* @param {string} name - Service name (e.g. 'user.get')
|
|
143
|
+
* @param {*} [input={}] - Input payload
|
|
144
|
+
* @param {Object} [opts] - Optional execution options
|
|
145
|
+
* @returns {Promise<*>}
|
|
146
|
+
*/
|
|
147
|
+
async function useService(name, input = {}, opts) {
|
|
148
|
+
const store = requestContextStorage.getStore();
|
|
149
|
+
if (store?.service && typeof store.service === 'function') {
|
|
150
|
+
return store.service(name, input, opts);
|
|
151
|
+
}
|
|
152
|
+
if (store?.req?.service && typeof store.req.service === 'function') {
|
|
153
|
+
return store.req.service(name, input, opts);
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`useService() failed: no service registry available in active context.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Hook to access the ambient background queue
|
|
160
|
+
* @returns {Object|null}
|
|
161
|
+
*/
|
|
162
|
+
function useQueue() {
|
|
163
|
+
const store = requestContextStorage.getStore();
|
|
164
|
+
return store?.queue || store?.req?.queue || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Hook to access the ambient cache
|
|
169
|
+
* @returns {Object|null}
|
|
170
|
+
*/
|
|
171
|
+
function useCache() {
|
|
172
|
+
const store = requestContextStorage.getStore();
|
|
173
|
+
return store?.cache || store?.req?.cache || null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Hook to access or create a circuit breaker by name
|
|
178
|
+
* @param {string} name - Circuit breaker name
|
|
179
|
+
* @param {Object} [options] - Circuit breaker options
|
|
180
|
+
* @returns {Object|null} CircuitBreaker instance or null
|
|
181
|
+
*/
|
|
182
|
+
function useBreaker(name, options) {
|
|
183
|
+
const store = requestContextStorage.getStore();
|
|
184
|
+
if (store?.breaker && typeof store.breaker === 'function') {
|
|
185
|
+
return store.breaker(name, options);
|
|
186
|
+
}
|
|
187
|
+
if (store?.req?.breaker && typeof store.req.breaker === 'function') {
|
|
188
|
+
return store.req.breaker(name, options);
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const { circuitBreakerRegistry } = require('../resilience');
|
|
192
|
+
return circuitBreakerRegistry.getBreaker(name, options);
|
|
193
|
+
} catch (_) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Hook to execute a callback within a managed transaction on the ambient database
|
|
200
|
+
* Automatically binds transactional context to ambient hooks inside the callback.
|
|
201
|
+
* @param {function(trx: any): Promise<*>} callback - Transaction callback
|
|
202
|
+
* @returns {Promise<*>} Result of callback
|
|
203
|
+
*/
|
|
204
|
+
async function useTransaction(callback) {
|
|
205
|
+
const db = useDb();
|
|
206
|
+
if (!db) {
|
|
207
|
+
throw new Error('useTransaction() failed: no database instance available in active context.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (typeof db.transaction === 'function') {
|
|
211
|
+
return db.transaction(async (trx) => {
|
|
212
|
+
const current = requestContextStorage.getStore() || {};
|
|
213
|
+
const txDb = trx.db || trx;
|
|
214
|
+
const txCtx = { ...current, db: txDb, trx: trx.trx || trx };
|
|
215
|
+
return requestContextStorage.run(txCtx, () => callback(trx));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error('useTransaction() failed: db does not support transaction().');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
requestContextStorage,
|
|
224
|
+
runWithContext,
|
|
225
|
+
withContext,
|
|
226
|
+
getContext,
|
|
227
|
+
useContext,
|
|
228
|
+
useDb,
|
|
229
|
+
useAuth,
|
|
230
|
+
useUser,
|
|
231
|
+
useLogger,
|
|
232
|
+
useReq,
|
|
233
|
+
useRes,
|
|
234
|
+
useParams,
|
|
235
|
+
useQuery,
|
|
236
|
+
useService,
|
|
237
|
+
useQueue,
|
|
238
|
+
useCache,
|
|
239
|
+
useBreaker,
|
|
240
|
+
useTransaction,
|
|
241
|
+
};
|
package/core/errors/domain.js
CHANGED
|
@@ -17,20 +17,38 @@ class ValidationError extends HttpError {
|
|
|
17
17
|
* @param {string} [message='Validation failed']
|
|
18
18
|
* @param {Object} [options]
|
|
19
19
|
* @param {Record<string, string[]|string>} [options.fields={}] - Field-specific validation error messages
|
|
20
|
+
* @param {Record<string, string[]>} [options.fieldErrors={}] - Detailed array of errors per field
|
|
20
21
|
* @param {number} [options.status=422]
|
|
22
|
+
* @param {number} [options.statusCode]
|
|
21
23
|
* @param {string} [options.code='VALIDATION_ERROR']
|
|
24
|
+
* @param {string} [options.service] - Service identifier if originating from services layer
|
|
22
25
|
* @param {unknown} [options.details]
|
|
26
|
+
* @param {unknown} [options.issues]
|
|
23
27
|
* @param {unknown} [options.cause]
|
|
24
28
|
*/
|
|
25
29
|
constructor(message = 'Validation failed', options = {}) {
|
|
26
|
-
|
|
30
|
+
const status = typeof options.status === 'number'
|
|
31
|
+
? options.status
|
|
32
|
+
: (typeof options.statusCode === 'number' ? options.statusCode : 422);
|
|
33
|
+
|
|
34
|
+
super(status, message, {
|
|
27
35
|
code: options.code || 'VALIDATION_ERROR',
|
|
28
36
|
expose: true,
|
|
29
37
|
...options,
|
|
30
38
|
});
|
|
31
39
|
|
|
40
|
+
this.statusCode = this.status;
|
|
32
41
|
this.fields = options.fields || {};
|
|
33
|
-
|
|
42
|
+
if (options.fieldErrors) {
|
|
43
|
+
this.fieldErrors = options.fieldErrors;
|
|
44
|
+
}
|
|
45
|
+
if (options.issues !== undefined || options.details !== undefined) {
|
|
46
|
+
this.issues = options.issues !== undefined ? options.issues : (options.details || []);
|
|
47
|
+
this.details = options.details !== undefined ? options.details : this.issues;
|
|
48
|
+
}
|
|
49
|
+
if (options.service) {
|
|
50
|
+
this.service = options.service;
|
|
51
|
+
}
|
|
34
52
|
}
|
|
35
53
|
}
|
|
36
54
|
|
package/core/errors/http.js
CHANGED
|
@@ -23,6 +23,7 @@ class HttpError extends WebspressoError {
|
|
|
23
23
|
super(message, options);
|
|
24
24
|
|
|
25
25
|
this.status = typeof status === 'number' ? status : 500;
|
|
26
|
+
this.statusCode = this.status;
|
|
26
27
|
this.headers = options.headers || {};
|
|
27
28
|
this.expose = options.expose !== undefined ? Boolean(options.expose) : this.status < 500;
|
|
28
29
|
}
|