@mimik/api-helper 2.0.7 → 2.0.8

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/index.js CHANGED
@@ -44,7 +44,7 @@ import { securityLib } from './lib/securityHandlers.js';
44
44
  * @module api-helper
45
45
  * @example
46
46
  * import apiHelper from '@mimik/api-helper';
47
- * or
47
+ * // or
48
48
  * import { apiSetup, securityLib, getAPIFile, validateSecuritySchemes, extractProperties, setupServerFiles } from '@mimik/api-helper';
49
49
  */
50
50
  const EMPTY = 0;
@@ -59,13 +59,12 @@ const POSTFIX_INDEX = 3;
59
59
  * Implement the security flows for the API.
60
60
  *
61
61
  * @function securityLib
62
- * @category async
62
+ * @category sync
63
63
  * @requires @mimik/swagger-helper
64
64
  * @requires jsonwebtoken
65
65
  * @requires lodash
66
66
  * @param {object} config - Configuration of the service.
67
- * &fulfil {object} The API file itself.
68
- * @throws {Promise} An error is thrown if the initiatilization failed.
67
+ * @return {object} An object containing `SystemSecurity`, `AdminSecurity`, `UserSecurity`, and `ApiKeySecurity` handlers.
69
68
  *
70
69
  * This function is used to setup the following security handlers for the API:
71
70
  * - `SystemSecurity` - used for the system operations, like /system, /onbehalf
@@ -78,33 +77,29 @@ export { securityLib };
78
77
 
