@manyos/smileconnect-api 1.74.0 → 1.74.3

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.
@@ -7,7 +7,10 @@
7
7
  "Bash(npm view *)",
8
8
  "Bash(npm pack *)",
9
9
  "WebFetch(domain:unpkg.com)",
10
- "WebFetch(domain:www.npmjs.com)"
10
+ "WebFetch(domain:www.npmjs.com)",
11
+ "Bash(node *)",
12
+ "Bash(grep -E '^\\(p-queue|proper-lockfile|async-mutex|p-limit\\)$')",
13
+ "Bash(awk NR>=160 && NR<=165 {printf \"%d: \", NR; for \\(i=1;i<=length\\($0\\);i++\\) {c=substr\\($0,i,1\\); printf \"%s\", c} printf \"\\\\n\"} *)"
11
14
  ]
12
15
  }
13
16
  }
@@ -153,7 +153,7 @@ async function renameScript(scriptIdOld, scriptIdNew) {
153
153
  })
154
154
  scriptDefOps[usage.scriptType] = scriptDefNew
155
155
  }
156
- setClient(usage.client, clientConfig)
156
+ await setClient(usage.client, clientConfig)
157
157
  }
158
158
  return true;
159
159
  }
package/docs/releases.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # Release Notes
2
2
 
3
3
  ## API
4
- ### 1.73.1 - 29.04.26
4
+ ### 1.74.3 - 20.05.26
5
+ Improve client config validation to avoid duplicate client configs.
6
+
7
+ ### 1.74.1 - 12.05.26
8
+ Fix issue with dynamic openapi spec for worklogs.
9
+
10
+ ### 1.74.0 - 29.04.26
5
11
  Update node.js to v22 and replace request-native with native fetch in adapter.
6
12
 
7
13
  ### 1.73.0 - 28.04.26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manyos/smileconnect-api",
3
- "version": "1.74.0",
3
+ "version": "1.74.3",
4
4
  "description": "A proxy and abstraction layer for BMCs IT Service Management Suite",
5
5
  "main": "app.js",
