webspresso 0.0.90 → 0.0.91
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 +358 -8
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/middleware.js +9 -0
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +2 -0
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +90 -17
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +196 -21
- package/src/helpers.js +20 -2
- package/src/plugin-manager.js +124 -4
- package/src/server.js +304 -44
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +140 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +288 -0
- package/src/services/validator.js +281 -0
|
@@ -12,7 +12,15 @@ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
|
12
12
|
function isExternalTarget(to) {
|
|
13
13
|
if (!to || typeof to !== 'string') return false;
|
|
14
14
|
const t = to.trim();
|
|
15
|
-
|
|
15
|
+
// Protocol-relative (//, \\, /\, \/) or URL schemes (http:, https:, javascript:, data:, vbscript:, etc.)
|
|
16
|
+
return (
|
|
17
|
+
/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(t) ||
|
|
18
|
+
t.startsWith('//') ||
|
|
19
|
+
t.startsWith('\\\\') ||
|
|
20
|
+
t.startsWith('/\\') ||
|
|
21
|
+
t.startsWith('\\/') ||
|
|
22
|
+
t.startsWith('\\')
|
|
23
|
+
);
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/**
|
|
@@ -7,6 +7,7 @@ const { attachDbMiddleware } = require('../../src/app-context');
|
|
|
7
7
|
const { getAllModels } = require('../../core/orm/model');
|
|
8
8
|
const { omit } = require('../../core/orm/utils');
|
|
9
9
|
const { trimUrlPathSlashes } = require('../../core/url-path-normalize');
|
|
10
|
+
const { validateQueryComplexity } = require('../../core/orm/complexity');
|
|
10
11
|
|
|
11
12
|
const RESERVED_QUERY_KEYS = new Set(['page', 'perPage', 'sort', 'order', 'include', 'trashed']);
|
|
12
13
|
|
|
@@ -167,22 +168,276 @@ function applySoftDeleteScope(query, countQuery, model, trashed) {
|
|
|
167
168
|
return { query, countQuery };
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Parse sort parameters into an array of { column, direction }
|
|
173
|
+
* @param {import('../../core/orm/types').ModelDefinition} model
|
|
174
|
+
* @param {*} rawSort
|
|
175
|
+
* @param {*} rawOrder
|
|
176
|
+
* @returns {Array<{ column: string, direction: 'asc'|'desc' }>}
|
|
177
|
+
*/
|
|
178
|
+
function parseSortParams(model, rawSort, rawOrder) {
|
|
179
|
+
const result = [];
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
|
|
182
|
+
function addSort(col, dir) {
|
|
183
|
+
if (!col || typeof col !== 'string') return;
|
|
184
|
+
const cleanCol = col.trim();
|
|
185
|
+
if (!model.columns.has(cleanCol) || seen.has(cleanCol)) return;
|
|
186
|
+
seen.add(cleanCol);
|
|
187
|
+
const cleanDir = String(dir || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
188
|
+
result.push({ column: cleanCol, direction: cleanDir });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (Array.isArray(rawSort)) {
|
|
192
|
+
for (const item of rawSort) {
|
|
193
|
+
if (typeof item === 'string') {
|
|
194
|
+
const tokens = item.split(',').map((s) => s.trim()).filter(Boolean);
|
|
195
|
+
for (const token of tokens) {
|
|
196
|
+
if (token.startsWith('-')) {
|
|
197
|
+
addSort(token.slice(1), 'desc');
|
|
198
|
+
} else if (token.startsWith('+')) {
|
|
199
|
+
addSort(token.slice(1), 'asc');
|
|
200
|
+
} else if (token.includes(':')) {
|
|
201
|
+
const [c, d] = token.split(':');
|
|
202
|
+
addSort(c, d);
|
|
203
|
+
} else {
|
|
204
|
+
addSort(token, rawOrder || 'asc');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else if (typeof rawSort === 'string' && rawSort.trim() !== '') {
|
|
210
|
+
const tokens = rawSort.split(',').map((s) => s.trim()).filter(Boolean);
|
|
211
|
+
for (const token of tokens) {
|
|
212
|
+
if (token.startsWith('-')) {
|
|
213
|
+
addSort(token.slice(1), 'desc');
|
|
214
|
+
} else if (token.startsWith('+')) {
|
|
215
|
+
addSort(token.slice(1), 'asc');
|
|
216
|
+
} else if (token.includes(':')) {
|
|
217
|
+
const [c, d] = token.split(':');
|
|
218
|
+
addSort(c, d);
|
|
219
|
+
} else if (tokens.length === 1 && rawOrder) {
|
|
220
|
+
addSort(token, rawOrder);
|
|
221
|
+
} else {
|
|
222
|
+
addSort(token, 'asc');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (result.length === 0) {
|
|
228
|
+
result.push({
|
|
229
|
+
column: model.primaryKey,
|
|
230
|
+
direction: String(rawOrder || 'desc').toLowerCase() === 'asc' ? 'asc' : 'desc',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
170
237
|
function applyColumnFilters(query, countQuery, model, req) {
|
|
171
238
|
let q = query;
|
|
172
239
|
let c = countQuery;
|
|
240
|
+
|
|
241
|
+
function applyCondition(col, op, rawVal) {
|
|
242
|
+
if (!model.columns.has(col)) return;
|
|
243
|
+
const meta = model.columns.get(col);
|
|
244
|
+
const colType = meta?.type;
|
|
245
|
+
|
|
246
|
+
let operator = String(op || 'eq').toLowerCase();
|
|
247
|
+
let val = rawVal;
|
|
248
|
+
|
|
249
|
+
// Normalize booleans
|
|
250
|
+
if (colType === 'boolean' && typeof val === 'string') {
|
|
251
|
+
if (val === 'true' || val === '1') val = true;
|
|
252
|
+
else if (val === 'false' || val === '0') val = false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
switch (operator) {
|
|
256
|
+
case 'eq':
|
|
257
|
+
case '=':
|
|
258
|
+
if (val !== undefined && val !== '') {
|
|
259
|
+
q = q.where(col, '=', val);
|
|
260
|
+
c = c.where(col, '=', val);
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case 'ne':
|
|
265
|
+
case '!=':
|
|
266
|
+
case '<>':
|
|
267
|
+
if (val !== undefined && val !== '') {
|
|
268
|
+
q = q.where(col, '!=', val);
|
|
269
|
+
c = c.where(col, '!=', val);
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case 'gt':
|
|
274
|
+
case '>':
|
|
275
|
+
if (val !== undefined && val !== '') {
|
|
276
|
+
q = q.where(col, '>', val);
|
|
277
|
+
c = c.where(col, '>', val);
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case 'gte':
|
|
282
|
+
case '>=':
|
|
283
|
+
if (val !== undefined && val !== '') {
|
|
284
|
+
q = q.where(col, '>=', val);
|
|
285
|
+
c = c.where(col, '>=', val);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case 'lt':
|
|
290
|
+
case '<':
|
|
291
|
+
if (val !== undefined && val !== '') {
|
|
292
|
+
q = q.where(col, '<', val);
|
|
293
|
+
c = c.where(col, '<', val);
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case 'lte':
|
|
298
|
+
case '<=':
|
|
299
|
+
if (val !== undefined && val !== '') {
|
|
300
|
+
q = q.where(col, '<=', val);
|
|
301
|
+
c = c.where(col, '<=', val);
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
|
|
305
|
+
case 'in': {
|
|
306
|
+
const arr = Array.isArray(val)
|
|
307
|
+
? val
|
|
308
|
+
: (typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : [val]);
|
|
309
|
+
if (arr.length > 0) {
|
|
310
|
+
q = q.whereIn(col, arr);
|
|
311
|
+
c = c.whereIn(col, arr);
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'nin':
|
|
317
|
+
case 'notin': {
|
|
318
|
+
const arr = Array.isArray(val)
|
|
319
|
+
? val
|
|
320
|
+
: (typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : [val]);
|
|
321
|
+
if (arr.length > 0) {
|
|
322
|
+
q = q.whereNotIn(col, arr);
|
|
323
|
+
c = c.whereNotIn(col, arr);
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case 'like':
|
|
329
|
+
if (val !== undefined && val !== '') {
|
|
330
|
+
q = q.where(col, 'like', String(val));
|
|
331
|
+
c = c.where(col, 'like', String(val));
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'ilike':
|
|
336
|
+
if (val !== undefined && val !== '') {
|
|
337
|
+
q = q.where(col, 'ilike', String(val));
|
|
338
|
+
c = c.where(col, 'ilike', String(val));
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'contains':
|
|
343
|
+
if (val !== undefined && val !== '') {
|
|
344
|
+
q = q.where(col, 'like', `%${String(val)}%`);
|
|
345
|
+
c = c.where(col, 'like', `%${String(val)}%`);
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case 'startswith':
|
|
350
|
+
if (val !== undefined && val !== '') {
|
|
351
|
+
q = q.where(col, 'like', `${String(val)}%`);
|
|
352
|
+
c = c.where(col, 'like', `${String(val)}%`);
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
|
|
356
|
+
case 'endswith':
|
|
357
|
+
if (val !== undefined && val !== '') {
|
|
358
|
+
q = q.where(col, 'like', `%${String(val)}`);
|
|
359
|
+
c = c.where(col, 'like', `%${String(val)}`);
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
|
|
363
|
+
case 'isnull':
|
|
364
|
+
case 'is_null': {
|
|
365
|
+
const isTrue = val === true || val === 'true' || val === '1' || val === 1 || val === '';
|
|
366
|
+
if (isTrue) {
|
|
367
|
+
q = q.whereNull(col);
|
|
368
|
+
c = c.whereNull(col);
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
case 'isnotnull':
|
|
374
|
+
case 'is_not_null': {
|
|
375
|
+
const isTrue = val === true || val === 'true' || val === '1' || val === 1 || val === '';
|
|
376
|
+
if (isTrue) {
|
|
377
|
+
q = q.whereNotNull(col);
|
|
378
|
+
c = c.whereNotNull(col);
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case 'between': {
|
|
384
|
+
let parts = [];
|
|
385
|
+
if (Array.isArray(val)) {
|
|
386
|
+
parts = val;
|
|
387
|
+
} else if (typeof val === 'string') {
|
|
388
|
+
parts = val.split(',').map(s => s.trim());
|
|
389
|
+
}
|
|
390
|
+
if (parts.length === 2 && parts[0] !== '' && parts[1] !== '') {
|
|
391
|
+
q = q.where(col, '>=', parts[0]).where(col, '<=', parts[1]);
|
|
392
|
+
c = c.where(col, '>=', parts[0]).where(col, '<=', parts[1]);
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
default:
|
|
398
|
+
if (val !== undefined && val !== '') {
|
|
399
|
+
q = q.where(col, '=', val);
|
|
400
|
+
c = c.where(col, '=', val);
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 1. Process dedicated filter object ?filter[col]=... or ?filter[col][op]=...
|
|
407
|
+
if (req.query.filter && typeof req.query.filter === 'object') {
|
|
408
|
+
for (const [col, colFilter] of Object.entries(req.query.filter)) {
|
|
409
|
+
if (!model.columns.has(col)) continue;
|
|
410
|
+
if (colFilter && typeof colFilter === 'object' && !Array.isArray(colFilter)) {
|
|
411
|
+
if (colFilter.op !== undefined && colFilter.value !== undefined) {
|
|
412
|
+
applyCondition(col, colFilter.op, colFilter.value);
|
|
413
|
+
} else {
|
|
414
|
+
for (const [op, val] of Object.entries(colFilter)) {
|
|
415
|
+
applyCondition(col, op, val);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} else if (colFilter !== undefined && colFilter !== '') {
|
|
419
|
+
applyCondition(col, 'eq', colFilter);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 2. Process top-level query parameters ?price[gte]=100 or ?status=active
|
|
173
425
|
for (const [key, value] of Object.entries(req.query)) {
|
|
174
|
-
if (RESERVED_QUERY_KEYS.has(key)) {
|
|
426
|
+
if (RESERVED_QUERY_KEYS.has(key) || key === 'filter') {
|
|
175
427
|
continue;
|
|
176
428
|
}
|
|
177
429
|
if (!model.columns.has(key)) {
|
|
178
430
|
continue;
|
|
179
431
|
}
|
|
180
|
-
if (value
|
|
181
|
-
|
|
432
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
433
|
+
for (const [op, val] of Object.entries(value)) {
|
|
434
|
+
applyCondition(key, op, val);
|
|
435
|
+
}
|
|
436
|
+
} else if (value !== undefined && value !== '') {
|
|
437
|
+
applyCondition(key, 'eq', value);
|
|
182
438
|
}
|
|
183
|
-
q = q.where(key, value);
|
|
184
|
-
c = c.where(key, value);
|
|
185
439
|
}
|
|
440
|
+
|
|
186
441
|
return { query: q, countQuery: c };
|
|
187
442
|
}
|
|
188
443
|
|
|
@@ -235,21 +490,29 @@ function restResourcePlugin(options = {}) {
|
|
|
235
490
|
...chain(async (req, res) => {
|
|
236
491
|
try {
|
|
237
492
|
const repo = db.getRepository(model.name);
|
|
493
|
+
const complexity = validateQueryComplexity(model, {
|
|
494
|
+
perPage: req.query.perPage,
|
|
495
|
+
includes: parseIncludeParam(model, req.query.include),
|
|
496
|
+
strict: false,
|
|
497
|
+
});
|
|
238
498
|
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
|
239
|
-
const perPage =
|
|
499
|
+
const perPage = complexity.perPage;
|
|
240
500
|
const offset = (page - 1) * perPage;
|
|
241
|
-
const include =
|
|
501
|
+
const include = complexity.includes;
|
|
242
502
|
|
|
243
503
|
let query = repo.query();
|
|
244
504
|
let countQuery = repo.query();
|
|
245
505
|
({ query, countQuery } = applySoftDeleteScope(query, countQuery, model, req.query.trashed));
|
|
246
506
|
({ query, countQuery } = applyColumnFilters(query, countQuery, model, req));
|
|
247
507
|
|
|
248
|
-
const
|
|
249
|
-
const order = String(req.query.order || 'desc').toLowerCase() === 'asc' ? 'asc' : 'desc';
|
|
508
|
+
const sortEntries = parseSortParams(model, req.query.sort, req.query.order);
|
|
250
509
|
|
|
251
510
|
const total = await countQuery.count();
|
|
252
|
-
let listQ = query
|
|
511
|
+
let listQ = query;
|
|
512
|
+
for (const s of sortEntries) {
|
|
513
|
+
listQ = listQ.orderBy(s.column, s.direction);
|
|
514
|
+
}
|
|
515
|
+
listQ = listQ.offset(offset).limit(perPage);
|
|
253
516
|
if (include.length > 0) {
|
|
254
517
|
listQ = listQ.with(...include);
|
|
255
518
|
}
|
|
@@ -276,7 +539,11 @@ function restResourcePlugin(options = {}) {
|
|
|
276
539
|
...chain(async (req, res) => {
|
|
277
540
|
try {
|
|
278
541
|
const repo = db.getRepository(model.name);
|
|
279
|
-
const
|
|
542
|
+
const complexity = validateQueryComplexity(model, {
|
|
543
|
+
includes: parseIncludeParam(model, req.query.include),
|
|
544
|
+
strict: false,
|
|
545
|
+
});
|
|
546
|
+
const include = complexity.includes;
|
|
280
547
|
const record = await repo.findById(req.params.id, { with: include });
|
|
281
548
|
|
|
282
549
|
if (!record) {
|
|
@@ -346,6 +613,8 @@ function restResourcePlugin(options = {}) {
|
|
|
346
613
|
module.exports = restResourcePlugin;
|
|
347
614
|
module.exports.pluralizeSegment = pluralizeSegment;
|
|
348
615
|
module.exports.parseIncludeParam = parseIncludeParam;
|
|
616
|
+
module.exports.parseSortParams = parseSortParams;
|
|
617
|
+
module.exports.applyColumnFilters = applyColumnFilters;
|
|
349
618
|
module.exports.sanitizeRecordTree = sanitizeRecordTree;
|
|
350
619
|
module.exports.pickWritableColumns = pickWritableColumns;
|
|
351
620
|
module.exports.resolveExposedModels = resolveExposedModels;
|
package/plugins/upload/index.js
CHANGED
|
@@ -126,6 +126,22 @@ function uploadPlugin(options = {}) {
|
|
|
126
126
|
version: '1.0.0',
|
|
127
127
|
description: 'Multipart file uploads with pluggable storage',
|
|
128
128
|
|
|
129
|
+
register(ctx) {
|
|
130
|
+
if (ctx.app && ctx.app.serviceRegistry) {
|
|
131
|
+
const { createMediaServices } = require('../../src/services/builtins/media');
|
|
132
|
+
const mediaServices = createMediaServices({
|
|
133
|
+
provider,
|
|
134
|
+
destDir: local?.destDir,
|
|
135
|
+
publicBasePath: local?.publicBasePath,
|
|
136
|
+
});
|
|
137
|
+
for (const [name, def] of Object.entries(mediaServices)) {
|
|
138
|
+
if (!ctx.app.serviceRegistry.has(name)) {
|
|
139
|
+
ctx.app.serviceRegistry.register(name, def);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
129
145
|
onRoutesReady(ctx) {
|
|
130
146
|
const { app } = ctx;
|
|
131
147
|
app.set('webspresso.uploadPath', normalizedPath);
|
package/src/app-context.js
CHANGED
|
@@ -3,21 +3,23 @@
|
|
|
3
3
|
* @module src/app-context
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/** @type {{ db: object|null }} */
|
|
6
|
+
/** @type {{ db: object|null, shutdownManager: object|null, serviceRegistry: object|null }} */
|
|
7
7
|
let context = {
|
|
8
8
|
db: null,
|
|
9
|
+
shutdownManager: null,
|
|
10
|
+
serviceRegistry: null,
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Merge into the current context (typically called once from createApp).
|
|
13
|
-
* @param {{ db?: object|null }} partial
|
|
15
|
+
* @param {{ db?: object|null, shutdownManager?: object|null, serviceRegistry?: object|null }} partial
|
|
14
16
|
*/
|
|
15
17
|
function setAppContext(partial) {
|
|
16
18
|
context = { ...context, ...partial };
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
* @returns {{ db: object|null }}
|
|
22
|
+
* @returns {{ db: object|null, shutdownManager: object|null, serviceRegistry: object|null }}
|
|
21
23
|
*/
|
|
22
24
|
function getAppContext() {
|
|
23
25
|
return context;
|
|
@@ -45,23 +47,57 @@ function hasDb() {
|
|
|
45
47
|
return context.db != null;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @returns {object|null}
|
|
52
|
+
*/
|
|
53
|
+
function getShutdownManager() {
|
|
54
|
+
return context.shutdownManager;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function hasShutdownManager() {
|
|
61
|
+
return context.shutdownManager != null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the registered ServiceRegistry instance
|
|
66
|
+
* @returns {object|null}
|
|
67
|
+
*/
|
|
68
|
+
function getServiceRegistry() {
|
|
69
|
+
return context.serviceRegistry;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a ServiceRegistry is registered
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function hasServiceRegistry() {
|
|
77
|
+
return context.serviceRegistry != null;
|
|
78
|
+
}
|
|
79
|
+
|
|
48
80
|
/**
|
|
49
81
|
* Clear context (e.g. between tests).
|
|
50
82
|
*/
|
|
51
83
|
function resetAppContext() {
|
|
52
|
-
context = { db: null };
|
|
84
|
+
context = { db: null, shutdownManager: null, serviceRegistry: null };
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
/**
|
|
56
|
-
* Express middleware: sets `req.db` from registered app context.
|
|
88
|
+
* Express middleware: sets `req.db` and `req.service` from registered app context.
|
|
57
89
|
* File-based `pages/api/*` routes attach this automatically; use in `setupRoutes`
|
|
58
|
-
* for manually registered handlers that need `req.db`.
|
|
90
|
+
* for manually registered handlers that need `req.db` and `req.service`.
|
|
59
91
|
* @type {import('express').RequestHandler}
|
|
60
92
|
*/
|
|
61
93
|
function attachDbMiddleware(req, res, next) {
|
|
62
94
|
if (context.db != null) {
|
|
63
95
|
req.db = context.db;
|
|
64
96
|
}
|
|
97
|
+
if (context.serviceRegistry != null) {
|
|
98
|
+
req.service = (name, input, opts) =>
|
|
99
|
+
context.serviceRegistry.call(name, input, req.context || { req, res, db: context.db }, opts);
|
|
100
|
+
}
|
|
65
101
|
next();
|
|
66
102
|
}
|
|
67
103
|
|
|
@@ -70,6 +106,10 @@ module.exports = {
|
|
|
70
106
|
getAppContext,
|
|
71
107
|
getDb,
|
|
72
108
|
hasDb,
|
|
109
|
+
getShutdownManager,
|
|
110
|
+
hasShutdownManager,
|
|
111
|
+
getServiceRegistry,
|
|
112
|
+
hasServiceRegistry,
|
|
73
113
|
resetAppContext,
|
|
74
114
|
attachDbMiddleware,
|
|
75
115
|
};
|