webspresso 0.0.108 → 0.0.110
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 +3 -3
- package/bin/commands/add-esbuild.js +7 -3
- package/bin/commands/add-tailwind.js +9 -5
- package/bin/commands/build.js +20 -4
- package/bin/commands/dev.js +57 -7
- package/bin/commands/doctor.js +42 -1
- package/bin/commands/favicon-generate.js +10 -3
- package/bin/commands/new.js +8 -3
- package/bin/commands/start.js +3 -1
- package/bin/utils/add-command.js +16 -0
- package/core/app.js +45 -3
- package/core/auth/authenticate.js +3 -3
- package/core/auth/hash.js +167 -43
- package/core/auth/index.js +13 -1
- package/core/auth/jwt.js +21 -2
- package/core/auth/manager.js +63 -14
- package/core/auth/middleware.js +4 -2
- package/core/auth/oauth.js +5 -1
- package/core/auth/tokens.js +7 -2
- package/core/auth/totp.js +42 -6
- package/core/auth/user-agent.js +2 -0
- package/core/client/adapter/datastar.js +1401 -0
- package/core/client/api.js +1337 -0
- package/core/client/controller.js +1706 -0
- package/core/client/debug.js +367 -0
- package/core/client/devtool.js +1134 -0
- package/core/client/forms.js +1164 -0
- package/core/client/fragments.js +894 -0
- package/core/client/index.d.ts +59 -0
- package/core/client/index.js +391 -0
- package/core/client/lite.js +175 -0
- package/core/client/server-fragments.js +868 -0
- package/core/client/storage.js +339 -0
- package/core/client/utils.js +281 -0
- package/core/compression/middleware.js +277 -26
- package/core/context/app-context.js +25 -3
- package/core/context/request.js +32 -4
- package/core/errors/middleware.js +14 -0
- package/core/events/dispatcher.js +33 -0
- package/core/forms/flash.js +136 -102
- package/core/health/middleware.js +36 -6
- package/core/http/pipeline.js +45 -6
- package/core/index.js +25 -7
- package/core/kernel/view.js +41 -10
- package/core/metrics/middleware.js +30 -6
- package/core/metrics/registry.js +13 -5
- package/core/orm/cache/fingerprint.js +32 -11
- package/core/orm/cache/layer.js +19 -2
- package/core/orm/hidden.js +145 -0
- package/core/orm/model.js +48 -2
- package/core/orm/query-builder.js +343 -6
- package/core/orm/query-filters.js +14 -2
- package/core/orm/repository.js +64 -13
- package/core/orm/schema-helpers.js +9 -6
- package/core/orm/transaction.js +50 -6
- package/core/orm/types.js +4 -0
- package/core/queue/adapters/database.js +184 -2
- package/core/queue/adapters/memory.js +153 -5
- package/core/queue/adapters/redis.js +59 -4
- package/core/queue/job.js +14 -1
- package/core/queue/manager.js +115 -0
- package/core/resilience/circuit-breaker.js +27 -8
- package/core/services/builtins/exchange.js +174 -7
- package/core/services/builtins/file-manager.js +36 -0
- package/core/services/builtins/mail.js +27 -0
- package/core/services/builtins/queue.js +144 -0
- package/core/services/builtins/security.js +118 -0
- package/core/services/builtins/webhook.js +110 -0
- package/core/services/executor.js +38 -16
- package/core/services/index.js +4 -1
- package/core/sse/stream.js +28 -5
- package/core/utils/timeout.js +8 -1
- package/core/validation/compiler.js +10 -7
- package/index.d.ts +1320 -74
- package/index.js +128 -8
- package/package.json +75 -27
- package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
- package/plugins/admin-panel/index.js +19 -8
- package/plugins/csrf/index.js +3 -3
- package/plugins/data-exchange/export.js +214 -0
- package/plugins/data-exchange/import.js +48 -5
- package/plugins/data-exchange/index.js +70 -28
- package/plugins/data-exchange/preview.js +266 -0
- package/plugins/data-exchange/template.js +242 -0
- package/plugins/email/admin-component.js +783 -141
- package/plugins/email/api-handlers.js +51 -1
- package/plugins/email/index.js +5 -0
- package/plugins/email/service.js +48 -0
- package/plugins/file-manager/admin-component.js +1546 -70
- package/plugins/file-manager/api-handlers.js +166 -1
- package/plugins/file-manager/core.js +598 -8
- package/plugins/file-manager/index.js +8 -0
- package/plugins/index.js +7 -0
- package/plugins/mcp/auth.js +3 -3
- package/plugins/oauth/index.js +36 -3
- package/plugins/queue/admin-component.js +1092 -0
- package/plugins/queue/api-handlers.js +248 -0
- package/plugins/queue/index.js +69 -0
- package/plugins/rest-resources/index.js +7 -2
- package/plugins/security/admin-component.js +689 -0
- package/plugins/security/api-handlers.js +169 -0
- package/plugins/security/audit.js +153 -0
- package/plugins/security/bruteforce.js +244 -0
- package/plugins/security/firewall.js +459 -0
- package/plugins/security/headers.js +214 -0
- package/plugins/security/honeypot.js +234 -0
- package/plugins/security/incident-recorder.js +137 -0
- package/plugins/security/index.js +211 -0
- package/plugins/seo-checker/analyzer.js +20 -7
- package/plugins/upload/index.js +36 -0
- package/plugins/webhooks/admin-component.js +702 -175
- package/plugins/webhooks/api-handlers.js +75 -1
- package/plugins/webhooks/dispatcher.js +290 -11
- package/plugins/webhooks/index.js +37 -2
- package/plugins/webhooks/sender.js +56 -0
- package/plugins/xlsx/engine.js +19 -4
- package/src/api/api-loader.js +19 -1
- package/src/bundle/index.js +443 -17
- package/src/client-runtime/bootstrap-swup.js +9 -0
- package/src/client-runtime/mount.js +15 -20
- package/src/client-runtime/resolve.js +3 -2
- package/src/dev/api-typegen.js +1300 -0
- package/src/dev/banner.js +10 -1
- package/src/dev/colors.js +1 -1
- package/src/dev/index.js +7 -1
- package/src/dev/typegen.js +173 -17
- package/src/dev-reload/index.js +1 -1
- package/src/discovery/file-route-parser.js +17 -3
- package/src/discovery/scan-routes.js +4 -1
- package/src/file-router.js +253 -69
- package/src/helpers.js +46 -26
- package/src/njk-frontmatter.js +30 -4
- package/src/routing/cascading.js +26 -8
- package/src/routing/mount-discovered-routes.js +76 -0
- package/src/server.js +231 -70
- package/views/partials/webspresso-client-runtime.njk +1 -9
- package/.agents/ADMIN.md +0 -426
- package/.agents/AGENTS.md +0 -96
- package/.agents/ARCHITECTURE.md +0 -163
- package/.agents/AUTH.md +0 -581
- package/.agents/CACHE.md +0 -148
- package/.agents/CLI.md +0 -649
- package/.agents/COMPRESSION.md +0 -59
- package/.agents/CONTEXT.md +0 -152
- package/.agents/CONVENTIONS.md +0 -50
- package/.agents/EARLY_HINTS.md +0 -120
- package/.agents/EMAIL.md +0 -223
- package/.agents/ERRORS.md +0 -260
- package/.agents/EVENTS.md +0 -179
- package/.agents/FLAGS.md +0 -118
- package/.agents/HEALTH.md +0 -118
- package/.agents/LIMITS.md +0 -109
- package/.agents/LOGGING.md +0 -93
- package/.agents/MCP.md +0 -103
- package/.agents/METRICS.md +0 -127
- package/.agents/MIGRATION_JOBS.md +0 -95
- package/.agents/ORM.md +0 -700
- package/.agents/PLUGINS.md +0 -729
- package/.agents/QUEUE.md +0 -290
- package/.agents/RATE_LIMIT.md +0 -147
- package/.agents/REALTIME.md +0 -213
- package/.agents/RESILIENCE.md +0 -235
- package/.agents/ROUTING.md +0 -605
- package/.agents/SCALE_OUT.md +0 -109
- package/.agents/SECRETS.md +0 -117
- package/.agents/SERVICES.md +0 -341
- package/.agents/SHUTDOWN.md +0 -74
- package/.agents/STREAMING.md +0 -108
- package/.agents/WORKFLOW.md +0 -27
- package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
- package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
- package/.agents/skills/webspresso-framework/SKILL.md +0 -89
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
- package/.agents/skills/webspresso-services/SKILL.md +0 -134
- package/.cursorrules +0 -14
- package/AGENTS.md +0 -203
- package/CLAUDE.md +0 -54
- package/core/kernel/run-demo.js +0 -58
- package/plugins/admin-panel/styles.js +0 -9
- package/plugins/data-exchange/export-xlsx.js +0 -119
- package/src/client-runtime/bootstrap-alpine-swup.js +0 -34
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Queue Admin API Handlers
|
|
3
|
+
* REST endpoints for dashboard metrics, job listing, details, retrying, and dispatching
|
|
4
|
+
* @module plugins/queue/api-handlers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create Queue API route handlers
|
|
11
|
+
*
|
|
12
|
+
* @param {import('../../core/queue/manager').QueueManager} queueManager
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @returns {Record<string, Function>}
|
|
15
|
+
*/
|
|
16
|
+
function createQueueApiHandlers(queueManager, options = {}) {
|
|
17
|
+
if (!queueManager) {
|
|
18
|
+
throw new Error('queueManager is required for createQueueApiHandlers');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* GET /stats — Real-time queue metrics
|
|
23
|
+
*/
|
|
24
|
+
async function getStatsHandler(req, res) {
|
|
25
|
+
try {
|
|
26
|
+
const stats = await queueManager.getStats();
|
|
27
|
+
return res.status(200).json(stats);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return res.status(500).json({ error: err.message || 'Failed to fetch queue statistics' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* GET /jobs — List jobs with status, search and pagination
|
|
35
|
+
*/
|
|
36
|
+
async function listJobsHandler(req, res) {
|
|
37
|
+
try {
|
|
38
|
+
const query = {
|
|
39
|
+
status: req.query.status,
|
|
40
|
+
name: req.query.name,
|
|
41
|
+
search: req.query.search || req.query.q,
|
|
42
|
+
page: req.query.page ? parseInt(req.query.page, 10) : 1,
|
|
43
|
+
limit: req.query.limit ? parseInt(req.query.limit, 10) : 20,
|
|
44
|
+
sortBy: req.query.sortBy || 'createdAt',
|
|
45
|
+
sortOrder: req.query.sortOrder || 'desc',
|
|
46
|
+
};
|
|
47
|
+
const result = await queueManager.listJobs(query);
|
|
48
|
+
return res.status(200).json(result);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return res.status(500).json({ error: err.message || 'Failed to list jobs' });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GET /jobs/:id — Get single job details
|
|
56
|
+
*/
|
|
57
|
+
async function getJobHandler(req, res) {
|
|
58
|
+
try {
|
|
59
|
+
const id = req.params.id;
|
|
60
|
+
const job = await queueManager.getJob(id);
|
|
61
|
+
if (!job) {
|
|
62
|
+
return res.status(404).json({ error: `Job #${id} not found` });
|
|
63
|
+
}
|
|
64
|
+
return res.status(200).json(typeof job.toJSON === 'function' ? job.toJSON() : job);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return res.status(500).json({ error: err.message || 'Failed to get job' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* POST /jobs/:id/retry — Retry a job
|
|
72
|
+
*/
|
|
73
|
+
async function retryJobHandler(req, res) {
|
|
74
|
+
try {
|
|
75
|
+
const id = req.params.id;
|
|
76
|
+
const opts = req.body || {};
|
|
77
|
+
const job = await queueManager.retryJob(id, opts);
|
|
78
|
+
if (!job) {
|
|
79
|
+
return res.status(404).json({ error: `Job #${id} not found to retry` });
|
|
80
|
+
}
|
|
81
|
+
return res.status(200).json({
|
|
82
|
+
success: true,
|
|
83
|
+
message: `Job #${id} has been re-queued for execution`,
|
|
84
|
+
job: typeof job.toJSON === 'function' ? job.toJSON() : job,
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return res.status(400).json({ error: err.message || 'Failed to retry job' });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* POST /jobs/bulk-retry — Bulk retry multiple jobs
|
|
93
|
+
*/
|
|
94
|
+
async function bulkRetryHandler(req, res) {
|
|
95
|
+
try {
|
|
96
|
+
const ids = req.body?.ids || req.body?.jobIds || [];
|
|
97
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
98
|
+
return res.status(400).json({ error: 'ids array is required for bulk retry' });
|
|
99
|
+
}
|
|
100
|
+
const opts = req.body || {};
|
|
101
|
+
const result = await queueManager.bulkRetry(ids, opts);
|
|
102
|
+
return res.status(200).json({
|
|
103
|
+
success: true,
|
|
104
|
+
retried: result.retried,
|
|
105
|
+
total: ids.length,
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return res.status(400).json({ error: err.message || 'Failed to bulk retry jobs' });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* DELETE /jobs/:id (or POST /jobs/:id/cancel) — Cancel and remove a job
|
|
114
|
+
*/
|
|
115
|
+
async function cancelJobHandler(req, res) {
|
|
116
|
+
try {
|
|
117
|
+
const id = req.params.id;
|
|
118
|
+
const result = await queueManager.cancelJob(id);
|
|
119
|
+
return res.status(200).json(result);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return res.status(400).json({ error: err.message || 'Failed to cancel job' });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* POST /jobs/purge — Purge completed and/or failed jobs
|
|
127
|
+
*/
|
|
128
|
+
async function purgeJobsHandler(req, res) {
|
|
129
|
+
try {
|
|
130
|
+
const status = req.body?.status || req.query?.status || ['completed', 'failed'];
|
|
131
|
+
const purgedCount = await queueManager.purgeJobs({ status });
|
|
132
|
+
return res.status(200).json({
|
|
133
|
+
success: true,
|
|
134
|
+
purged: purgedCount,
|
|
135
|
+
message: `Successfully purged ${purgedCount} jobs.`,
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return res.status(500).json({ error: err.message || 'Failed to purge jobs' });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* POST /dispatch — Manually dispatch an ad-hoc job
|
|
144
|
+
*/
|
|
145
|
+
async function dispatchJobHandler(req, res) {
|
|
146
|
+
try {
|
|
147
|
+
const { name, data, delay, delayMs, priority, attempts, timeout } = req.body || {};
|
|
148
|
+
if (!name) {
|
|
149
|
+
return res.status(400).json({ error: 'Job name is required' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const job = await queueManager.dispatch(name, data || {}, {
|
|
153
|
+
delay: delay !== undefined ? delay : delayMs,
|
|
154
|
+
priority: priority !== undefined ? Number(priority) : undefined,
|
|
155
|
+
attempts: attempts !== undefined ? Number(attempts) : undefined,
|
|
156
|
+
timeout: timeout !== undefined ? timeout : undefined,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return res.status(201).json({
|
|
160
|
+
success: true,
|
|
161
|
+
message: `Job '${name}' (${job.id}) enqueued successfully`,
|
|
162
|
+
job: typeof job.toJSON === 'function' ? job.toJSON() : job,
|
|
163
|
+
});
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return res.status(400).json({ error: err.message || 'Failed to dispatch job' });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* POST /pause — Pause queue processing
|
|
171
|
+
*/
|
|
172
|
+
async function pauseQueueHandler(req, res) {
|
|
173
|
+
try {
|
|
174
|
+
queueManager.pause();
|
|
175
|
+
return res.status(200).json({
|
|
176
|
+
success: true,
|
|
177
|
+
isPaused: queueManager.isPaused,
|
|
178
|
+
message: 'Queue processing paused',
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return res.status(500).json({ error: err.message || 'Failed to pause queue' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* POST /resume — Resume queue processing
|
|
187
|
+
*/
|
|
188
|
+
async function resumeQueueHandler(req, res) {
|
|
189
|
+
try {
|
|
190
|
+
queueManager.resume();
|
|
191
|
+
return res.status(200).json({
|
|
192
|
+
success: true,
|
|
193
|
+
isPaused: queueManager.isPaused,
|
|
194
|
+
message: 'Queue processing resumed',
|
|
195
|
+
});
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return res.status(500).json({ error: err.message || 'Failed to resume queue' });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* GET /schedules — List registered schedules (cron jobs)
|
|
203
|
+
*/
|
|
204
|
+
async function getSchedulesHandler(req, res) {
|
|
205
|
+
try {
|
|
206
|
+
const schedules = queueManager.getSchedules ? queueManager.getSchedules() : [];
|
|
207
|
+
return res.status(200).json(schedules);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return res.status(500).json({ error: err.message || 'Failed to list schedules' });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* POST /schedules/:id/trigger — Manually trigger a scheduled job
|
|
215
|
+
*/
|
|
216
|
+
async function triggerScheduleHandler(req, res) {
|
|
217
|
+
try {
|
|
218
|
+
const idOrName = req.params.id || req.body?.id;
|
|
219
|
+
const job = await queueManager.triggerSchedule(idOrName);
|
|
220
|
+
return res.status(200).json({
|
|
221
|
+
success: true,
|
|
222
|
+
message: `Scheduled task '${idOrName}' triggered successfully`,
|
|
223
|
+
job: job ? (typeof job.toJSON === 'function' ? job.toJSON() : job) : null,
|
|
224
|
+
});
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return res.status(400).json({ error: err.message || 'Failed to trigger schedule' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
getStatsHandler,
|
|
232
|
+
listJobsHandler,
|
|
233
|
+
getJobHandler,
|
|
234
|
+
retryJobHandler,
|
|
235
|
+
bulkRetryHandler,
|
|
236
|
+
cancelJobHandler,
|
|
237
|
+
purgeJobsHandler,
|
|
238
|
+
dispatchJobHandler,
|
|
239
|
+
pauseQueueHandler,
|
|
240
|
+
resumeQueueHandler,
|
|
241
|
+
getSchedulesHandler,
|
|
242
|
+
triggerScheduleHandler,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
createQueueApiHandlers,
|
|
248
|
+
};
|
package/plugins/queue/index.js
CHANGED
|
@@ -109,6 +109,22 @@ function queuePlugin(options = {}) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Register built-in queue services if service registry is available
|
|
113
|
+
const registry = ctx.serviceRegistry || ctx.services || ctx.app?.serviceRegistry;
|
|
114
|
+
if (registry && typeof registry.register === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
const { createQueueServices } = require('../../core/services/builtins/queue');
|
|
117
|
+
const services = createQueueServices({ queueManager });
|
|
118
|
+
for (const [name, def] of Object.entries(services)) {
|
|
119
|
+
if (!registry.has(name)) {
|
|
120
|
+
registry.register(name, def);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (svcErr) {
|
|
124
|
+
console.warn('[queuePlugin] Failed to register built-in queue services:', svcErr.message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
this.queue = queueManager;
|
|
113
129
|
},
|
|
114
130
|
|
|
@@ -125,6 +141,59 @@ function queuePlugin(options = {}) {
|
|
|
125
141
|
if (jobsDir && fs.existsSync(jobsDir)) {
|
|
126
142
|
queueManager.loadJobsFromDirectory(jobsDir);
|
|
127
143
|
}
|
|
144
|
+
|
|
145
|
+
// Check if admin-panel plugin is active
|
|
146
|
+
const adminApi = ctx.usePlugin ? ctx.usePlugin('admin-panel') : null;
|
|
147
|
+
if (adminApi && typeof adminApi.registerModule === 'function') {
|
|
148
|
+
const { createQueueApiHandlers } = require('./api-handlers');
|
|
149
|
+
const { generateQueueComponent } = require('./admin-component');
|
|
150
|
+
|
|
151
|
+
const pageTitle = options.adminTitle || 'Job Queue';
|
|
152
|
+
const apiPrefix = options.adminApiPrefix || '/queue';
|
|
153
|
+
const menuOrder = options.menuOrder !== undefined ? options.menuOrder : 55;
|
|
154
|
+
const apiHandlers = createQueueApiHandlers(queueManager, options);
|
|
155
|
+
|
|
156
|
+
adminApi.registerModule({
|
|
157
|
+
id: 'queue',
|
|
158
|
+
title: pageTitle,
|
|
159
|
+
pages: [
|
|
160
|
+
{
|
|
161
|
+
id: 'queue',
|
|
162
|
+
title: pageTitle,
|
|
163
|
+
path: '/queue',
|
|
164
|
+
icon: 'clock',
|
|
165
|
+
component: generateQueueComponent({ apiPrefix }),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
menu: [
|
|
169
|
+
{
|
|
170
|
+
id: 'queue',
|
|
171
|
+
label: pageTitle,
|
|
172
|
+
path: '/queue',
|
|
173
|
+
icon: 'clock',
|
|
174
|
+
order: menuOrder,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
api: {
|
|
178
|
+
prefix: apiPrefix,
|
|
179
|
+
routes: [
|
|
180
|
+
{ method: 'get', path: '/stats', handler: apiHandlers.getStatsHandler },
|
|
181
|
+
{ method: 'get', path: '/jobs', handler: apiHandlers.listJobsHandler },
|
|
182
|
+
{ method: 'get', path: '/jobs/:id', handler: apiHandlers.getJobHandler },
|
|
183
|
+
{ method: 'post', path: '/jobs/:id/retry', handler: apiHandlers.retryJobHandler },
|
|
184
|
+
{ method: 'post', path: '/jobs/bulk-retry', handler: apiHandlers.bulkRetryHandler },
|
|
185
|
+
{ method: 'post', path: '/jobs/:id/cancel', handler: apiHandlers.cancelJobHandler },
|
|
186
|
+
{ method: 'delete', path: '/jobs/:id', handler: apiHandlers.cancelJobHandler },
|
|
187
|
+
{ method: 'post', path: '/jobs/purge', handler: apiHandlers.purgeJobsHandler },
|
|
188
|
+
{ method: 'post', path: '/dispatch', handler: apiHandlers.dispatchJobHandler },
|
|
189
|
+
{ method: 'post', path: '/pause', handler: apiHandlers.pauseQueueHandler },
|
|
190
|
+
{ method: 'post', path: '/resume', handler: apiHandlers.resumeQueueHandler },
|
|
191
|
+
{ method: 'get', path: '/schedules', handler: apiHandlers.getSchedulesHandler },
|
|
192
|
+
{ method: 'post', path: '/schedules/:id/trigger', handler: apiHandlers.triggerScheduleHandler },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
128
197
|
},
|
|
129
198
|
|
|
130
199
|
async dispose() {
|
|
@@ -8,7 +8,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
10
|
const { validateQueryComplexity } = require('../../core/orm/complexity');
|
|
11
|
-
const { applyColumnFilters,
|
|
11
|
+
const { applyColumnFilters, isPublicColumn } = require('../../core/orm/query-filters');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Pluralize a PascalCase model name to a URL segment (e.g. User -> users, Company -> companies)
|
|
@@ -95,6 +95,7 @@ function sanitizeRecordTree(record, model) {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Only real table columns; drop relations and hidden (aligned with repo validation).
|
|
98
|
+
* When model.rest.writable is set, only those columns are accepted (mass-assignment guard).
|
|
98
99
|
* @param {Object} body
|
|
99
100
|
* @param {import('../../core/orm/types').ModelDefinition} model
|
|
100
101
|
* @returns {Object}
|
|
@@ -104,10 +105,14 @@ function pickWritableColumns(body, model) {
|
|
|
104
105
|
if (!body || typeof body !== 'object') {
|
|
105
106
|
return out;
|
|
106
107
|
}
|
|
108
|
+
const writable = Array.isArray(model.rest?.writable) ? new Set(model.rest.writable) : null;
|
|
107
109
|
for (const key of Object.keys(body)) {
|
|
108
110
|
if (!model.columns.has(key)) {
|
|
109
111
|
continue;
|
|
110
112
|
}
|
|
113
|
+
if (writable && !writable.has(key)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
111
116
|
const meta = model.columns.get(key);
|
|
112
117
|
if (meta.primary && meta.autoIncrement) {
|
|
113
118
|
continue;
|
|
@@ -181,7 +186,7 @@ function parseSortParams(model, rawSort, rawOrder) {
|
|
|
181
186
|
function addSort(col, dir) {
|
|
182
187
|
if (!col || typeof col !== 'string') return;
|
|
183
188
|
const cleanCol = col.trim();
|
|
184
|
-
if (!model
|
|
189
|
+
if (!isPublicColumn(model, cleanCol) || seen.has(cleanCol)) return;
|
|
185
190
|
seen.add(cleanCol);
|
|
186
191
|
const cleanDir = String(dir || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
187
192
|
result.push({ column: cleanCol, direction: cleanDir });
|