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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -3
  3. package/bin/commands/add-esbuild.js +7 -3
  4. package/bin/commands/add-tailwind.js +9 -5
  5. package/bin/commands/build.js +20 -4
  6. package/bin/commands/dev.js +57 -7
  7. package/bin/commands/doctor.js +42 -1
  8. package/bin/commands/favicon-generate.js +10 -3
  9. package/bin/commands/new.js +8 -3
  10. package/bin/commands/start.js +3 -1
  11. package/bin/utils/add-command.js +16 -0
  12. package/core/app.js +45 -3
  13. package/core/auth/authenticate.js +3 -3
  14. package/core/auth/hash.js +167 -43
  15. package/core/auth/index.js +13 -1
  16. package/core/auth/jwt.js +21 -2
  17. package/core/auth/manager.js +63 -14
  18. package/core/auth/middleware.js +4 -2
  19. package/core/auth/oauth.js +5 -1
  20. package/core/auth/tokens.js +7 -2
  21. package/core/auth/totp.js +42 -6
  22. package/core/auth/user-agent.js +2 -0
  23. package/core/client/adapter/datastar.js +1401 -0
  24. package/core/client/api.js +1337 -0
  25. package/core/client/controller.js +1706 -0
  26. package/core/client/debug.js +367 -0
  27. package/core/client/devtool.js +1134 -0
  28. package/core/client/forms.js +1164 -0
  29. package/core/client/fragments.js +894 -0
  30. package/core/client/index.d.ts +59 -0
  31. package/core/client/index.js +391 -0
  32. package/core/client/lite.js +175 -0
  33. package/core/client/server-fragments.js +868 -0
  34. package/core/client/storage.js +339 -0
  35. package/core/client/utils.js +281 -0
  36. package/core/compression/middleware.js +277 -26
  37. package/core/context/app-context.js +25 -3
  38. package/core/context/request.js +32 -4
  39. package/core/errors/middleware.js +14 -0
  40. package/core/events/dispatcher.js +33 -0
  41. package/core/forms/flash.js +136 -102
  42. package/core/health/middleware.js +36 -6
  43. package/core/http/pipeline.js +45 -6
  44. package/core/index.js +25 -7
  45. package/core/kernel/view.js +41 -10
  46. package/core/metrics/middleware.js +30 -6
  47. package/core/metrics/registry.js +13 -5
  48. package/core/orm/cache/fingerprint.js +32 -11
  49. package/core/orm/cache/layer.js +19 -2
  50. package/core/orm/hidden.js +145 -0
  51. package/core/orm/model.js +48 -2
  52. package/core/orm/query-builder.js +343 -6
  53. package/core/orm/query-filters.js +14 -2
  54. package/core/orm/repository.js +64 -13
  55. package/core/orm/schema-helpers.js +9 -6
  56. package/core/orm/transaction.js +50 -6
  57. package/core/orm/types.js +4 -0
  58. package/core/queue/adapters/database.js +184 -2
  59. package/core/queue/adapters/memory.js +153 -5
  60. package/core/queue/adapters/redis.js +59 -4
  61. package/core/queue/job.js +14 -1
  62. package/core/queue/manager.js +115 -0
  63. package/core/resilience/circuit-breaker.js +27 -8
  64. package/core/services/builtins/exchange.js +174 -7
  65. package/core/services/builtins/file-manager.js +36 -0
  66. package/core/services/builtins/mail.js +27 -0
  67. package/core/services/builtins/queue.js +144 -0
  68. package/core/services/builtins/security.js +118 -0
  69. package/core/services/builtins/webhook.js +110 -0
  70. package/core/services/executor.js +38 -16
  71. package/core/services/index.js +4 -1
  72. package/core/sse/stream.js +28 -5
  73. package/core/utils/timeout.js +8 -1
  74. package/core/validation/compiler.js +10 -7
  75. package/index.d.ts +1320 -74
  76. package/index.js +128 -8
  77. package/package.json +75 -27
  78. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  79. package/plugins/admin-panel/index.js +19 -8
  80. package/plugins/csrf/index.js +3 -3
  81. package/plugins/data-exchange/export.js +214 -0
  82. package/plugins/data-exchange/import.js +48 -5
  83. package/plugins/data-exchange/index.js +70 -28
  84. package/plugins/data-exchange/preview.js +266 -0
  85. package/plugins/data-exchange/template.js +242 -0
  86. package/plugins/email/admin-component.js +783 -141
  87. package/plugins/email/api-handlers.js +51 -1
  88. package/plugins/email/index.js +5 -0
  89. package/plugins/email/service.js +48 -0
  90. package/plugins/file-manager/admin-component.js +1546 -70
  91. package/plugins/file-manager/api-handlers.js +166 -1
  92. package/plugins/file-manager/core.js +598 -8
  93. package/plugins/file-manager/index.js +8 -0
  94. package/plugins/index.js +7 -0
  95. package/plugins/mcp/auth.js +3 -3
  96. package/plugins/oauth/index.js +36 -3
  97. package/plugins/queue/admin-component.js +1092 -0
  98. package/plugins/queue/api-handlers.js +248 -0
  99. package/plugins/queue/index.js +69 -0
  100. package/plugins/rest-resources/index.js +7 -2
  101. package/plugins/security/admin-component.js +689 -0
  102. package/plugins/security/api-handlers.js +169 -0
  103. package/plugins/security/audit.js +153 -0
  104. package/plugins/security/bruteforce.js +244 -0
  105. package/plugins/security/firewall.js +459 -0
  106. package/plugins/security/headers.js +214 -0
  107. package/plugins/security/honeypot.js +234 -0
  108. package/plugins/security/incident-recorder.js +137 -0
  109. package/plugins/security/index.js +211 -0
  110. package/plugins/seo-checker/analyzer.js +20 -7
  111. package/plugins/upload/index.js +36 -0
  112. package/plugins/webhooks/admin-component.js +702 -175
  113. package/plugins/webhooks/api-handlers.js +75 -1
  114. package/plugins/webhooks/dispatcher.js +290 -11
  115. package/plugins/webhooks/index.js +37 -2
  116. package/plugins/webhooks/sender.js +56 -0
  117. package/plugins/xlsx/engine.js +19 -4
  118. package/src/api/api-loader.js +19 -1
  119. package/src/bundle/index.js +443 -17
  120. package/src/client-runtime/bootstrap-swup.js +9 -0
  121. package/src/client-runtime/mount.js +15 -20
  122. package/src/client-runtime/resolve.js +3 -2
  123. package/src/dev/api-typegen.js +1300 -0
  124. package/src/dev/banner.js +10 -1
  125. package/src/dev/colors.js +1 -1
  126. package/src/dev/index.js +7 -1
  127. package/src/dev/typegen.js +173 -17
  128. package/src/dev-reload/index.js +1 -1
  129. package/src/discovery/file-route-parser.js +17 -3
  130. package/src/discovery/scan-routes.js +4 -1
  131. package/src/file-router.js +253 -69
  132. package/src/helpers.js +46 -26
  133. package/src/njk-frontmatter.js +30 -4
  134. package/src/routing/cascading.js +26 -8
  135. package/src/routing/mount-discovered-routes.js +76 -0
  136. package/src/server.js +231 -70
  137. package/views/partials/webspresso-client-runtime.njk +1 -9
  138. package/.agents/ADMIN.md +0 -426
  139. package/.agents/AGENTS.md +0 -96
  140. package/.agents/ARCHITECTURE.md +0 -163
  141. package/.agents/AUTH.md +0 -581
  142. package/.agents/CACHE.md +0 -148
  143. package/.agents/CLI.md +0 -649
  144. package/.agents/COMPRESSION.md +0 -59
  145. package/.agents/CONTEXT.md +0 -152
  146. package/.agents/CONVENTIONS.md +0 -50
  147. package/.agents/EARLY_HINTS.md +0 -120
  148. package/.agents/EMAIL.md +0 -223
  149. package/.agents/ERRORS.md +0 -260
  150. package/.agents/EVENTS.md +0 -179
  151. package/.agents/FLAGS.md +0 -118
  152. package/.agents/HEALTH.md +0 -118
  153. package/.agents/LIMITS.md +0 -109
  154. package/.agents/LOGGING.md +0 -93
  155. package/.agents/MCP.md +0 -103
  156. package/.agents/METRICS.md +0 -127
  157. package/.agents/MIGRATION_JOBS.md +0 -95
  158. package/.agents/ORM.md +0 -700
  159. package/.agents/PLUGINS.md +0 -729
  160. package/.agents/QUEUE.md +0 -290
  161. package/.agents/RATE_LIMIT.md +0 -147
  162. package/.agents/REALTIME.md +0 -213
  163. package/.agents/RESILIENCE.md +0 -235
  164. package/.agents/ROUTING.md +0 -605
  165. package/.agents/SCALE_OUT.md +0 -109
  166. package/.agents/SECRETS.md +0 -117
  167. package/.agents/SERVICES.md +0 -341
  168. package/.agents/SHUTDOWN.md +0 -74
  169. package/.agents/STREAMING.md +0 -108
  170. package/.agents/WORKFLOW.md +0 -27
  171. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  172. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  173. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  174. package/.agents/skills/webspresso-framework/SKILL.md +0 -89
  175. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
  176. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
  177. package/.agents/skills/webspresso-services/SKILL.md +0 -134
  178. package/.cursorrules +0 -14
  179. package/AGENTS.md +0 -203
  180. package/CLAUDE.md +0 -54
  181. package/core/kernel/run-demo.js +0 -58
  182. package/plugins/admin-panel/styles.js +0 -9
  183. package/plugins/data-exchange/export-xlsx.js +0 -119
  184. 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
+ };
@@ -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, RESERVED_QUERY_KEYS } = require('../../core/orm/query-filters');
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.columns.has(cleanCol) || seen.has(cleanCol)) return;
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 });