79
78
  /**
80
79
  *
81
- * Setup the API to be use for a service
80
+ * Setup the API to be used for a service
82
81
  *
83
82
  * @function apiSetup
84
83
  * @category async
85
84
  * @requires @mimik/response-helper
86
85
  * @requires @mimik/sumologic-winston-logger
87
- * @requires @mimik/swagger-helper
88
- * @requires ajv-formats
89
- * @requires fs
90
- * @requires jsonwebtoken
91
- * @requires lodash
92
- * @param {object} setup - Object containing the apiFilename and the exisiting security schemes in the API definition.
86
+ * @requires lodash.difference
87
+ * @requires openapi-backend
88
+ * @param {object} setup - Object containing the apiFilename and the existing security schemes in the API definition.
93
89
  * @param {object} registeredOperations - List of the operation to register for the API.
94
90
  * @param {object} securityHandlers - List of the security handlers to add for the service.
95
- * @param {object} extraFormats - list of the formats to add for validatng properties.
91
+ * @param {object} extraFormats - list of the formats to add for validating properties.
96
92
  * @param {object} config - Configuration of the service.
97
- * @param {UUID.<string>} correlationId - CorrelationId when logging activites.
98
- * @return {Promise}.
99
- * &fulfil {object} The API file itself.
100
- * @throws {Promise} An error is thrown if the initiatilization failed.
93
+ * @param {UUID.<string>} correlationId - CorrelationId when logging activities.
94
+ * @return {Promise.<object>} The API file itself.
95
+ * @throws {Promise} An error is thrown if the initialization failed.
101
96
  *
102
97
  * The following scheme names are reserved: `SystemSecurity`, `AdminSecurity`, `UserSecurity`, `PeerSecurity`, `ApiKeySecurity`.
103
98
  * The following security schemes can be defaulted: `SystemSecurity`, `AdminSecurity`, `UserSecurity`, `ApiKeySecurity`.
104
99
  * The secOptions in the options property passed when using `init` allows the following operations:
105
100
  * - introduce a customer security scheme, in this case secOptions contains: { newSecurityScheme: {function}newSecurityHandler },
106
101
  * - disable a security scheme that is defined in the swagger API, in this case secOptions contains: { securitySchemeToDisable: { {boolean}notEnabled: true } },
107
- * - overwite an existing security scheme, in this case secOptions contains: { securitySchemeToOverwrite: {function}newSecurityHandler }.
102
+ * - overwrite an existing security scheme, in this case secOptions contains: { securitySchemeToOverwrite: {function}newSecurityHandler }.
108
103
  * If the secOptions is not present either to introduce, disable or overwrite a security scheme that is present in the swagger API file an error is generated.
109
104
  * If the secOptions contains unused security schemes, an error is generated.
110
105
  *
@@ -142,7 +137,7 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
142
137
  }
143
138
  const appliedSecurities = [];
144
139
  const registerDefault = (securitySchemeName, securityHandler) => {
145
- if (existingSecuritySchemes.includes(securitySchemeName) && (!securityHandlers || (securityHandlers && !securityHandlers[securitySchemeName]))) {
140
+ if (existingSecuritySchemes.includes(securitySchemeName) && (!securityHandlers || !securityHandlers[securitySchemeName])) {
146
141
  api.registerSecurityHandler(securitySchemeName, securityHandler);
147
142
  appliedSecurities.push(securitySchemeName);
148
143
  }
@@ -161,7 +156,7 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
161
156
  if (unusedSecuritySchemes.length !== EMPTY) throw getRichError('System', 'unused handlers for security schemes', { unusedSecuritySchemes });
162
157
 
163
158
  remainingSecurities.forEach((securityScheme) => {
164
- if (!securityHandlerNames.includes(securityScheme) && !securityHandlers[securityScheme].notEnabled) {
159
+ if (!securityHandlerNames.includes(securityScheme) && !securityHandlers[securityScheme]?.notEnabled) {
165
160
  throw getRichError('System', 'missing handler for security scheme', { securityScheme });
166
161
  }
167
162
  });
@@ -172,16 +167,72 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
172
167
  });
173
168
  }
174
169
  else if (remainingSecurities.length !== EMPTY) throw getRichError('System', 'missing handlers for security schemes', { missingSecuritySchemes: remainingSecurities });
175
- api.init()
170
+ return api.init()
176
171
  .catch((err) => {
177
172
  throw getRichError('System', 'could not initialize the api', { api }, err);
178
- });
179
- return api;
173
+ })
174
+ .then(() => api);
175
+ };
176
+
177
+ const swaggerOptions = spec => ({
178
+ spec,
179
+ allowMetaPatches: false,
180
+ skipNormalization: true,
181
+ mode: 'strict',
182
+ });
183
+
184
+ const saveResolvedSpec = (apiDefinitionResult, apiFilename, correlationId) => {
185
+ if (apiDefinitionResult.errors.length !== EMPTY) {
186
+ logger.error('errors while resolving definition', { errors: apiDefinitionResult.errors }, correlationId);
187
+ throw getRichError('Parameter', 'errors while resolving definition', { apiFilename, errors: apiDefinitionResult.errors });
188
+ }
189
+ try {
190
+ fs.writeFileSync(apiFilename, JSON.stringify(apiDefinitionResult.spec, null, TAB));
191
+ }
192
+ catch (err) {
193
+ throw getRichError('System', 'file system error', { apiFilename }, err);
194
+ }
195
+ return apiDefinitionResult.spec;
196
+ };
197
+
198
+ const buildProviderRequest = (params, apiInfo, apiFilename) => {
199
+ const provider = apiInfo.provider || BITBUCKET;
200
+
201
+ switch (provider) {
202
+ case SWAGGERHUB: {
203
+ const result = {
204
+ url: `${API_PROVIDER_SWAGGERHUB}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}/${params[API_VERSION_INDEX]}?${RESOLVED}`,
205
+ };
206
+
207
+ if (apiInfo.apiApiKey) result.authorization = apiInfo.apiApiKey;
208
+ return result;
209
+ }
210
+ case BITBUCKET: {
211
+ if (!apiInfo.apiBasicAuth || !apiInfo.apiBasicAuth.username || !apiInfo.apiBasicAuth.password) {
212
+ throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
213
+ }
214
+ if (apiInfo.apiBasicAuth.username === DEFAULT_BITBUCKET_USERNAME || apiInfo.apiBasicAuth.password === DEFAULT_BITBUCKET_PASSWORD) {
215
+ throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
216
+ }
217
+ try {
218
+ return {
219
+ url: `${API_PROVIDER_BITBUCKET}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}${API_SOURCE}/${params[API_VERSION_INDEX]}/${SWAGGER}${EXTENSION_YML}`,
220
+ authorization: `Basic ${Base64.encode(`${apiInfo.apiBasicAuth.username}:${apiInfo.apiBasicAuth.password}`)}`,
221
+ };
222
+ }
223
+ catch (err) {
224
+ throw getRichError('System', 'could not create basicAuth', { apiFilename }, err);
225
+ }
226
+ }
227
+ default: {
228
+ throw getRichError('Parameter', 'invalid API provider', { provider, apiFilename });
229
+ }
230
+ }
180
231
  };
181
232
 
182
233
  /**
183
234
  *
184
- * Gets the API file from swaggerhub and store it in the give PATH location.
235
+ * Gets the API file from swaggerhub and store it in the given PATH location.
185
236
  *
186
237
  * @function getAPIFile
187
238
  * @category async
@@ -192,11 +243,10 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
192
243
  * @requires js-yaml
193
244
  * @requires path
194
245
  * @param {PATH.<string>} apiFilename - Name of the file where the API file will be stored.
195
- * @param {UUID.<string>} correlationId - CorrelationId when logging activites.
246
+ * @param {UUID.<string>} correlationId - CorrelationId when logging activities.
196
247
  * @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiInfo` to access the api file in the api provider.
197
- * @return {Promise}.
198
- * &fulfil {object} The API file itself.
199
- * @throws {Promise} An error is thrown if the apiFilename resolution generates an error or the request to the API provider fails or the file connot be saved.
248
+ * @return {Promise.<object>} The API file itself.
249
+ * @throws {Promise} An error is thrown if the apiFilename resolution generates an error or the request to the API provider fails or the file cannot be saved.
200
250
  *
201
251
  * `apiInfo` options has the following format:
202
252
  * ``` javascript
@@ -206,17 +256,11 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
206
256
  * "username": "username for bitbucket",
207
257
  * "password": "password for bitbucket"
208
258
  * },
209
- * "apiApiKey": "apiKey for access private API on swaggerhub, can be optional if the API is accessible publically"
259
+ * "apiApiKey": "apiKey for access private API on swaggerhub, can be optional if the API is accessible publicly"
210
260
  * }
261
+ * ```
211
262
  */
