webspresso 0.0.89 → 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 +450 -53
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/manager.js +36 -26
- package/core/auth/middleware.js +21 -2
- 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 +10 -2
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/scopes.js +2 -1
- 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 +67 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/03-pagination-intro.js +25 -18
- 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 +27 -34
- 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 +131 -18
- package/plugins/admin-panel/modules/admin-users.js +770 -0
- 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/content/client/inline-edit.js +41 -17
- package/plugins/cors.js +12 -8
- package/plugins/csrf/index.js +8 -8
- 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 +220 -28
- package/src/helpers.js +24 -5
- package/src/plugin-manager.js +134 -7
- package/src/server.js +470 -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
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +2 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Background Job Queue Plugin
|
|
3
|
+
* @module plugins/queue
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const {
|
|
11
|
+
createQueueManager,
|
|
12
|
+
MemoryQueueAdapter,
|
|
13
|
+
DatabaseQueueAdapter,
|
|
14
|
+
RedisQueueAdapter,
|
|
15
|
+
} = require('../../core/queue');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {'memory'|'database'|'redis'|Object} [options.adapter='memory'] - Queue backend adapter or name
|
|
20
|
+
* @param {Object} [options.db] - Knex/ORM database instance for database adapter
|
|
21
|
+
* @param {Object} [options.redisClient] - Redis client instance for redis adapter
|
|
22
|
+
* @param {string} [options.tableName='webspresso_jobs'] - Database queue table name
|
|
23
|
+
* @param {number} [options.concurrency=2] - Worker concurrency
|
|
24
|
+
* @param {number} [options.pollInterval=500] - Idle poll interval in ms
|
|
25
|
+
* @param {string} [options.jobsDir] - Path to jobs directory (defaults to <cwd>/jobs)
|
|
26
|
+
* @param {boolean} [options.autoStart=true] - Auto start processing
|
|
27
|
+
* @returns {Object} Plugin definition
|
|
28
|
+
*/
|
|
29
|
+
function queuePlugin(options = {}) {
|
|
30
|
+
const concurrency = options.concurrency || 2;
|
|
31
|
+
const pollInterval = options.pollInterval || 500;
|
|
32
|
+
const autoStart = options.autoStart !== false;
|
|
33
|
+
const jobsDir = options.jobsDir || path.join(process.cwd(), 'jobs');
|
|
34
|
+
|
|
35
|
+
let queueManager = null;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: 'queue',
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
description: 'Background Job Queue Engine with Memory, DB, and Redis adapters',
|
|
41
|
+
|
|
42
|
+
register(ctx) {
|
|
43
|
+
const { app, db } = ctx;
|
|
44
|
+
|
|
45
|
+
let adapter = options.adapter;
|
|
46
|
+
|
|
47
|
+
if (!adapter || adapter === 'memory') {
|
|
48
|
+
adapter = new MemoryQueueAdapter();
|
|
49
|
+
} else if (adapter === 'database' || adapter === 'db') {
|
|
50
|
+
const knexInstance = options.db?.knex || db?.knex || options.knex;
|
|
51
|
+
if (!knexInstance) {
|
|
52
|
+
throw new Error('[queuePlugin] Database queue adapter requires a valid database/knex instance.');
|
|
53
|
+
}
|
|
54
|
+
adapter = new DatabaseQueueAdapter({
|
|
55
|
+
knex: knexInstance,
|
|
56
|
+
tableName: options.tableName || 'webspresso_jobs',
|
|
57
|
+
});
|
|
58
|
+
} else if (adapter === 'redis') {
|
|
59
|
+
const client = options.redisClient || options.redis;
|
|
60
|
+
if (!client) {
|
|
61
|
+
throw new Error('[queuePlugin] Redis queue adapter requires a valid redisClient in options.');
|
|
62
|
+
}
|
|
63
|
+
adapter = new RedisQueueAdapter({
|
|
64
|
+
client,
|
|
65
|
+
prefix: options.prefix,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
queueManager = createQueueManager({
|
|
70
|
+
adapter,
|
|
71
|
+
concurrency,
|
|
72
|
+
pollInterval,
|
|
73
|
+
jobsDir,
|
|
74
|
+
autoStart,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Expose to ctx & app
|
|
78
|
+
ctx.queue = queueManager;
|
|
79
|
+
if (app) {
|
|
80
|
+
app.queue = queueManager;
|
|
81
|
+
app.set('webspresso.queue', queueManager);
|
|
82
|
+
|
|
83
|
+
// Attach req.queue middleware
|
|
84
|
+
app.use((req, res, next) => {
|
|
85
|
+
req.queue = queueManager;
|
|
86
|
+
next();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.queue = queueManager;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
onRoutesReady(ctx) {
|
|
94
|
+
if (jobsDir && fs.existsSync(jobsDir)) {
|
|
95
|
+
queueManager.loadJobsFromDirectory(jobsDir);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async dispose() {
|
|
100
|
+
if (queueManager) {
|
|
101
|
+
await queueManager.drain(5000);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = queuePlugin;
|
|
108
|
+
module.exports.queuePlugin = queuePlugin;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Realtime Plugin
|
|
3
|
+
* Realtime plugin for Webspresso applications
|
|
4
|
+
* @module plugins/realtime
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
createRealtime,
|
|
9
|
+
RealtimeClient,
|
|
10
|
+
RealtimeSubscription,
|
|
11
|
+
SubscriptionRegistry,
|
|
12
|
+
createSubscriptionKey,
|
|
13
|
+
AuthManager,
|
|
14
|
+
ReconnectManager,
|
|
15
|
+
RealtimeError,
|
|
16
|
+
normalizeRealtimeError,
|
|
17
|
+
sanitizeErrorMessage,
|
|
18
|
+
EventEmitter,
|
|
19
|
+
DEFAULT_CAPABILITIES,
|
|
20
|
+
|
|
21
|
+
// Adapters
|
|
22
|
+
websocket,
|
|
23
|
+
createWebSocketAdapter,
|
|
24
|
+
WebSocketAdapter,
|
|
25
|
+
resolveWebSocketUrl,
|
|
26
|
+
|
|
27
|
+
sse,
|
|
28
|
+
createSseAdapter,
|
|
29
|
+
SseAdapter,
|
|
30
|
+
resolveSseUrl,
|
|
31
|
+
|
|
32
|
+
socketIo,
|
|
33
|
+
createSocketIoAdapter,
|
|
34
|
+
SocketIoAdapter,
|
|
35
|
+
|
|
36
|
+
redis,
|
|
37
|
+
createRedisAdapter,
|
|
38
|
+
RedisAdapter,
|
|
39
|
+
} = require('../../core/realtime');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Webspresso Realtime Plugin Factory
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} options
|
|
45
|
+
* @returns {Object} Webspresso plugin definition
|
|
46
|
+
*/
|
|
47
|
+
function realtimePlugin(options = {}) {
|
|
48
|
+
let clientInstance = null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name: 'realtime',
|
|
52
|
+
version: '1.0.0',
|
|
53
|
+
description: 'Framework-agnostic, plugin-based realtime layer for Webspresso',
|
|
54
|
+
_options: options,
|
|
55
|
+
|
|
56
|
+
api: {
|
|
57
|
+
getClient: () => clientInstance,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
setup(app) {
|
|
61
|
+
if (!clientInstance) {
|
|
62
|
+
clientInstance = createRealtime(options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (app) {
|
|
66
|
+
app.realtime = clientInstance;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
if (clientInstance) {
|
|
71
|
+
clientInstance.destroy();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
register(ctx) {
|
|
77
|
+
if (!clientInstance) {
|
|
78
|
+
clientInstance = createRealtime(options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (ctx.app) {
|
|
82
|
+
ctx.app.realtime = clientInstance;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof ctx.onDispose === 'function') {
|
|
86
|
+
ctx.onDispose(() => {
|
|
87
|
+
if (clientInstance) {
|
|
88
|
+
clientInstance.destroy();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
browserReady(app) {
|
|
95
|
+
if (clientInstance) {
|
|
96
|
+
clientInstance.browserReady();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
destroy(app) {
|
|
101
|
+
if (clientInstance) {
|
|
102
|
+
clientInstance.destroy();
|
|
103
|
+
clientInstance = null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function realtime(options = {}) {
|
|
110
|
+
const client = createRealtime(options);
|
|
111
|
+
client.plugin = realtimePlugin(options);
|
|
112
|
+
return client;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
realtimePlugin,
|
|
117
|
+
realtime,
|
|
118
|
+
createRealtime,
|
|
119
|
+
RealtimeClient,
|
|
120
|
+
RealtimeSubscription,
|
|
121
|
+
SubscriptionRegistry,
|
|
122
|
+
createSubscriptionKey,
|
|
123
|
+
AuthManager,
|
|
124
|
+
ReconnectManager,
|
|
125
|
+
RealtimeError,
|
|
126
|
+
normalizeRealtimeError,
|
|
127
|
+
sanitizeErrorMessage,
|
|
128
|
+
EventEmitter,
|
|
129
|
+
DEFAULT_CAPABILITIES,
|
|
130
|
+
|
|
131
|
+
// Adapters
|
|
132
|
+
websocket,
|
|
133
|
+
createWebSocketAdapter,
|
|
134
|
+
WebSocketAdapter,
|
|
135
|
+
resolveWebSocketUrl,
|
|
136
|
+
|
|
137
|
+
sse,
|
|
138
|
+
createSseAdapter,
|
|
139
|
+
SseAdapter,
|
|
140
|
+
resolveSseUrl,
|
|
141
|
+
|
|
142
|
+
socketIo,
|
|
143
|
+
createSocketIoAdapter,
|
|
144
|
+
SocketIoAdapter,
|
|
145
|
+
|
|
146
|
+
redis,
|
|
147
|
+
createRedisAdapter,
|
|
148
|
+
RedisAdapter,
|
|
149
|
+
};
|
|
150
|
+
|
|
@@ -107,6 +107,17 @@ function recaptchaPlugin(options = {}) {
|
|
|
107
107
|
},
|
|
108
108
|
|
|
109
109
|
register(ctx) {
|
|
110
|
+
if (ctx.middlewares) {
|
|
111
|
+
ctx.middlewares.recaptcha = (mwOpts = {}) =>
|
|
112
|
+
createRecaptchaMiddleware({
|
|
113
|
+
secret: resolveSecret(),
|
|
114
|
+
bodyField: mwOpts.bodyField,
|
|
115
|
+
version: mwOpts.version || version,
|
|
116
|
+
minScore: mwOpts.minScore ?? minScore,
|
|
117
|
+
expectedAction: mwOpts.expectedAction ?? expectedAction,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
ctx.addHelper('recaptchaScript', () =>
|
|
111
122
|
version === 'v3' ? recaptchaScriptTag(siteKey) : recaptchaScriptTag()
|
|
112
123
|
);
|
|
@@ -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);
|