underpost 3.2.8 → 3.2.9

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.
@@ -675,28 +675,27 @@ class DefaultManagement {
675
675
  EventsUI.onClick(`.management-table-btn-clear-filter-${id}`, async () => {
676
676
  try {
677
677
  const gridApi = AgGrid.grids[gridId];
678
- // Clear all filters
679
- DefaultManagement.clearIdFilter(id);
680
- if (gridApi) {
681
- gridApi.setFilterModel({});
682
- gridApi.applyColumnState({ defaultState: { sort: null } });
683
- }
684
- // Clear token state
685
- if (DefaultManagement.Tokens[id]) {
686
- DefaultManagement.Tokens[id].filterModel = {};
687
- DefaultManagement.Tokens[id].sortModel = [];
688
- }
689
- // Update URL - keep only page and limit
690
- const queryParams = getQueryParams();
691
- setQueryParams({
692
- page: queryParams.page || 1,
693
- limit: queryParams.limit || DefaultManagement.Tokens[id]?.limit || 10,
694
- filterModel: null,
695
- sortModel: null,
696
- id: null,
678
+ await DefaultManagement.runIsolated(id, async () => {
679
+ // Clear all filters without letting grid/query listeners trigger their own reloads.
680
+ DefaultManagement.clearIdFilter(id);
681
+ if (gridApi) {
682
+ gridApi.setFilterModel({});
683
+ gridApi.applyColumnState({ defaultState: { sort: null } });
684
+ }
685
+ if (DefaultManagement.Tokens[id]) {
686
+ DefaultManagement.Tokens[id].filterModel = {};
687
+ DefaultManagement.Tokens[id].sortModel = [];
688
+ }
689
+ const queryParams = getQueryParams();
690
+ setQueryParams({
691
+ page: queryParams.page || 1,
692
+ limit: queryParams.limit || DefaultManagement.Tokens[id]?.limit || 10,
693
+ filterModel: null,
694
+ sortModel: null,
695
+ id: null,
696
+ });
697
+ await DefaultManagement.loadTable(id, { force: true, reload: true, skipUrlUpdate: true });
697
698
  });
698
- // Reload table
699
- await DefaultManagement.loadTable(id, { force: true, reload: true });
700
699
  NotificationManager.Push({
701
700
  html: Translate.instance('success-clear-filter') || 'Filters cleared',
702
701
  status: 'success',
@@ -729,15 +728,11 @@ class DefaultManagement {
729
728
  s(`#ag-pagination-${gridId}`).addEventListener('page-change', async (event) => {
730
729
  const token = DefaultManagement.Tokens[id];
731
730
  token.page = event.detail.page;
732
- // Skip URL update since Pagination component already updated it
733
- await DefaultManagement.loadTable(id, { skipUrlUpdate: true });
734
731
  });
735
732
  s(`#ag-pagination-${gridId}`).addEventListener('limit-change', async (event) => {
736
733
  const token = DefaultManagement.Tokens[id];
737
734
  token.limit = event.detail.limit;
738
735
  token.page = 1; // Reset to first page
739
- // Skip URL update since Pagination component already updated it
740
- await DefaultManagement.loadTable(id, { skipUrlUpdate: true });
741
736
  });
742
737
  RouterEvents[id] = async (...args) => {
743
738
  const queryParams = getQueryParams();
@@ -15,7 +15,14 @@ class SessionMetaDb extends Dexie {
15
15
  }
16
16
  }
17
17
 
18
- const db = new SessionMetaDb();
18
+ // Lazy singleton avoids opening IndexedDB at module-load time.
19
+ // Firefox is measurably slower at IDB open than Chromium; deferring the
20
+ // open until first actual read/write eliminates that latency from page startup.
21
+ let _db = null;
22
+ const getDb = () => {
23
+ if (!_db) _db = new SessionMetaDb();
24
+ return _db;
25
+ };
19
26
 
20
27
  class GuestService {
21
28
  static setUserToken(value = '') {
@@ -59,7 +66,7 @@ class GuestService {
59
66
 
60
67
  static async setMeta(key, value) {
61
68
  try {
62
- await db.meta.put({ key, value, updatedAt: Date.now() });
69
+ await getDb().meta.put({ key, value, updatedAt: Date.now() });
63
70
  } catch (error) {
64
71
  logger.warn('session meta write failed', { key, error: error?.message });
65
72
  }
@@ -67,7 +74,7 @@ class GuestService {
67
74
 
68
75
  static async getMeta(key) {
69
76
  try {
70
- const row = await db.meta.get(key);
77
+ const row = await getDb().meta.get(key);
71
78
  return row ? row.value : null;
72
79
  } catch (error) {
73
80
  logger.warn('session meta read failed', { key, error: error?.message });
package/src/index.js CHANGED
@@ -44,7 +44,7 @@ class Underpost {
44
44
  * @type {String}
45
45
  * @memberof Underpost
46
46
  */
47
- static version = 'v3.2.8';
47
+ static version = 'v3.2.9';
48
48
 
49
49
  /**
50
50
  * Required Node.js major version
@@ -5,7 +5,16 @@
5
5
  * @namespace DataQuery
6
6
  */
7
7
 
8
- export const DataQuery = {
8
+ /**
9
+ * @class DataQuery
10
+ * @description Utility class for parsing request query parameters into Mongoose query options,
11
+ * including support for AG Grid filterModel and sortModel. Provides a static `parse`
12
+ * method that takes in request query parameters and returns an object containing the MongoDB
13
+ * query, sort options, pagination skip/limit, and page number. Designed to be used in API
14
+ * controllers to handle complex querying needs from the frontend.
15
+ * @memberof DataQuery
16
+ */
17
+ class DataQuery {
9
18
  /**
10
19
  * Parse request query parameters into Mongoose query options
11
20
  * @param {Object} params - The request query parameters (req.query)
@@ -20,7 +29,7 @@ export const DataQuery = {
20
29
  * @memberof DataQuery
21
30
  * @returns {Object} { query, sort, skip, limit, page }
22
31
  */
23
- parse: (params = {}) => {
32
+ static parse(params = {}) {
24
33
  let { filterModel, sortModel, page, limit, sort: sortParam, asc, order, query: defaultQuery } = params;
25
34
 
26
35
  // === 1. Pagination ===
@@ -35,7 +44,7 @@ export const DataQuery = {
35
44
  const query = DataQuery._parseFilter(filterModel, defaultQuery);
36
45
 
37
46
  return { query, sort, skip, limit, page };
38
- },
47
+ }
39
48
 
40
49
  /**
41
50
  * Parse sort parameters from AG Grid sortModel or simple sort params
@@ -47,7 +56,7 @@ export const DataQuery = {
47
56
  * @return {Object} sort object for Mongoose
48
57
  * @memberof DataQuery
49
58
  */
50
- _parseSort: (sortModel, sortParam, asc, order) => {
59
+ static _parseSort(sortModel, sortParam, asc, order) {
51
60
  const sort = {};
52
61
 
53
62
  // Parse sortModel from string if needed
@@ -90,7 +99,7 @@ export const DataQuery = {
90
99
  }
91
100
 
92
101
  return sort;
93
- },
102
+ }
94
103
 
95
104
  /**
96
105
  * Parse filter parameters from AG Grid filterModel
@@ -100,7 +109,7 @@ export const DataQuery = {
100
109
  * @return {Object} query object for Mongoose
101
110
  * @memberof DataQuery
102
111
  */
103
- _parseFilter: (filterModel, defaultQuery) => {
112
+ static _parseFilter(filterModel, defaultQuery) {
104
113
  let query = defaultQuery ? { ...defaultQuery } : {};
105
114
 
106
115
  // Parse filterModel from string if needed
@@ -127,7 +136,7 @@ export const DataQuery = {
127
136
  });
128
137
 
129
138
  return query;
130
- },
139
+ }
131
140
 
132
141
  /**
133
142
  * Parse a single field filter
@@ -137,7 +146,7 @@ export const DataQuery = {
137
146
  * @return {Object|null} query condition for the field or null if invalid
138
147
  * @memberof DataQuery
139
148
  */
140
- _parseFieldFilter: (field, filter) => {
149
+ static _parseFieldFilter(field, filter) {
141
150
  if (!filter || !filter.filterType) {
142
151
  return null;
143
152
  }
@@ -158,7 +167,7 @@ export const DataQuery = {
158
167
  default:
159
168
  return null;
160
169
  }
161
- },
170
+ }
162
171
 
163
172
  /**
164
173
  * Parse text filter
@@ -168,7 +177,7 @@ export const DataQuery = {
168
177
  * @return {Object|null} query condition for the text field or null if invalid
169
178
  * @memberof DataQuery
170
179
  */
171
- _parseTextFilter: (field, filter) => {
180
+ static _parseTextFilter(field, filter) {
172
181
  const { type, filter: filterValue } = filter;
173
182
 
174
183
  if (filterValue === null || filterValue === undefined || filterValue === '') {
@@ -219,7 +228,7 @@ export const DataQuery = {
219
228
  }
220
229
 
221
230
  return query;
222
- },
231
+ }
223
232
 
224
233
  /**
225
234
  * Parse number filter
@@ -229,7 +238,7 @@ export const DataQuery = {
229
238
  * @return {Object|null} query condition for the number field or null if invalid
230
239
  * @memberof DataQuery
231
240
  */
232
- _parseNumberFilter: (field, filter) => {
241
+ static _parseNumberFilter(field, filter) {
233
242
  const { type, filter: filterValue, filterTo } = filter;
234
243
 
235
244
  if (filterValue === null || filterValue === undefined) {
@@ -281,7 +290,7 @@ export const DataQuery = {
281
290
  }
282
291
 
283
292
  return query;
284
- },
293
+ }
285
294
 
286
295
  /**
287
296
  * Parse date filter
@@ -291,7 +300,7 @@ export const DataQuery = {
291
300
  * @return {Object|null} query condition for the date field or null if invalid
292
301
  * @memberof DataQuery
293
302
  */
294
- _parseDateFilter: (field, filter) => {
303
+ static _parseDateFilter(field, filter) {
295
304
  const { type, dateFrom, dateTo } = filter;
296
305
 
297
306
  // Handle blank/notBlank without dates
@@ -391,7 +400,7 @@ export const DataQuery = {
391
400
  }
392
401
 
393
402
  return query;
394
- },
403
+ }
395
404
 
396
405
  /**
397
406
  * Parse set filter
@@ -401,7 +410,7 @@ export const DataQuery = {
401
410
  * @return {Object|null} query condition for the set field or null if invalid
402
411
  * @memberof DataQuery
403
412
  */
404
- _parseSetFilter: (field, filter) => {
413
+ static _parseSetFilter(field, filter) {
405
414
  const { values } = filter;
406
415
 
407
416
  if (!Array.isArray(values) || values.length === 0) {
@@ -409,7 +418,7 @@ export const DataQuery = {
409
418
  }
410
419
 
411
420
  return { [field]: { $in: values } };
412
- },
421
+ }
413
422
 
414
423
  /**
415
424
  * Parse multi filter (combines multiple filters with AND/OR)
@@ -419,7 +428,7 @@ export const DataQuery = {
419
428
  * @return {Object|null} query condition for the multi filter or null if invalid
420
429
  * @memberof DataQuery
421
430
  */
422
- _parseMultiFilter: (field, filter) => {
431
+ static _parseMultiFilter(field, filter) {
423
432
  const { filterModels, operator } = filter;
424
433
 
425
434
  if (!Array.isArray(filterModels) || filterModels.length === 0) {
@@ -445,5 +454,8 @@ export const DataQuery = {
445
454
  // AND operator (default)
446
455
  return { $and: conditions };
447
456
  }
448
- },
449
- };
457
+ }
458
+ }
459
+
460
+ export { DataQuery };
461
+ export default DataQuery;
package/src/server/dns.js CHANGED
@@ -101,6 +101,22 @@ class Dns {
101
101
  return ipv4.address;
102
102
  }
103
103
 
104
+ /**
105
+ * Gets the MAC address of the main (default route) network interface.
106
+ * @static
107
+ * @memberof UnderpostDns
108
+ * @returns {string|null} The MAC address, or null if not found.
109
+ */
110
+ static getMainInterfaceMac() {
111
+ const interfaceName = Dns.getDefaultNetworkInterface();
112
+ const networkInfo = os.networkInterfaces()[interfaceName];
113
+ if (!networkInfo || networkInfo.length === 0) {
114
+ logger.error(`Could not find network interface: ${interfaceName}`);
115
+ return null;
116
+ }
117
+ return networkInfo[0].mac;
118
+ }
119
+
104
120
  /**
105
121
  * Setup nftables tables and chains if they don't exist.
106
122
  * @static
@@ -491,6 +507,12 @@ class Dns {
491
507
  });
492
508
  }
493
509
 
510
+ if (options.mac) {
511
+ const mac = Dns.getMainInterfaceMac();
512
+ console.log(mac);
513
+ return mac;
514
+ }
515
+
494
516
  let ip;
495
517
  if (options.dhcp) ip = Dns.getLocalIPv4Address();
496
518
  else ip = await Dns.getPublicIp();
@@ -133,11 +133,19 @@ class UnderpostStartUp {
133
133
  * @param {boolean} options.underpostQuicklyInstall - Whether to use underpost quickly install.
134
134
  * @param {boolean} options.skipPullBase - Whether to skip pulling the base code.
135
135
  * @param {boolean} options.skipFullBuild - Whether to skip building the full client bundle.
136
+ * @param {boolean} options.pullBundle - When true, download pre-built client bundle from Cloudinary via pull-bundle before starting.
136
137
  */
137
138
  async callback(
138
139
  deployId = 'dd-default',
139
140
  env = 'development',
140
- options = { build: false, run: false, underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false },
141
+ options = {
142
+ build: false,
143
+ run: false,
144
+ underpostQuicklyInstall: false,
145
+ skipPullBase: false,
146
+ skipFullBuild: false,
147
+ pullBundle: false,
148
+ },
141
149
  ) {
142
150
  Underpost.env.set('container-status', `${deployId}-${env}-build-deployment`);
143
151
  if (options.build === true) await Underpost.start.build(deployId, env, options);
@@ -152,12 +160,14 @@ class UnderpostStartUp {
152
160
  * @param {boolean} options.skipPullBase - Whether to skip pulling the base code and use the current workspace code directly.
153
161
  * @param {boolean} options.underpostQuicklyInstall - Whether to use underpost quickly install.
154
162
  * @param {boolean} options.skipFullBuild - Whether to skip building the full client bundle.
163
+ * @param {boolean} options.pullBundle - When true, download pre-built client bundle from Cloudinary via pull-bundle (must be pushed first with push-bundle).
164
+ * This flag is independent of skipFullBuild: it can be combined with skipFullBuild or used alone.
155
165
  * @memberof UnderpostStartUp
156
166
  */
157
167
  async build(
158
168
  deployId = 'dd-default',
159
169
  env = 'development',
160
- options = { underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false },
170
+ options = { underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false, pullBundle: false },
161
171
  ) {
162
172
  const buildBasePath = `/home/dd`;
163
173
  const repoName = `engine-${deployId.split('-')[1]}`;
@@ -176,7 +186,8 @@ class UnderpostStartUp {
176
186
  for (const itcScript of itcScripts)
177
187
  if (itcScript.match(deployId)) shellExec(`node ./engine-private/itc-scripts/${itcScript}`);
178
188
  }
179
- if (!options.skipFullBuild) shellExec(`node bin client ${deployId}`);
189
+ if (options.pullBundle === true) shellExec(`node bin run pull-bundle --deploy-id ${deployId}`);
190
+ else if (!options.skipFullBuild) shellExec(`node bin client ${deployId}`);
180
191
  },
181
192
  /**
182
193
  * Runs a deployment.
@@ -22,7 +22,7 @@ const logger = loggerFactory(import.meta);
22
22
  /** @type {Record<string, import('iovalkey').default>} */
23
23
  const ValkeyInstances = {};
24
24
 
25
- /** @type {Record<string, 'connected' | 'error'>} */
25
+ /** @type {Record<string, 'connected' | 'reconnecting' | 'error'>} */
26
26
  const ValkeyStatus = {};
27
27
 
28
28
  /**
@@ -57,7 +57,10 @@ const createValkeyConnection = async (instance = {}, connectionOptions = {}) =>
57
57
  const client = new Valkey({
58
58
  port: connectionOptions.port ?? undefined,
59
59
  host: connectionOptions.host ?? undefined,
60
- retryStrategy: (attempt) => (attempt === 1 ? undefined : 1000),
60
+ // Retry indefinitely with capped exponential backoff (1 s 30 s)
61
+ retryStrategy: (attempt) => Math.min(attempt * 1000, 30000),
62
+ // Fail commands immediately when not connected; do NOT queue them
63
+ maxRetriesPerRequest: 0,
61
64
  });
62
65
 
63
66
  client.on('ready', () => {
@@ -68,6 +71,10 @@ const createValkeyConnection = async (instance = {}, connectionOptions = {}) =>
68
71
  ValkeyStatus[key] = 'error';
69
72
  logger.error('Valkey error', { err: err?.message, instance });
70
73
  });
74
+ client.on('reconnecting', () => {
75
+ ValkeyStatus[key] = 'reconnecting';
76
+ logger.warn('Valkey reconnecting...', { instance });
77
+ });
71
78
  client.on('end', () => {
72
79
  ValkeyStatus[key] = 'error';
73
80
  logger.warn('Valkey connection ended', { instance });
package/typedoc.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "Nexodev - ERP, CRM Development & Cloud DevOps Services",
3
- "entryPoints": ["./src/server", "./src/api", "./src/db", "./src/ws", "./src/grpc", "./src/mailer", "./src/runtime"],
3
+ "entryPoints": [
4
+ "./src/server",
5
+ "./src/api",
6
+ "./src/cli",
7
+ "./src/db",
8
+ "./src/ws",
9
+ "./src/grpc",
10
+ "./src/mailer",
11
+ "./src/runtime"
12
+ ],
4
13
  "entryPointStrategy": "expand",
5
14
  "exclude": ["**/node_modules/**", "**/docs/**", "**/client/**"],
6
15
  "out": "./public/www.nexodev.org/docs/",