212
263
  export const getAPIFile = (apiFilename, correlationId, options) => {
213
- const swaggerOptions = spec => ({
214
- spec,
215
- allowMetaPatches: false,
216
- skipNormalization: true,
217
- mode: 'strict',
218
- });
219
-
220
264
  logger.info('getting API definition', correlationId);
221
265
  let apiDefinition;
222
266
 
@@ -238,24 +282,12 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
238
282
  }
239
283
  return SwaggerClient.resolve(swaggerOptions(apiDefinition))
240
284
  .catch((err) => {
241
- throw getRichError('System', 'could not resolve apiDefiniton', { apiFilename }, err);
285
+ throw getRichError('System', 'could not resolve apiDefinition', { apiFilename }, err);
242
286
  })
243
- .then((apiDefinitionResult) => {
244
- if (apiDefinitionResult.errors.length !== EMPTY) {
245
- logger.error('errors while resolving definition', { errors: apiDefinitionResult.errors }, correlationId);
246
- throw getRichError('Parameter', 'errors while resolving definition', { apiFilename, errors: apiDefinitionResult.errors });
247
- }
248
- try {
249
- fs.writeFileSync(apiFilename, JSON.stringify(apiDefinitionResult.spec, null, TAB));
250
- }
251
- catch (err) {
252
- throw getRichError('System', 'file system error', { apiFilename }, err);
253
- }
254
- return apiDefinitionResult.spec;
255
- });
287
+ .then(result => saveResolvedSpec(result, apiFilename, correlationId));
256
288
  }
257
289
  if (!options) {
258
- return Promise.reject(getRichError('Paremater', 'no options', { apiFilename }));
290
+ return Promise.reject(getRichError('Parameter', 'no options', { apiFilename }));
259
291
  }
260
292
  const { apiInfo } = options;
261
293
 
