@underpostnet/underpost 2.97.0 → 2.97.5
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 +2 -2
- package/baremetal/commission-workflows.json +33 -3
- package/bin/deploy.js +1 -1
- package/cli.md +7 -2
- package/conf.js +3 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/packer/scripts/fuse-tar-root +3 -3
- package/scripts/disk-clean.sh +23 -23
- package/scripts/gpu-diag.sh +2 -2
- package/scripts/ip-info.sh +11 -11
- package/scripts/maas-upload-boot-resource.sh +1 -1
- package/scripts/nvim.sh +1 -1
- package/scripts/packer-setup.sh +13 -13
- package/scripts/rocky-setup.sh +2 -2
- package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
- package/scripts/ssl.sh +7 -7
- package/src/api/core/core.service.js +0 -5
- package/src/api/default/default.service.js +7 -5
- package/src/api/document/document.model.js +30 -1
- package/src/api/document/document.router.js +6 -0
- package/src/api/document/document.service.js +423 -51
- package/src/api/file/file.model.js +112 -4
- package/src/api/file/file.ref.json +42 -0
- package/src/api/file/file.service.js +380 -32
- package/src/api/user/user.model.js +38 -1
- package/src/api/user/user.router.js +96 -63
- package/src/api/user/user.service.js +81 -48
- package/src/cli/baremetal.js +689 -329
- package/src/cli/cluster.js +50 -52
- package/src/cli/db.js +424 -166
- package/src/cli/deploy.js +1 -1
- package/src/cli/index.js +12 -1
- package/src/cli/lxd.js +3 -3
- package/src/cli/repository.js +1 -1
- package/src/cli/run.js +2 -1
- package/src/cli/ssh.js +10 -10
- package/src/client/components/core/Account.js +327 -36
- package/src/client/components/core/AgGrid.js +3 -0
- package/src/client/components/core/Auth.js +9 -3
- package/src/client/components/core/Chat.js +2 -2
- package/src/client/components/core/Content.js +159 -78
- package/src/client/components/core/Css.js +16 -2
- package/src/client/components/core/CssCore.js +16 -12
- package/src/client/components/core/FileExplorer.js +115 -8
- package/src/client/components/core/Input.js +204 -11
- package/src/client/components/core/LogIn.js +42 -20
- package/src/client/components/core/Modal.js +257 -177
- package/src/client/components/core/Panel.js +324 -27
- package/src/client/components/core/PanelForm.js +280 -73
- package/src/client/components/core/PublicProfile.js +888 -0
- package/src/client/components/core/Router.js +117 -15
- package/src/client/components/core/SearchBox.js +1117 -0
- package/src/client/components/core/SignUp.js +26 -7
- package/src/client/components/core/SocketIo.js +6 -3
- package/src/client/components/core/Translate.js +98 -0
- package/src/client/components/core/Validator.js +15 -0
- package/src/client/components/core/windowGetDimensions.js +6 -6
- package/src/client/components/default/MenuDefault.js +59 -12
- package/src/client/components/default/RoutesDefault.js +1 -0
- package/src/client/services/core/core.service.js +163 -1
- package/src/client/services/default/default.management.js +451 -64
- package/src/client/services/default/default.service.js +13 -6
- package/src/client/services/document/document.service.js +23 -0
- package/src/client/services/file/file.service.js +43 -16
- package/src/client/services/user/user.service.js +13 -9
- package/src/db/DataBaseProvider.js +1 -1
- package/src/db/mongo/MongooseDB.js +1 -1
- package/src/index.js +1 -1
- package/src/mailer/MailerProvider.js +4 -4
- package/src/runtime/express/Express.js +2 -1
- package/src/runtime/lampp/Lampp.js +2 -2
- package/src/server/auth.js +3 -6
- package/src/server/data-query.js +449 -0
- package/src/server/dns.js +4 -4
- package/src/server/object-layer.js +0 -3
- package/src/ws/IoInterface.js +2 -2
|
@@ -29,24 +29,51 @@ const FileService = {
|
|
|
29
29
|
}),
|
|
30
30
|
),
|
|
31
31
|
get: (options = { id: '' }) =>
|
|
32
|
-
new Promise((resolve, reject) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
new Promise((resolve, reject) => {
|
|
33
|
+
// Handle blob endpoint - fetch binary data directly
|
|
34
|
+
if (options.id && options.id.startsWith('blob/')) {
|
|
35
|
+
const blobId = options.id.substring(5); // Remove 'blob/' prefix
|
|
36
|
+
fetch(getApiBaseUrl({ id: blobId, endpoint: 'file/blob' }), {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: headersFactory(),
|
|
39
|
+
credentials: 'include',
|
|
40
40
|
})
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
.then(async (res) => {
|
|
42
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
43
|
+
return await res.blob();
|
|
44
|
+
})
|
|
45
|
+
.then((blob) => {
|
|
46
|
+
logger.info('Blob fetched successfully');
|
|
47
|
+
return resolve({
|
|
48
|
+
status: 'success',
|
|
49
|
+
data: [blob],
|
|
50
|
+
});
|
|
51
|
+
})
|
|
52
|
+
.catch((error) => {
|
|
53
|
+
logger.error(error);
|
|
54
|
+
return reject(error);
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
// Handle regular metadata endpoint - fetch JSON
|
|
58
|
+
fetch(getApiBaseUrl({ id: options.id, endpoint }), {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
headers: headersFactory(),
|
|
61
|
+
credentials: 'include',
|
|
44
62
|
})
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
.then(async (res) => {
|
|
64
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
65
|
+
return await res.json();
|
|
66
|
+
})
|
|
67
|
+
.then((res) => {
|
|
68
|
+
logger.info(res);
|
|
69
|
+
return resolve(res);
|
|
70
|
+
})
|
|
71
|
+
.catch((error) => {
|
|
72
|
+
logger.error(error);
|
|
73
|
+
return reject(error);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
50
77
|
delete: (options = { id: '', body: {} }) =>
|
|
51
78
|
new Promise((resolve, reject) =>
|
|
52
79
|
fetch(getApiBaseUrl({ id: options.id, endpoint }), {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Auth } from '../../components/core/Auth.js';
|
|
2
2
|
import { loggerFactory } from '../../components/core/Logger.js';
|
|
3
|
-
import { getApiBaseUrl, headersFactory, payloadFactory } from '../core/core.service.js';
|
|
3
|
+
import { getApiBaseUrl, headersFactory, payloadFactory, buildQueryUrl } from '../core/core.service.js';
|
|
4
4
|
|
|
5
5
|
const logger = loggerFactory(import.meta);
|
|
6
6
|
|
|
@@ -37,15 +37,19 @@ const UserService = {
|
|
|
37
37
|
return reject(error);
|
|
38
38
|
}),
|
|
39
39
|
),
|
|
40
|
-
get: (options = {
|
|
41
|
-
const { id
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
get: (options = {}) => {
|
|
41
|
+
const { id, page, limit, filterModel, sortModel, sort, asc, order } = options;
|
|
42
|
+
const url = buildQueryUrl(getApiBaseUrl({ id, endpoint }), {
|
|
43
|
+
page,
|
|
44
|
+
limit,
|
|
45
|
+
filterModel,
|
|
46
|
+
sortModel,
|
|
47
|
+
sort,
|
|
48
|
+
asc,
|
|
49
|
+
order,
|
|
50
|
+
});
|
|
47
51
|
return new Promise((resolve, reject) =>
|
|
48
|
-
fetch(url, {
|
|
52
|
+
fetch(url.toString(), {
|
|
49
53
|
method: 'GET',
|
|
50
54
|
headers: headersFactory(),
|
|
51
55
|
credentials: 'include',
|
|
@@ -28,7 +28,7 @@ class MongooseDBService {
|
|
|
28
28
|
*/
|
|
29
29
|
async connect(host, name) {
|
|
30
30
|
const uri = `${host}/${name}`;
|
|
31
|
-
logger.info('MongooseDB connect', { host, name, uri });
|
|
31
|
+
// logger.info('MongooseDB connect', { host, name, uri });
|
|
32
32
|
return await mongoose
|
|
33
33
|
.createConnection(uri, {
|
|
34
34
|
serverSelectionTimeoutMS: 5000,
|
package/src/index.js
CHANGED
|
@@ -41,7 +41,7 @@ class MailerProviderService {
|
|
|
41
41
|
/**
|
|
42
42
|
* Internal storage for mailer instances (transporters, options, templates), keyed by ID.
|
|
43
43
|
* @type {object.<string, object>}
|
|
44
|
-
* @
|
|
44
|
+
* @method
|
|
45
45
|
*/
|
|
46
46
|
#instance = {};
|
|
47
47
|
|
|
@@ -108,14 +108,14 @@ class MailerProviderService {
|
|
|
108
108
|
P1: {
|
|
109
109
|
en: `Email confirmed! Thanks.
|
|
110
110
|
<br />
|
|
111
|
-
<span style="font-size: 12px; color: gray">
|
|
112
|
-
If it is not automatically verified,
|
|
111
|
+
<span style="font-size: 12px; color: gray">
|
|
112
|
+
If it is not automatically verified,
|
|
113
113
|
please allow the image to be seen, thank you.
|
|
114
114
|
</span>
|
|
115
115
|
`,
|
|
116
116
|
es: `Correo electrónico confirmado! Gracias.
|
|
117
117
|
<br />
|
|
118
|
-
<span style="font-size: 12px; color: gray">
|
|
118
|
+
<span style="font-size: 12px; color: gray">
|
|
119
119
|
Si no se verifica automáticamente, por favor permita que se vea la imagen, gracias.
|
|
120
120
|
</span>
|
|
121
121
|
`,
|
|
@@ -180,6 +180,7 @@ class ExpressService {
|
|
|
180
180
|
|
|
181
181
|
// Database and Valkey connections
|
|
182
182
|
if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
|
|
183
|
+
|
|
183
184
|
if (valkey) await createValkeyConnection({ host, path }, valkey);
|
|
184
185
|
|
|
185
186
|
// Mailer setup
|
|
@@ -202,7 +203,7 @@ class ExpressService {
|
|
|
202
203
|
for (const api of apis) {
|
|
203
204
|
logger.info(`Build api server`, `${host}${apiPath}/${api}`);
|
|
204
205
|
const { ApiRouter } = await import(`../../api/${api}/${api}.router.js`);
|
|
205
|
-
const router = ApiRouter({ host, path, apiPath, mailer, db, authMiddleware, origins });
|
|
206
|
+
const router = ApiRouter({ app, host, path, apiPath, mailer, db, authMiddleware, origins });
|
|
206
207
|
app.use(`${apiPath}/${api}`, router);
|
|
207
208
|
}
|
|
208
209
|
}
|
|
@@ -20,7 +20,7 @@ const logger = loggerFactory(import.meta);
|
|
|
20
20
|
*/
|
|
21
21
|
class LamppService {
|
|
22
22
|
/**
|
|
23
|
-
* @
|
|
23
|
+
* @method
|
|
24
24
|
* @type {string | undefined}
|
|
25
25
|
* @description Stores the accumulated Apache virtual host configuration (router definition).
|
|
26
26
|
* @memberof LamppService
|
|
@@ -273,7 +273,7 @@ Listen ${port}
|
|
|
273
273
|
ErrorDocument 504 ${path === '/' ? '' : path}/500.html
|
|
274
274
|
|
|
275
275
|
</VirtualHost>
|
|
276
|
-
|
|
276
|
+
|
|
277
277
|
`);
|
|
278
278
|
|
|
279
279
|
return { disabled: false };
|
package/src/server/auth.js
CHANGED
|
@@ -221,7 +221,7 @@ const authMiddlewareFactory = (options = { host: '', path: '' }) => {
|
|
|
221
221
|
|
|
222
222
|
if (payload.userAgent && payload.userAgent !== req.headers['user-agent']) {
|
|
223
223
|
logger.warn(`UA mismatch for ${payload._id}`);
|
|
224
|
-
return res.status(401).json({ status: 'error', message: 'unauthorized
|
|
224
|
+
return res.status(401).json({ status: 'error', message: 'unauthorized device' });
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// Non-guest verify session exists
|
|
@@ -247,7 +247,7 @@ const authMiddlewareFactory = (options = { host: '', path: '' }) => {
|
|
|
247
247
|
// check session userAgent
|
|
248
248
|
if (session.userAgent !== req.headers['user-agent']) {
|
|
249
249
|
logger.warn(`UA mismatch for ${payload._id}`);
|
|
250
|
-
return res.status(401).json({ status: 'error', message: 'unauthorized
|
|
250
|
+
return res.status(401).json({ status: 'error', message: 'unauthorized device' });
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// compare payload host and path with session host and path
|
|
@@ -441,22 +441,19 @@ async function logoutSession(User, req, res) {
|
|
|
441
441
|
* @param {import('express').Request} req The Express request object.
|
|
442
442
|
* @param {import('express').Response} res The Express response object.
|
|
443
443
|
* @param {import('mongoose').Model} User The Mongoose User model.
|
|
444
|
-
* @param {import('mongoose').Model} File The Mongoose File model.
|
|
445
444
|
* @param {object} [options={}] Additional options.
|
|
446
|
-
* @param {Function} options.getDefaultProfileImageId Function to get the default profile image ID.
|
|
447
445
|
* @param {string} options.host The host name.
|
|
448
446
|
* @param {string} options.path The path name.
|
|
449
447
|
* @returns {Promise<{token: string, user: object}>} The access token and user object.
|
|
450
448
|
* @throws {Error} If password validation fails.
|
|
451
449
|
* @memberof Auth
|
|
452
450
|
*/
|
|
453
|
-
async function createUserAndSession(req, res, User,
|
|
451
|
+
async function createUserAndSession(req, res, User, options = { host: '', path: '' }) {
|
|
454
452
|
const pwdCheck = validatePasswordMiddleware(req);
|
|
455
453
|
if (pwdCheck.status === 'error') throw new Error(pwdCheck.message);
|
|
456
454
|
|
|
457
455
|
req.body.password = await hashPassword(req.body.password);
|
|
458
456
|
req.body.role = req.body.role === 'guest' ? 'guest' : 'user';
|
|
459
|
-
req.body.profileImageId = await options.getDefaultProfileImageId(File);
|
|
460
457
|
|
|
461
458
|
const saved = await new User(req.body).save();
|
|
462
459
|
const user = await User.findOne({ _id: saved._id }).select(UserDto.select.get());
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module for parsing data query parameters into Mongoose query options.
|
|
3
|
+
* Supports AG Grid filterModel/sortModel as well as simple legacy parameters.
|
|
4
|
+
* @module src/server/data-query.js
|
|
5
|
+
* @namespace DataQuery
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const DataQuery = {
|
|
9
|
+
/**
|
|
10
|
+
* Parse request query parameters into Mongoose query options
|
|
11
|
+
* @param {Object} params - The request query parameters (req.query)
|
|
12
|
+
* @param {string|Object} [params.filterModel] - AG Grid filterModel as JSON string or object
|
|
13
|
+
* @param {string|Object} [params.sortModel] - AG Grid sortModel as JSON string or object
|
|
14
|
+
* @param {number|string} [params.page=1] - Page number for pagination
|
|
15
|
+
* @param {number|string} [params.limit=10] - Items per page for pagination
|
|
16
|
+
* @param {string} [params.sort] - Simple sort field (legacy)
|
|
17
|
+
* @param {string|boolean} [params.asc] - Simple sort direction (legacy, '1'/'true' for asc)
|
|
18
|
+
* @param {string} [params.order] - Simple order string, e.g. "field1:asc,field2:desc" (legacy)
|
|
19
|
+
* @param {Object} [params.query] - Default query object to merge with filters
|
|
20
|
+
* @memberof DataQuery
|
|
21
|
+
* @returns {Object} { query, sort, skip, limit, page }
|
|
22
|
+
*/
|
|
23
|
+
parse: (params = {}) => {
|
|
24
|
+
let { filterModel, sortModel, page, limit, sort: sortParam, asc, order, query: defaultQuery } = params;
|
|
25
|
+
|
|
26
|
+
// === 1. Pagination ===
|
|
27
|
+
page = parseInt(page, 10) || 1;
|
|
28
|
+
limit = parseInt(limit, 10) || 10;
|
|
29
|
+
const skip = (page - 1) * limit;
|
|
30
|
+
|
|
31
|
+
// === 2. Sorting ===
|
|
32
|
+
const sort = DataQuery._parseSort(sortModel, sortParam, asc, order);
|
|
33
|
+
|
|
34
|
+
// === 3. Filtering ===
|
|
35
|
+
const query = DataQuery._parseFilter(filterModel, defaultQuery);
|
|
36
|
+
|
|
37
|
+
return { query, sort, skip, limit, page };
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse sort parameters from AG Grid sortModel or simple sort params
|
|
42
|
+
* @method
|
|
43
|
+
* @param {string|Object} sortModel - AG Grid sortModel as JSON string or object
|
|
44
|
+
* @param {string} sortParam - Simple sort field (legacy)
|
|
45
|
+
* @param {string|boolean} asc - Simple sort direction (legacy)
|
|
46
|
+
* @param {string} order - Simple order string (legacy)
|
|
47
|
+
* @return {Object} sort object for Mongoose
|
|
48
|
+
* @memberof DataQuery
|
|
49
|
+
*/
|
|
50
|
+
_parseSort: (sortModel, sortParam, asc, order) => {
|
|
51
|
+
const sort = {};
|
|
52
|
+
|
|
53
|
+
// Parse sortModel from string if needed
|
|
54
|
+
if (typeof sortModel === 'string' && sortModel.trim()) {
|
|
55
|
+
try {
|
|
56
|
+
sortModel = JSON.parse(sortModel);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn('DataQuery: Failed to parse sortModel JSON:', e.message);
|
|
59
|
+
sortModel = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// AG Grid sortModel format: [{ colId: 'field', sort: 'asc' | 'desc' }]
|
|
64
|
+
if (Array.isArray(sortModel) && sortModel.length > 0) {
|
|
65
|
+
sortModel.forEach((sortItem) => {
|
|
66
|
+
if (sortItem && sortItem.colId && sortItem.sort) {
|
|
67
|
+
sort[sortItem.colId] = sortItem.sort === 'asc' ? 1 : -1;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return sort;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Simple sort params (legacy support)
|
|
74
|
+
if (sortParam && typeof sortParam === 'string') {
|
|
75
|
+
const direction = asc === '1' || asc === 'true' || asc === true ? 1 : -1;
|
|
76
|
+
sort[sortParam] = direction;
|
|
77
|
+
return sort;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Order param format: "field1:asc,field2:desc"
|
|
81
|
+
if (order && typeof order === 'string') {
|
|
82
|
+
const orderParts = order.split(',');
|
|
83
|
+
orderParts.forEach((part) => {
|
|
84
|
+
const [field, dir] = part.split(':');
|
|
85
|
+
if (field && field.trim()) {
|
|
86
|
+
sort[field.trim()] = dir === 'desc' ? -1 : 1;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return sort;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return sort;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse filter parameters from AG Grid filterModel
|
|
97
|
+
* @method
|
|
98
|
+
* @param {string|Object} filterModel - AG Grid filterModel as JSON string or object
|
|
99
|
+
* @param {Object} defaultQuery - Default query object to merge with filters
|
|
100
|
+
* @return {Object} query object for Mongoose
|
|
101
|
+
* @memberof DataQuery
|
|
102
|
+
*/
|
|
103
|
+
_parseFilter: (filterModel, defaultQuery) => {
|
|
104
|
+
let query = defaultQuery ? { ...defaultQuery } : {};
|
|
105
|
+
|
|
106
|
+
// Parse filterModel from string if needed
|
|
107
|
+
if (typeof filterModel === 'string' && filterModel.trim()) {
|
|
108
|
+
try {
|
|
109
|
+
filterModel = JSON.parse(filterModel);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.warn('DataQuery: Failed to parse filterModel JSON:', e.message);
|
|
112
|
+
filterModel = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!filterModel || typeof filterModel !== 'object' || Array.isArray(filterModel)) {
|
|
117
|
+
return query;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Process each filter in the filterModel
|
|
121
|
+
Object.entries(filterModel).forEach(([field, filter]) => {
|
|
122
|
+
if (!field || !filter) return;
|
|
123
|
+
const fieldQuery = DataQuery._parseFieldFilter(field, filter);
|
|
124
|
+
if (fieldQuery) {
|
|
125
|
+
query = { ...query, ...fieldQuery };
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return query;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse a single field filter
|
|
134
|
+
* @method
|
|
135
|
+
* @param {string} field - The field name
|
|
136
|
+
* @param {Object} filter - The filter object
|
|
137
|
+
* @return {Object|null} query condition for the field or null if invalid
|
|
138
|
+
* @memberof DataQuery
|
|
139
|
+
*/
|
|
140
|
+
_parseFieldFilter: (field, filter) => {
|
|
141
|
+
if (!filter || !filter.filterType) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { filterType } = filter;
|
|
146
|
+
|
|
147
|
+
switch (filterType) {
|
|
148
|
+
case 'text':
|
|
149
|
+
return DataQuery._parseTextFilter(field, filter);
|
|
150
|
+
case 'number':
|
|
151
|
+
return DataQuery._parseNumberFilter(field, filter);
|
|
152
|
+
case 'date':
|
|
153
|
+
return DataQuery._parseDateFilter(field, filter);
|
|
154
|
+
case 'set':
|
|
155
|
+
return DataQuery._parseSetFilter(field, filter);
|
|
156
|
+
case 'multi':
|
|
157
|
+
return DataQuery._parseMultiFilter(field, filter);
|
|
158
|
+
default:
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse text filter
|
|
165
|
+
* @method
|
|
166
|
+
* @param {string} field - The field name
|
|
167
|
+
* @param {Object} filter - The filter object
|
|
168
|
+
* @return {Object|null} query condition for the text field or null if invalid
|
|
169
|
+
* @memberof DataQuery
|
|
170
|
+
*/
|
|
171
|
+
_parseTextFilter: (field, filter) => {
|
|
172
|
+
const { type, filter: filterValue } = filter;
|
|
173
|
+
|
|
174
|
+
if (filterValue === null || filterValue === undefined || filterValue === '') {
|
|
175
|
+
// Handle blank/notBlank without a filter value
|
|
176
|
+
if (type === 'blank' || type === 'notBlank') {
|
|
177
|
+
const query = {};
|
|
178
|
+
if (type === 'blank') {
|
|
179
|
+
query[field] = { $in: [null, ''] };
|
|
180
|
+
} else {
|
|
181
|
+
query[field] = { $nin: [null, ''], $exists: true };
|
|
182
|
+
}
|
|
183
|
+
return query;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const query = {};
|
|
189
|
+
// Escape special regex characters for safety
|
|
190
|
+
const escapeRegex = (str) => String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
191
|
+
|
|
192
|
+
switch (type) {
|
|
193
|
+
case 'equals':
|
|
194
|
+
query[field] = filterValue;
|
|
195
|
+
break;
|
|
196
|
+
case 'notEqual':
|
|
197
|
+
query[field] = { $ne: filterValue };
|
|
198
|
+
break;
|
|
199
|
+
case 'contains':
|
|
200
|
+
query[field] = { $regex: escapeRegex(filterValue), $options: 'i' };
|
|
201
|
+
break;
|
|
202
|
+
case 'notContains':
|
|
203
|
+
query[field] = { $not: { $regex: escapeRegex(filterValue), $options: 'i' } };
|
|
204
|
+
break;
|
|
205
|
+
case 'startsWith':
|
|
206
|
+
query[field] = { $regex: `^${escapeRegex(filterValue)}`, $options: 'i' };
|
|
207
|
+
break;
|
|
208
|
+
case 'endsWith':
|
|
209
|
+
query[field] = { $regex: `${escapeRegex(filterValue)}$`, $options: 'i' };
|
|
210
|
+
break;
|
|
211
|
+
case 'blank':
|
|
212
|
+
query[field] = { $in: [null, ''] };
|
|
213
|
+
break;
|
|
214
|
+
case 'notBlank':
|
|
215
|
+
query[field] = { $nin: [null, ''], $exists: true };
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
query[field] = { $regex: escapeRegex(filterValue), $options: 'i' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return query;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parse number filter
|
|
226
|
+
* @method
|
|
227
|
+
* @param {string} field - The field name
|
|
228
|
+
* @param {Object} filter - The filter object
|
|
229
|
+
* @return {Object|null} query condition for the number field or null if invalid
|
|
230
|
+
* @memberof DataQuery
|
|
231
|
+
*/
|
|
232
|
+
_parseNumberFilter: (field, filter) => {
|
|
233
|
+
const { type, filter: filterValue, filterTo } = filter;
|
|
234
|
+
|
|
235
|
+
if (filterValue === null || filterValue === undefined) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const query = {};
|
|
240
|
+
const numValue = parseFloat(filterValue);
|
|
241
|
+
|
|
242
|
+
if (isNaN(numValue)) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
switch (type) {
|
|
247
|
+
case 'equals':
|
|
248
|
+
query[field] = numValue;
|
|
249
|
+
break;
|
|
250
|
+
case 'notEqual':
|
|
251
|
+
query[field] = { $ne: numValue };
|
|
252
|
+
break;
|
|
253
|
+
case 'lessThan':
|
|
254
|
+
query[field] = { $lt: numValue };
|
|
255
|
+
break;
|
|
256
|
+
case 'lessThanOrEqual':
|
|
257
|
+
query[field] = { $lte: numValue };
|
|
258
|
+
break;
|
|
259
|
+
case 'greaterThan':
|
|
260
|
+
query[field] = { $gt: numValue };
|
|
261
|
+
break;
|
|
262
|
+
case 'greaterThanOrEqual':
|
|
263
|
+
query[field] = { $gte: numValue };
|
|
264
|
+
break;
|
|
265
|
+
case 'inRange':
|
|
266
|
+
if (filterTo !== null && filterTo !== undefined) {
|
|
267
|
+
const numTo = parseFloat(filterTo);
|
|
268
|
+
if (!isNaN(numTo)) {
|
|
269
|
+
query[field] = { $gte: numValue, $lte: numTo };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
case 'blank':
|
|
274
|
+
query[field] = { $in: [null, undefined] };
|
|
275
|
+
break;
|
|
276
|
+
case 'notBlank':
|
|
277
|
+
query[field] = { $nin: [null, undefined], $exists: true };
|
|
278
|
+
break;
|
|
279
|
+
default:
|
|
280
|
+
query[field] = numValue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return query;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse date filter
|
|
288
|
+
* @method
|
|
289
|
+
* @param {string} field - The field name
|
|
290
|
+
* @param {Object} filter - The filter object
|
|
291
|
+
* @return {Object|null} query condition for the date field or null if invalid
|
|
292
|
+
* @memberof DataQuery
|
|
293
|
+
*/
|
|
294
|
+
_parseDateFilter: (field, filter) => {
|
|
295
|
+
const { type, dateFrom, dateTo } = filter;
|
|
296
|
+
|
|
297
|
+
// Handle blank/notBlank without dates
|
|
298
|
+
if (type === 'blank' || type === 'notBlank') {
|
|
299
|
+
const query = {};
|
|
300
|
+
if (type === 'blank') {
|
|
301
|
+
query[field] = { $in: [null, undefined] };
|
|
302
|
+
} else {
|
|
303
|
+
query[field] = { $nin: [null, undefined], $exists: true };
|
|
304
|
+
}
|
|
305
|
+
return query;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!dateFrom && !dateTo) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const query = {};
|
|
313
|
+
|
|
314
|
+
const parseDate = (dateStr) => {
|
|
315
|
+
if (!dateStr) return null;
|
|
316
|
+
const date = new Date(dateStr);
|
|
317
|
+
return isNaN(date.getTime()) ? null : date;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const fromDate = parseDate(dateFrom);
|
|
321
|
+
const toDate = parseDate(dateTo);
|
|
322
|
+
|
|
323
|
+
if (!fromDate && !toDate) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
switch (type) {
|
|
328
|
+
case 'equals':
|
|
329
|
+
if (fromDate) {
|
|
330
|
+
// Match the entire day
|
|
331
|
+
const startOfDay = new Date(fromDate);
|
|
332
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
333
|
+
const endOfDay = new Date(fromDate);
|
|
334
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
335
|
+
query[field] = { $gte: startOfDay, $lte: endOfDay };
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
case 'notEqual':
|
|
339
|
+
if (fromDate) {
|
|
340
|
+
const startOfDay = new Date(fromDate);
|
|
341
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
342
|
+
const endOfDay = new Date(fromDate);
|
|
343
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
344
|
+
query[field] = { $not: { $gte: startOfDay, $lte: endOfDay } };
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
case 'lessThan':
|
|
348
|
+
if (fromDate) {
|
|
349
|
+
query[field] = { $lt: fromDate };
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case 'lessThanOrEqual':
|
|
353
|
+
if (fromDate) {
|
|
354
|
+
query[field] = { $lte: fromDate };
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case 'greaterThan':
|
|
358
|
+
if (fromDate) {
|
|
359
|
+
query[field] = { $gt: fromDate };
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
case 'greaterThanOrEqual':
|
|
363
|
+
if (fromDate) {
|
|
364
|
+
query[field] = { $gte: fromDate };
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
case 'inRange':
|
|
368
|
+
if (fromDate && toDate) {
|
|
369
|
+
// For inRange, set toDate to end of day
|
|
370
|
+
const endOfToDate = new Date(toDate);
|
|
371
|
+
endOfToDate.setHours(23, 59, 59, 999);
|
|
372
|
+
query[field] = { $gte: fromDate, $lte: endOfToDate };
|
|
373
|
+
} else if (fromDate) {
|
|
374
|
+
query[field] = { $gte: fromDate };
|
|
375
|
+
} else if (toDate) {
|
|
376
|
+
const endOfToDate = new Date(toDate);
|
|
377
|
+
endOfToDate.setHours(23, 59, 59, 999);
|
|
378
|
+
query[field] = { $lte: endOfToDate };
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
case 'blank':
|
|
382
|
+
query[field] = { $in: [null, undefined] };
|
|
383
|
+
break;
|
|
384
|
+
case 'notBlank':
|
|
385
|
+
query[field] = { $nin: [null, undefined], $exists: true };
|
|
386
|
+
break;
|
|
387
|
+
default:
|
|
388
|
+
if (fromDate) {
|
|
389
|
+
query[field] = fromDate;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return query;
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Parse set filter
|
|
398
|
+
* @method
|
|
399
|
+
* @param {string} field - The field name
|
|
400
|
+
* @param {Object} filter - The filter object
|
|
401
|
+
* @return {Object|null} query condition for the set field or null if invalid
|
|
402
|
+
* @memberof DataQuery
|
|
403
|
+
*/
|
|
404
|
+
_parseSetFilter: (field, filter) => {
|
|
405
|
+
const { values } = filter;
|
|
406
|
+
|
|
407
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { [field]: { $in: values } };
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Parse multi filter (combines multiple filters with AND/OR)
|
|
416
|
+
* @method
|
|
417
|
+
* @param {string} field - The field name
|
|
418
|
+
* @param {Object} filter - The multi filter object
|
|
419
|
+
* @return {Object|null} query condition for the multi filter or null if invalid
|
|
420
|
+
* @memberof DataQuery
|
|
421
|
+
*/
|
|
422
|
+
_parseMultiFilter: (field, filter) => {
|
|
423
|
+
const { filterModels, operator } = filter;
|
|
424
|
+
|
|
425
|
+
if (!Array.isArray(filterModels) || filterModels.length === 0) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const conditions = filterModels
|
|
430
|
+
.map((subFilter) => DataQuery._parseFieldFilter(field, subFilter))
|
|
431
|
+
.filter((condition) => condition !== null);
|
|
432
|
+
|
|
433
|
+
if (conditions.length === 0) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (conditions.length === 1) {
|
|
438
|
+
return conditions[0];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Combine conditions with AND or OR
|
|
442
|
+
if (operator === 'OR') {
|
|
443
|
+
return { $or: conditions };
|
|
444
|
+
} else {
|
|
445
|
+
// AND operator (default)
|
|
446
|
+
return { $and: conditions };
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
};
|