6
6
  "scripts": {
@@ -295,7 +295,7 @@ module.exports = (function() {
295
295
  });
296
296
 
297
297
  routes.delete('/clients/:id', isAuthorizedAdmin,
298
- function (req, res, next) {
298
+ async function (req, res, next) {
299
299
  const id = req.params.id;
300
300
  setEventData(
301
301
  req,
@@ -305,7 +305,11 @@ module.exports = (function() {
305
305
  id
306
306
  );
307
307
  try {
308
- config.deleteClient(id);
308
+ const removed = await config.deleteClient(id);
309
+ if (!removed) {
310
+ req.errorStatus = 404;
311
+ return next(new Error(`client '${id}' not found`));
312
+ }
309
313
  req.result = {
310
314
  "status": "success"
311
315
  };
@@ -317,7 +321,7 @@ module.exports = (function() {
317
321
 
318
322
  routes.post('/clients', isAuthorizedAdmin,
319
323
  checkSchema(clientConfigSchema),
320
- function (req, res, next) {
324
+ async function (req, res, next) {
321
325
  const origData = JSON.parse(JSON.stringify(req.body));
322
326
  setEventData(
323
327
  req,
@@ -336,7 +340,7 @@ module.exports = (function() {
336
340
  try {
337
341
  const clientConfig = req.body.data;
338
342
  const clientName = clientConfig.name;
339
- config.setClient(clientName, clientConfig)
343
+ await config.setClient(clientName, clientConfig)
340
344
  const savedClientConfig = config.getClient(clientName);
341
345
  req.result = {
342
346
  "data": savedClientConfig
@@ -351,7 +355,7 @@ module.exports = (function() {
351
355
 
352
356
  routes.put('/clients/:id', isAuthorizedAdmin,
353
357
  checkSchema(clientConfigSchema),
354
- function (req, res, next) {
358
+ async function (req, res, next) {
355
359
  const id = req.params.id;
356
360
  const origData = JSON.parse(JSON.stringify(req.body));
357
361
  setEventData(
@@ -370,7 +374,7 @@ module.exports = (function() {
370
374
  try {
371
375
  const clientConfig = req.body.data;
372
376
  const clientName = clientConfig.name;
373
- config.setClient(id, clientConfig);
377
+ await config.setClient(id, clientConfig);
374
378
  const savedClientConfig = config.getClient(clientName);
375
379
  req.result = {
376
380
  "data": savedClientConfig
package/util/config.js CHANGED
@@ -1,11 +1,14 @@
1
1
  const fs = require('fs');
2
2
  const fsPromises = require('fs').promises;
3
+ const crypto = require('crypto');
4
+ const pLimit = require('p-limit');
3
5
  require('dotenv').config();
4
6
 
5
7
  const path = require('path');
6
8
  const log = require('@manyos/logger').setupLog('SMILEconnect', path.basename(__filename));
7
9
 
8
10
  const clientConfigFile = 'conf/clients.json';
11
+ const clientConfigWriteLimit = pLimit(1);
9
12
  const arquery = require('../util/arquery');
10
13
  const mappingUtil = require('../util/mappingUtil');
11
14
  const CacheService = require ('../util/cache.service');
@@ -123,29 +126,66 @@ function setMappings(mapping) {
123
126
  return true
124
127
  }
125
128
 
126
- function setClient(clientName, clientConfig) {
129
+ async function setClient(clientName, clientConfig) {
127
130
  log.debug('start to update config in file', clientConfigFile);
128
131
  log.debug('set client data', clientName, clientConfig);
129
- let config = JSON.parse(fs.readFileSync(clientConfigFile)) || {};
130
- config = config.filter(function( obj ) {
131
- return obj.name !== clientName;
132
+ if (!clientConfig || typeof clientConfig !== 'object' || !clientConfig.name) {
133
+ throw new Error('setClient: clientConfig.name is required');
134
+ }
135
+ const normalizedParam = typeof clientName === 'string' ? clientName.trim() : clientName;
136
+ clientConfig.name = clientConfig.name.trim();
137
+ return clientConfigWriteLimit(async () => {
138
+ let config;
139
+ try {
140
+ const raw = await fsPromises.readFile(clientConfigFile, 'utf8');
141
+ config = JSON.parse(raw);
142
+ } catch (e) {
143
+ log.warn('setClient: cannot parse existing clients.json, starting empty', e.message);
144
+ config = [];
145
+ }
146
+ if (!Array.isArray(config)) config = [];
147
+ const namesToRemove = new Set([normalizedParam, clientConfig.name].filter(Boolean));
148
+ config = config.filter(obj => obj && !namesToRemove.has(obj.name && obj.name.trim ? obj.name.trim() : obj.name));
149
+ config.push(clientConfig);
150
+ log.debug('new config size', config.length);
151
+ const tmp = `${clientConfigFile}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
152
+ await fsPromises.writeFile(tmp, JSON.stringify(config, null, 2));
153
+ await fsPromises.rename(tmp, clientConfigFile);
154
+ return true;
132
155
  });
133
- config.push(clientConfig)
134
- log.debug('new config', config);
135
- fs.writeFileSync(clientConfigFile, JSON.stringify(config, null, 2));
136
- return true
137
156
  }
138
157
 
139
- function deleteClient(clientName) {
158
+ async function deleteClient(clientName) {
140
159
  log.debug('start to update config in file', clientConfigFile);
141
160
  log.debug('delete client data', clientName);
142
- let config = JSON.parse(fs.readFileSync(clientConfigFile)) || {};
143
- config = config.filter(function( obj ) {
144
- return obj.name !== clientName;
161
+ const normalizedName = typeof clientName === 'string' ? clientName.trim() : clientName;
162
+ return clientConfigWriteLimit(async () => {
163
+ let config;
164
+ try {
165
+ const raw = await fsPromises.readFile(clientConfigFile, 'utf8');
166
+ config = JSON.parse(raw);
167
+ } catch (e) {
168
+ log.warn('deleteClient: cannot parse existing clients.json, nothing to delete', e.message);
169
+ return false;
170
+ }
171
+ if (!Array.isArray(config)) return false;
172
+ const before = config.length;
173
+ config = config.filter(obj => {
174
+ if (!obj || !obj.name) return true;
175
+ const objName = typeof obj.name === 'string' ? obj.name.trim() : obj.name;
176
+ return objName !== normalizedName;
177
+ });
178
+ const removed = before - config.length;
179
+ if (removed === 0) {
180
+ log.warn('deleteClient: no entry matched', clientName);
181
+ return false;
182
+ }
183
+ const tmp = `${clientConfigFile}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
184
+ await fsPromises.writeFile(tmp, JSON.stringify(config, null, 2));
185
+ await fsPromises.rename(tmp, clientConfigFile);
186
+ log.debug('deleteClient: removed', removed, 'entries for', normalizedName);
187
+ return true;
145
188
  });
146
- log.debug('new config', config);
147
- fs.writeFileSync(clientConfigFile, JSON.stringify(config, null, 2));
148
- return true
149
189
  }
150
190
 
151
191
  function getConstants(objectType) {
@@ -243,7 +283,7 @@ async function checkCustomFormMapping() {
243
283
  }
244
284
  }
245
285
 
246
- function checkClientConfig(client) {
286
+ async function checkClientConfig(client) {
247
287
  let updateRequired = false;
248
288
  const clientKeys = [
249
289
  'cmdbobject',
@@ -361,25 +401,25 @@ function checkClientConfig(client) {
361
401
  log.warn(`check config for client ${client.name} done. Update required.`);
362
402
  client.config = clientConfig;
363
403
  //save
364
- setClient(client.name, client);
404
+ await setClient(client.name, client);
365
405
  log.warn(`check config for client ${client.name} done. Update done.`);
366
406
  }
367
407
  log.info(`check config for client ${client.name} done. No update required.`);
368
408
  }
369
409
 
370
- function checkClientConfigs() {
410
+ async function checkClientConfigs() {
371
411
  log.info('check client configs');
372
412
  const clientConfigs = getClients();
373
- clientConfigs.forEach(clientConfig => {
374
- checkClientConfig(clientConfig);
375
- });
413
+ for (const clientConfig of clientConfigs) {
414
+ await checkClientConfig(clientConfig);
415
+ }
376
416
  log.info('check client configs done');
377
417
  }
378
418
 
379
419
  async function checkConfig() {
380
420
  log.info('check config');
381
421
  checkMapping();
382
- checkClientConfigs();
422
+ await checkClientConfigs();
383
423
  await checkCustomFormMapping();
384
424
  log.info('check config done');
385
425
  }
@@ -434,7 +474,7 @@ async function getDesignPackage(clientId) {
434
474
  const mapping = getDeprecatedMappingAsCustom(config.requestTypeWorkLog)
435
475
  schemas[config.requestTypeWorkLog] = await getObjectSchema(clientConfig[config.requestTypeWorkLog].fields, mapping, config.forms.workLog)
436
476
  schemas[config.requestTypeWorkLog + '_create_update'] = await getObjectSchema(clientConfig[config.requestTypeWorkLog].fields, mapping, config.forms.workLog, true)
437
- paths = {...paths, ... await getPathDef(config.assocTicketType, config.baseURI + '/{ticketId}/worklogs', config.requestTypeWorkLog, true, true)}
477
+ paths = {...paths, ... await getPathDef(`${config.assocTicketType} Worklog`, config.baseURI + '/{ticketId}/worklogs', config.requestTypeWorkLog, true, true)}
438
478
  paths = {...paths, ... await getAttachmentPathDef(config.assocTicketType, config.baseURI + '/{ticketId}/worklogs')}
439
479
  }
440
480
  if (config.requestTemplate && clientConfig[config.requestTemplate].fields && clientConfig[config.requestTemplate].fields.length > 0) {