@@ -287,56 +319,33 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
287
319
  catch (err) {
288
320
  return Promise.reject(getRichError('System', 'file system error', { apiDirectory }, err));
289
321
  }
322
+ let providerResult;
323
+
324
+ try {
325
+ providerResult = buildProviderRequest(params, apiInfo, apiFilename);
326
+ }
327
+ catch (err) {
328
+ return Promise.reject(err);
329
+ }
290
330
  const opts = {
291
331
  method: 'GET',
332
+ url: providerResult.url,
292
333
  headers: {
293
334
  'x-correlation-id': correlationId,
294
335
  },
336
+ retry: {
337
+ logLevel: {
338
+ response: 'debug',
339
+ responseDetails: 'type',
340
+ request: 'debug',
341
+ },
342
+ },
295
343
  };
296
- const provider = apiInfo.provider || BITBUCKET;
297
344
 
298
- try {
299
- switch (provider) {
300
- case SWAGGERHUB: {
301
- opts.url = `${API_PROVIDER_SWAGGERHUB}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}/${params[API_VERSION_INDEX]}?${RESOLVED}`;
302
- if (apiInfo.apiApiKey) opts.headers.Authorization = apiInfo.apiApiKey;
303
- break;
304
- }
305
- case BITBUCKET: {
306
- if (!apiInfo.apiBasicAuth || !apiInfo.apiBasicAuth.username || !apiInfo.apiBasicAuth.password) {
307
- throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
308
- }
309
- if (apiInfo.apiBasicAuth.username === DEFAULT_BITBUCKET_USERNAME || apiInfo.apiBasicAuth.password === DEFAULT_BITBUCKET_PASSWORD) {
310
- throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
311
- }
312
- try {
313
- opts.headers.Authorization = `Basic ${Base64.encode(`${apiInfo.apiBasicAuth.username}:${apiInfo.apiBasicAuth.password}`)}`;
314
- }
315
- catch (err) {
316
- throw getRichError('System', 'could not create basicAuth', { apiFilename }, err);
317
- }
318
- opts.url = `${API_PROVIDER_BITBUCKET}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}${API_SOURCE}/${params[API_VERSION_INDEX]}/${SWAGGER}${EXTENSION_YML}`;
319
- break;
320
- }
321
- default: {
322
- throw getRichError('Parameter', 'invalid API provider', { provider, apiFilename });
323
- }
324
- }
325
- }
326
- catch (err) {
327
- return Promise.reject(err);
328
- }
345
+ if (providerResult.authorization) opts.headers.Authorization = providerResult.authorization;
329
346
  if (options.metrics) {
330
- opts.metrics = options.metrics;
331
- opts.metrics.url = opts.url;
347
+ opts.metrics = { ...options.metrics, url: opts.url };
332
348
  }
333
- opts.retry = {
334
- logLevel: {
335
- response: 'debug',
336
- responseDetails: 'type',
337
- request: 'debug',
338
- },
339
- };
340
349
  logger.debug('API file does not exist, retrieving it', { url: opts.url }, correlationId);
341
350
  return rpRetry(opts)
342
351
  .then((result) => {
@@ -349,21 +358,9 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
349
358
  if (err.statusCode) {
350
359
  throw err;
351
360
  }
352
- throw getRichError('System', 'could not resolve apiDefiniton', { apiFilename }, err);
361
+ throw getRichError('System', 'could not resolve apiDefinition', { apiFilename }, err);
353
362
  })
354
- .then((apiDefinitionResult) => {
355
- if (apiDefinitionResult.errors.length !== EMPTY) {
356
- logger.error('errors while resolving definition', { errors: apiDefinitionResult.errors }, correlationId);
357
- throw getRichError('Parameter', 'errors while resolving definition', { apiFilename, errors: apiDefinitionResult.errors });
358
- }
359
- try {
360
- fs.writeFileSync(apiFilename, JSON.stringify(apiDefinitionResult.spec, null, TAB));
361
- }
362
- catch (err) {
363
- throw getRichError('System', 'file system error', { apiFilename }, err);
364
- }
365
- return apiDefinitionResult.spec;
366
- });
363
+ .then(result => saveResolvedSpec(result, apiFilename, correlationId));
367
364
  };
