@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.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/baremetal/commission-workflows.json +33 -3
  3. package/bin/deploy.js +1 -1
  4. package/cli.md +7 -2
  5. package/conf.js +3 -0
  6. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  7. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  8. package/package.json +1 -1
  9. package/packer/scripts/fuse-tar-root +3 -3
  10. package/scripts/disk-clean.sh +23 -23
  11. package/scripts/gpu-diag.sh +2 -2
  12. package/scripts/ip-info.sh +11 -11
  13. package/scripts/maas-upload-boot-resource.sh +1 -1
  14. package/scripts/nvim.sh +1 -1
  15. package/scripts/packer-setup.sh +13 -13
  16. package/scripts/rocky-setup.sh +2 -2
  17. package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
  18. package/scripts/ssl.sh +7 -7
  19. package/src/api/core/core.service.js +0 -5
  20. package/src/api/default/default.service.js +7 -5
  21. package/src/api/document/document.model.js +30 -1
  22. package/src/api/document/document.router.js +6 -0
  23. package/src/api/document/document.service.js +423 -51
  24. package/src/api/file/file.model.js +112 -4
  25. package/src/api/file/file.ref.json +42 -0
  26. package/src/api/file/file.service.js +380 -32
  27. package/src/api/user/user.model.js +38 -1
  28. package/src/api/user/user.router.js +96 -63
  29. package/src/api/user/user.service.js +81 -48
  30. package/src/cli/baremetal.js +689 -329
  31. package/src/cli/cluster.js +50 -52
  32. package/src/cli/db.js +424 -166
  33. package/src/cli/deploy.js +1 -1
  34. package/src/cli/index.js +12 -1
  35. package/src/cli/lxd.js +3 -3
  36. package/src/cli/repository.js +1 -1
  37. package/src/cli/run.js +2 -1
  38. package/src/cli/ssh.js +10 -10
  39. package/src/client/components/core/Account.js +327 -36
  40. package/src/client/components/core/AgGrid.js +3 -0
  41. package/src/client/components/core/Auth.js +9 -3
  42. package/src/client/components/core/Chat.js +2 -2
  43. package/src/client/components/core/Content.js +159 -78
  44. package/src/client/components/core/Css.js +16 -2
  45. package/src/client/components/core/CssCore.js +16 -12
  46. package/src/client/components/core/FileExplorer.js +115 -8
  47. package/src/client/components/core/Input.js +204 -11
  48. package/src/client/components/core/LogIn.js +42 -20
  49. package/src/client/components/core/Modal.js +257 -177
  50. package/src/client/components/core/Panel.js +324 -27
  51. package/src/client/components/core/PanelForm.js +280 -73
  52. package/src/client/components/core/PublicProfile.js +888 -0
  53. package/src/client/components/core/Router.js +117 -15
  54. package/src/client/components/core/SearchBox.js +1117 -0
  55. package/src/client/components/core/SignUp.js +26 -7
  56. package/src/client/components/core/SocketIo.js +6 -3
  57. package/src/client/components/core/Translate.js +98 -0
  58. package/src/client/components/core/Validator.js +15 -0
  59. package/src/client/components/core/windowGetDimensions.js +6 -6
  60. package/src/client/components/default/MenuDefault.js +59 -12
  61. package/src/client/components/default/RoutesDefault.js +1 -0
  62. package/src/client/services/core/core.service.js +163 -1
  63. package/src/client/services/default/default.management.js +451 -64
  64. package/src/client/services/default/default.service.js +13 -6
  65. package/src/client/services/document/document.service.js +23 -0
  66. package/src/client/services/file/file.service.js +43 -16
  67. package/src/client/services/user/user.service.js +13 -9
  68. package/src/db/DataBaseProvider.js +1 -1
  69. package/src/db/mongo/MongooseDB.js +1 -1
  70. package/src/index.js +1 -1
  71. package/src/mailer/MailerProvider.js +4 -4
  72. package/src/runtime/express/Express.js +2 -1
  73. package/src/runtime/lampp/Lampp.js +2 -2
  74. package/src/server/auth.js +3 -6
  75. package/src/server/data-query.js +449 -0
  76. package/src/server/dns.js +4 -4
  77. package/src/server/object-layer.js +0 -3
  78. 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
- fetch(getApiBaseUrl({ id: options.id, endpoint }), {
34
- method: 'GET',
35
- headers: headersFactory(),
36
- credentials: 'include',
37
- })
38
- .then(async (res) => {
39
- return await res.json();
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
- .then((res) => {
42
- logger.info(res);
43
- return resolve(res);
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
- .catch((error) => {
46
- logger.error(error);
47
- return reject(error);
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 = { id: '', page: 1, limit: 10 }) => {
41
- const { id = '', page, limit } = options;
42
- const query = new URLSearchParams();
43
- if (page) query.set('page', page);
44
- if (limit) query.set('limit', limit);
45
- const queryString = query.toString();
46
- const url = `${getApiBaseUrl({ id, endpoint })}${queryString ? (id.includes('?') ? '&' : '?') + queryString : ''}`;
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',
@@ -20,7 +20,7 @@ class DataBaseProviderService {
20
20
  /**
21
21
  * Internal storage for database connection instances, keyed by host+path.
22
22
  * @type {object.<string, object>}
23
- * @private
23
+ * @method
24
24
  */
25
25
  #instance = {};
26
26
 
@@ -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
@@ -36,7 +36,7 @@ class Underpost {
36
36
  * @type {String}
37
37
  * @memberof Underpost
38
38
  */
39
- static version = 'v2.97.0';
39
+ static version = 'v2.97.5';
40
40
  /**
41
41
  * Repository cli API
42
42
  * @static
@@ -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
- * @private
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
- * @private
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 };
@@ -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: user-agent mismatch' });
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: user-agent mismatch' });
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, File, options = { host: '', path: '' }) {
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
+ };