368
365
 
369
366
  /**
@@ -375,9 +372,9 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
375
372
  * @requires @mimik/sumologic-winston-logger
376
373
  * @requires @mimik/response-helper
377
374
  * @param {object} apiDefinition - JSON object containing the API definition.
378
- * @param {UUID.<string>} correlationId - CorrelationId when logging activites.
379
- * @return An array of the known securitySchemes that are in the API definition.
380
- * @throws An error is thrown for the first validation fails.
375
+ * @param {UUID.<string>} correlationId - CorrelationId when logging activities.
376
+ * @return {Array.<string>} An array of the known securitySchemes that are in the API definition.
377
+ * @throws An error is thrown if a validation fails.
381
378
  */
382
379
  export const validateSecuritySchemes = (apiDefinition, correlationId) => {
383
380
  const existingSecuritySchemes = [];
@@ -397,7 +394,7 @@ export const validateSecuritySchemes = (apiDefinition, correlationId) => {
397
394
 
398
395
  /**
399
396
  *
400
- * Extracts the properties from API definiton and creates a file binding the handler with the controller operations.
397
+ * Extracts the properties from API definition and creates a file binding the handler with the controller operations.
401
398
  *
402
399
  * @function extractProperties
403
400
  * @category sync
@@ -407,9 +404,9 @@ export const validateSecuritySchemes = (apiDefinition, correlationId) => {
407
404
  * @param {object} apiDefinition - JSON object containing the API definition.
408
405
  * @param {PATH.<string>} controllersDirectory - Directory to find the controller files.
409
406
  * @param {PATH.<string>} buildDirectory - Directory where the register file will be stored.
410
- * @param {UUID.<string>} correlationId - CorrelationId when logging activites.
411
- * @return null
412
- * @throws An error is thrown for many reasons, like operationId does not exist in controllers, controller dies not exist...
407
+ * @param {UUID.<string>} correlationId - CorrelationId when logging activities.
408
+ * @return {void}
409
+ * @throws An error is thrown for many reasons, like operationId does not exist in controllers, controller does not exist...
413
410
  */
414
411
  export const extractProperties = (apiDefinition, controllersDirectory, buildDirectory, correlationId) => {
415
412
  const result = {};
@@ -493,11 +490,10 @@ export const extractProperties = (apiDefinition, controllersDirectory, buildDire
493
490
  * @param {PATH.<string>} apiFilename - Name of the file where the API file will be stored.
494
491
  * @param {PATH.<string>} controllersDirectory - Directory to find the controller files.
495
492
  * @param {PATH.<string>} buildDirectory - Directory where the register file will be stored.
496
- * @param {UUID.<string>} correlationId - CorrelationId when logging activites.
497
- * @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey`` to access private API.
498
- * @return {Promise}.
499
- * &fulfil {object} The API file, the API filename, the existing known security schemes and the defined security schemes.
500
- * @throws {Promise} An error is thrown for many reasons assocated with getAPIFile or validateSecuritySchemes or extractProperties.
493
+ * @param {UUID.<string>} correlationId - CorrelationId when logging activities.
494
+ * @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey` to access private API.
495
+ * @return {Promise.<object>} The API file, the API filename, the existing known security schemes and the defined security schemes.
496
+ * @throws {Promise} An error is thrown for many reasons associated with getAPIFile or validateSecuritySchemes or extractProperties.
501
497
  */
502
498
  export const setupServerFiles = (apiFilename, controllersDirectory, buildDirectory, correlationId, options) => getAPIFile(apiFilename, correlationId, options)
503
499
  .then((apiDefinition) => {
package/lib/ajvHelpers.js CHANGED
@@ -14,7 +14,7 @@ const ajvFormats = origFormats => (ajv) => {
14
14
  validate: /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/u,
15
15
  },
16
16
  };
17
- const libFormats = DEFAULT_FORMATS;
17
+ const libFormats = [...DEFAULT_FORMATS];
18
18
  let formats = origFormats;
19
19
 
20
20
  if (!formats) formats = {};
@@ -34,9 +34,8 @@ const unauthorizedHandler = (con, req, res) => {
34
34
  let error = new Error('Unauthorized');
35
35
 
36
36
  error.statusCode = UNAUTHORIZED_ERROR;
37
- const schemes = Object.keys(con.security);
37
+ const schemes = Object.keys(con.security).filter(key => key !== 'authorized');
38
38
 
39
- delete schemes.authorized;
40
39
  schemes.forEach((scheme) => {
41
40
  const { error: schemeError } = con.security[scheme] || {};
42
41
  if (schemeError) {
@@ -49,7 +48,7 @@ const unauthorizedHandler = (con, req, res) => {
49
48
  const notImplemented = (con, req, res) => {
50
49
  const { method } = req;
51
50
  const path = req.url;
52
- const error = new Error(`${req.method} ${path} defined in Swagger specification, but no implemented`);
51
+ const error = new Error(`${method} ${path} defined in Swagger specification, but not implemented`);
53
52
 
54
53
  error.statusCode = NOT_IMPLEMENTED_ERROR;
55
54
  error.info = {
@@ -13,7 +13,7 @@ const validateOauth2 = (securitySchemes, securityType, flow) => {
13
13
  if (security.type !== OAUTH2) {
14
14
  throw getRichError('System', `auth type is not ${OAUTH2}`, { securityType, receivedAuth: security.type, expectedAuth: OAUTH2 });
15
15
  }
16
- if (!security.flows[flow]) {
16
+ if (!security.flows || !security.flows[flow]) {
17
17
  throw getRichError('System', 'no flow type available', { securityType, flow });
18
18
  }
19
19
  return securityType;
@@ -93,7 +93,7 @@ const checkHeaders = (headers) => {
93
93
 
94
94
  const checkScopes = (tokenScopes, defScopes, definition) => {
95
95
  if (!tokenScopes) {
96
- throw new Error('no scope in authorization token');
96
+ throw getError('no scope in authorization token', UNAUTHORIZED_ERROR);
97
97
  }
98
98
  let claims = [];
99
99
  let onBehalf = false;
package/package.json CHANGED
@@ -1,23 +1,17 @@
1
1
  {
2
2
  "name": "@mimik/api-helper",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "helper for openAPI backend and mimik service",
5
5
  "main": "./index.js",
6
- "type": "module",
6
+ "type": "module",
7
7
  "scripts": {
8
8
  "lint": "eslint . --no-error-on-unmatched-pattern",
9
9
  "docs": "jsdoc2md index.js > README.md",
10
- "test": "echo \"Error: no test specified\" && exit 0",
11
- "test-ci": "echo \"Error: no test specified\" && exit 0",
10
+ "test": "mocha test/ --recursive",
11
+ "test-ci": "c8 --reporter=lcov --reporter=text npm test",
12
12
  "prepublishOnly": "npm run docs && npm run lint && npm run test-ci",
13
13
  "commit-ready": "npm run docs && npm run lint && npm run test-ci"
14
14
  },
15
- "husky": {
16
- "hooks": {
17
- "pre-commit": "npm run commit-ready",
18
- "pre-push": "npm run test"
19
- }
20
- },
21
15
  "keywords": [
22
16
  "mimik",
23
17
  "microservice",
@@ -25,33 +19,40 @@
25
19
  ],
26
20
  "author": "mimik technology inc <support@mimik.com> (https://developer.mimik.com/)",
27
21
  "license": "MIT",
22
+ "engines": {
23
+ "node": ">=24"
24
+ },
28
25
  "repository": {
29
26
  "type": "git",
30
27
  "url": "https://bitbucket.org/mimiktech/api-helper"
31
28
  },
32
29
  "dependencies": {
33
- "@mimik/request-helper":"^2.0.2",
34
- "@mimik/request-retry": "^4.0.3",
35
- "@mimik/response-helper": "^4.0.4",
36
- "@mimik/sumologic-winston-logger": "^2.0.3",
37
- "@mimik/swagger-helper": "^5.0.2",
30
+ "@mimik/request-retry": "^4.0.9",
31
+ "@mimik/response-helper": "^4.0.10",
32
+ "@mimik/sumologic-winston-logger": "^2.1.13",
33
+ "@mimik/swagger-helper": "^5.0.3",
38
34
  "ajv-formats": "3.0.1",
39
- "js-base64": "3.7.7",
40
- "js-yaml":"4.1.0",
41
- "jsonwebtoken": "9.0.2",
42
- "lodash.compact": "3.0.1",
35
+ "js-base64": "3.7.8",
36
+ "js-yaml": "4.1.1",
37
+ "jsonwebtoken": "9.0.3",
38
+ "lodash.compact": "3.0.1",
43
39
  "lodash.difference": "4.5.0",
44
40
  "lodash.intersection": "4.4.0",
45
- "openapi-backend": "5.13.0",
46
- "swagger-client": "3.35.6"
41
+ "openapi-backend": "5.16.1",
42
+ "swagger-client": "3.36.2"
47
43
  },
48
44
  "devDependencies": {
49
45
  "@eslint/js": "9.32.0",
50
46
  "@mimik/eslint-plugin-document-env": "^2.0.8",
51
- "@stylistic/eslint-plugin": "5.2.2",
47
+ "@stylistic/eslint-plugin": "5.9.0",
48
+ "c8": "10.1.3",
49
+ "chai": "6.2.2",
52
50
  "eslint": "9.32.0",
53
51
  "eslint-plugin-import": "2.32.0",
52
+ "esmock": "2.7.3",
53
+ "globals": "17.3.0",
54
54
  "husky": "9.1.7",
55
- "jsdoc-to-markdown": "9.1.2"
55
+ "jsdoc-to-markdown": "9.1.3",
56
+ "mocha": "11.7.5"
56
57
  }
57
58
  }
@@ -0,0 +1,159 @@
1
+ import esmock from 'esmock';
2
+ import { expect } from 'chai';
3
+
4
+ let ajvFormats;
5
+ let addedLibFormats;
6
+ let addedCustomFormats;
7
+
8
+ describe('ajvHelpers', () => {
9
+ before(async () => {
10
+ const mod = await esmock('../lib/ajvHelpers.js', {
11
+ 'ajv-formats': {
12
+ default: (ajv, formats) => {
13
+ addedLibFormats = formats;
14
+ },
15
+ },
16
+ });
17
+
18
+ ({ ajvFormats } = mod);
19
+ });
20
+
21
+ beforeEach(() => {
22
+ addedLibFormats = null;
23
+ addedCustomFormats = {};
24
+ });
25
+
26
+ const createMockAjv = () => ({
27
+ addFormat: (name, format) => {
28
+ addedCustomFormats[name] = format;
29
+ },
30
+ });
31
+
32
+ describe('with no extra formats', () => {
33
+ it('should add default library formats', () => {
34
+ const ajv = createMockAjv();
35
+ const configurer = ajvFormats();
36
+
37
+ configurer(ajv);
38
+ expect(addedLibFormats).to.deep.equal(['date', 'time', 'date-time', 'byte', 'uuid', 'uri', 'email', 'ipv4', 'ipv6']);
39
+ });
40
+
41
+ it('should add built-in semver and ip custom formats', () => {
42
+ const ajv = createMockAjv();
43
+ const configurer = ajvFormats();
44
+
45
+ configurer(ajv);
46
+ expect(addedCustomFormats).to.have.property('semver');
47
+ expect(addedCustomFormats.semver.type).to.equal('string');
48
+ expect(addedCustomFormats).to.have.property('ip');
49
+ expect(addedCustomFormats.ip.type).to.equal('string');
50
+ });
51
+
52
+ it('should return the ajv instance', () => {
53
+ const ajv = createMockAjv();
54
+ const configurer = ajvFormats();
55
+ const result = configurer(ajv);
56
+
57
+ expect(result).to.equal(ajv);
58
+ });
59
+ });
60
+
61
+ describe('with null extra formats', () => {
62
+ it('should treat null as no extra formats', () => {
63
+ const ajv = createMockAjv();
64
+ const configurer = ajvFormats(null);
65
+
66
+ configurer(ajv);
67
+ expect(addedLibFormats).to.deep.equal(['date', 'time', 'date-time', 'byte', 'uuid', 'uri', 'email', 'ipv4', 'ipv6']);
68
+ });
69
+ });
70
+
71
+ describe('with extra library formats (string-only)', () => {
72
+ it('should append format names to the library formats list', () => {
73
+ const ajv = createMockAjv();
74
+ const configurer = ajvFormats({ hostname: {} });
75
+
76
+ configurer(ajv);
77
+ expect(addedLibFormats).to.include('hostname');
78
+ expect(addedLibFormats).to.have.lengthOf(10);
79
+ });
80
+ });
81
+
82
+ describe('with extra custom formats (with type)', () => {
83
+ it('should add custom format via ajv.addFormat', () => {
84
+ const customFormat = { type: 'string', validate: /^[A-Z]+$/u };
85
+ const ajv = createMockAjv();
86
+ const configurer = ajvFormats({ uppercase: customFormat });
87
+
88
+ configurer(ajv);
89
+ expect(addedCustomFormats).to.have.property('uppercase');
90
+ expect(addedCustomFormats.uppercase).to.equal(customFormat);
91
+ expect(addedLibFormats).to.not.include('uppercase');
92
+ });
93
+ });
94
+
95
+ describe('semver validation', () => {
96
+ it('should accept valid semver strings', () => {
97
+ const ajv = createMockAjv();
98
+
99
+ ajvFormats()(ajv);
100
+ const { validate } = addedCustomFormats.semver;
101
+
102
+ expect(validate.test('1.0.0')).to.equal(true);
103
+ expect(validate.test('0.1.0')).to.equal(true);
104
+ expect(validate.test('1.2.3-alpha.1')).to.equal(true);
105
+ expect(validate.test('1.2.3+build.123')).to.equal(true);
106
+ expect(validate.test('1.2.3-beta+build')).to.equal(true);
107
+ });
108
+
109
+ it('should reject invalid semver strings', () => {
110
+ const ajv = createMockAjv();
111
+
112
+ ajvFormats()(ajv);
113
+ const { validate } = addedCustomFormats.semver;
114
+
115
+ expect(validate.test('1.0')).to.equal(false);
116
+ expect(validate.test('abc')).to.equal(false);
117
+ expect(validate.test('1.0.0.0')).to.equal(false);
118
+ });
119
+ });
120
+
121
+ describe('ip validation', () => {
122
+ it('should accept valid IPv4 addresses', () => {
123
+ const ajv = createMockAjv();
124
+
125
+ ajvFormats()(ajv);
126
+ const { validate } = addedCustomFormats.ip;
127
+
128
+ expect(validate.test('192.168.1.1')).to.equal(true);
129
+ expect(validate.test('10.0.0.1')).to.equal(true);
130
+ expect(validate.test('255.255.255.255')).to.equal(true);
131
+ });
132
+
133
+ it('should accept valid IPv6 addresses', () => {
134
+ const ajv = createMockAjv();
135
+
136
+ ajvFormats()(ajv);
137
+ const { validate } = addedCustomFormats.ip;
138
+
139
+ expect(validate.test('::1')).to.equal(true);
140
+ expect(validate.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).to.equal(true);
141
+ });
142
+ });
143
+
144
+ describe('DEFAULT_FORMATS isolation', () => {
145
+ it('should not mutate DEFAULT_FORMATS between calls', () => {
146
+ const ajv1 = createMockAjv();
147
+ const ajv2 = createMockAjv();
148
+
149
+ ajvFormats({ extra1: {} })(ajv1);
150
+ const firstCallLength = addedLibFormats.length;
151
+
152
+ ajvFormats()(ajv2);
153
+ const secondCallLength = addedLibFormats.length;
154
+
155
+ expect(secondCallLength).to.equal(9);
156
+ expect(firstCallLength).to.equal(10);
157
+ });
158
+ });
159
+ });