diff --git a/.eslintrc b/.eslintrc index 8e474d09..f1952004 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,16 +13,18 @@ { "files": ["*.ts"], "parserOptions": { - "project": ["./tsconfig.json"] + "project": ["./tsconfig.dev.json"] } } ], "plugins": ["@typescript-eslint", "simple-import-sort", "prettier"], "rules": { - "no-await-in-loop": "off", "no-shadow": "off", - "import/no-extraneous-dependencies": "off", "@typescript-eslint/no-shadow": "off", + "require-await": "off", + "@typescript-eslint/require-await": "error", + "import/no-extraneous-dependencies": "off", + "import/prefer-default-export": "off", "simple-import-sort/imports": [ "error", { @@ -36,9 +38,7 @@ ] } ], - "simple-import-sort/exports": "error", - "import/prefer-default-export": "off", - "consistent-return": "off" + "simple-import-sort/exports": "error" }, - "ignorePatterns": ["jest.config.js", "mock-server.config.js"] + "ignorePatterns": ["jest.config.js"] } diff --git a/README.md b/README.md index 1d4190dd..ec415164 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ $ yarn add mock-config-server --dev ## Features - **TypeScript support out of the box** - full typed package -- **Full Rest Api support** - using simple configs of a certain format, you can easily simulate the operation of servers +- **Full Rest Api support** - using simple configs of a certain format, you can easily simulate rest operation of servers +- **GraphQL support** - using simple configs of a certain format, you can easily simulate graphlql operation of servers - **CORS setup** - turn on and off CORS, fully customizable when CORS is turned on - **Support for any kind of static** - server can return any type of static file if needed. Images, HTML, CSS, JSON, etc @@ -36,15 +37,18 @@ $ yarn add mock-config-server --dev Create a `mock-server.config.js` file with server configuration ```javascript -/** @type {import('mock-config-server').Mock.ServerConfig} */ +/** @type {import('mock-config-server').MockServerConfig} */ const mockServerConfig = { - configs: [ - { - path: '/user', - method: 'get', - routes: [{ data: { emoji: '🦁', name: 'Nursultan' } }] - } - ] + rest: { + baseUrl: '/api', + configs: [ + { + path: '/user', + method: 'get', + routes: [{ data: { emoji: '🦁', name: 'Nursultan' } }] + } + ] + } }; export default mockServerConfig; @@ -60,7 +64,12 @@ $ npx mock-config-server ## 🎭 Parameters for mock-server.config.(js|ts) -- `configs` {Array} configs for mock requests, [read](#configs) +- `rest?` Rest configs for mock requests + - `baseUrl?` {string} part of the url that will be substituted at the beginning of rest request url (default: `'/'`) + - `configs` {Array} configs for mock requests, [read](#configs) +- `graphql?` GraphQL configs for mock requests + - `baseUrl?` {string} part of the url that will be substituted at the beginning of graphql request url (default: `'/'`) + - `configs` {Array} configs for mock requests, [read](#configs) - `staticPath?` {StaticPath} entity for working with static files, [read](#static-path) - `interceptors?` {Interceptors} functions to change request or response parameters, [read](#interceptors) - `cors?` {Cors} CORS settings object (default: `CORS is turn off`), [read](#cors) @@ -69,57 +78,102 @@ $ npx mock-config-server ### Configs -Configs are the fundamental part of the mock server. These configs are easy to fill and maintain. Config entities is an object with which you can emulate various application behaviors. You can specify `headers` | `query` | `params` | `body` to define what contract data you need to get. Using this mechanism, you can easily simulate the operation of the server and emulate various cases +Configs are the fundamental part of the mock server. These configs are easy to fill and maintain. Config entities is an object with which you can emulate various application behaviors. You can specify `headers` | `query` | `params` | `body` for Rest request or `headers` | `query` | `variables` for GraphQL request to define what contract data you need to get. Using this mechanism, you can easily simulate the operation of the server and emulate various cases -##### request config +##### Rest request config - `path` {string | RegExp} request path - `method` {GET | POST | DELETE | PUT | PATCH} rest api method -- `routes` {RouteConfig} request routes +- `routes` {RestRouteConfig[]} request routes + - `data` {any} mock data of request + - `entities?` Object object that helps in data retrieval + - `interceptors?` {Interceptors} functions to change request or response parameters, [read](#interceptors) - `interceptors?` {Interceptors} functions to change request or response parameters, [read](#interceptors) -##### route config +##### GraphQL request config -- `data` {any} mock data of request -- `entities?` Object object that helps in data retrieval +- `operationType` {query | mutation} graphql operation type +- `operationName` {string} graphql operation name +- `routes` {GraphQLRouteConfig[]} request routes + - `data` {any} mock data of request + - `entities?` Object object that helps in data retrieval + - `interceptors?` {Interceptors} functions to change request or response parameters, [read](#interceptors) - `interceptors?` {Interceptors} functions to change request or response parameters, [read](#interceptors) -##### entities +##### Rest example + +```javascript +/** @type {import('mock-config-server').MockServerConfig} */ +const mockServerConfig = { + rest: { + baseUrl: '/api', + configs: [ + { + path: '/user', + method: 'get', + routes: [ + { + entities: { + headers: { 'name-header': 'Nursultan' } + }, + data: { emoji: '🦁', name: 'Nursultan' } + }, + { + entities: { + headers: { 'name-header': 'Dmitriy' } + }, + data: { emoji: '☄', name: 'Dmitriy' } + } + ] + } + ] + } +}; + +module.exports = mockServerConfig; +``` -```typescript -interface Entities { - headers?: { [string]: string | number }; - query?: { [string]: string | number }; - params?: { [string]: string | number }; - body?: any; -} +Now you can make a request with an additional header and get the desired result + +```javascript +fetch('http://localhost:31299/api/user', { + headers: { + 'name-header': 'Nursultan', + 'Content-Type': 'application/json' + } +}) + .then((response) => response.json()) + .then((data) => console.log(data)); // { emoji: '🦁', name: 'Nursultan' } ``` -##### Example +##### GraphQL example ```javascript /** @type {import('mock-config-server').MockServerConfig} */ const mockServerConfig = { - configs: [ - { - path: '/user', - method: 'get', - routes: [ - { - entities: { - headers: { 'name-header': 'Nursultan' } - }, - data: { emoji: '🦁', name: 'Nursultan' } - }, - { - entities: { - headers: { 'name-header': 'Dmitriy' } + graphql: { + baseUrl: '/graphql', + configs: [ + { + operationType: 'query', + operationName: 'GetUser', + routes: [ + { + entities: { + headers: { 'name-header': 'Nursultan' } + }, + data: { emoji: '🦁', name: 'Nursultan' } }, - data: { emoji: '☄', name: 'Dmitriy' } - } - ] - } - ] + { + entities: { + headers: { 'name-header': 'Dmitriy' } + }, + data: { emoji: '☄', name: 'Dmitriy' } + } + ] + } + ] + } }; module.exports = mockServerConfig; @@ -128,8 +182,17 @@ module.exports = mockServerConfig; Now you can make a request with an additional header and get the desired result ```javascript -fetch('http://localhost:31299/user', { - headers: { 'name-header': 'Nursultan' } +const body = JSON.stringify({ + query: 'query GetUser { name }' +}); + +fetch('http://localhost:31299/graphql', { + method: 'POST', + headers: { + 'name-header': 'Nursultan', + 'Content-Type': 'application/json' + }, + body }) .then((response) => response.json()) .then((data) => console.log(data)); // { emoji: '🦁', name: 'Nursultan' } @@ -151,7 +214,8 @@ Object with settings for [CORS](https://developer.mozilla.org/en-US/docs/Web/HTT - `origin` {string | RegExp | Array | Function | Promise } available origins from which requests can be made - `methods?` {Array} available methods (default: `*`) -- `headers?` {Array} available methods (default: `*`) +- `allowedHeaders?` {Array} allowed headers (default: `*`) +- `exposedHeaders?` {Array} exposed headers (default: `*`) - `credentials?` {boolean} param tells browsers whether to expose the response to the frontend JavaScript code (default: `true`) - `maxAge?` {number} how long the results can be cached (default: `3600`) diff --git a/bin/mock-config-server.ts b/bin/mock-config-server.ts index 7905fb55..af7e79a4 100644 --- a/bin/mock-config-server.ts +++ b/bin/mock-config-server.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import { startMockServer } from '../src'; +import { validateMockServerConfig } from './validateMockServerConfig/validateMockServerConfig'; import { resolveExportsFromSourceCode } from './resolveExportsFromSourceCode'; const start = async () => { @@ -40,9 +41,10 @@ const start = async () => { throw new Error('Cannot handle exports of mock-server.config.(ts|js)'); } + validateMockServerConfig(mockServerConfigExports.default); startMockServer(mockServerConfigExports.default); - } catch (e: any) { - console.error(e.message); + } catch (error: any) { + console.error(error.message); } }; diff --git a/bin/validateMockServerConfig/validateBaseUrl/validateBaseUrl.test.ts b/bin/validateMockServerConfig/validateBaseUrl/validateBaseUrl.test.ts new file mode 100644 index 00000000..4e4fdc99 --- /dev/null +++ b/bin/validateMockServerConfig/validateBaseUrl/validateBaseUrl.test.ts @@ -0,0 +1,15 @@ +import { validateBaseUrl } from './validateBaseUrl'; + +describe('validateBaseUrl', () => { + test('Should correctly handle baseUrl only with correct type', () => { + const correctBaseUrls = ['/stringWithLeadingSlash', undefined]; + correctBaseUrls.forEach((correctBaseUrl) => { + expect(() => validateBaseUrl(correctBaseUrl)).not.toThrow(Error); + }); + + const incorrectBaseUrls = ['stringWithoutLeadingSlash', true, 3000, null, {}, [], () => {}]; + incorrectBaseUrls.forEach((incorrectBaseUrl) => { + expect(() => validateBaseUrl(incorrectBaseUrl)).toThrow(new Error('baseUrl')); + }); + }); +}); diff --git a/bin/validateMockServerConfig/validateBaseUrl/validateBaseUrl.ts b/bin/validateMockServerConfig/validateBaseUrl/validateBaseUrl.ts new file mode 100644 index 00000000..7fad190f --- /dev/null +++ b/bin/validateMockServerConfig/validateBaseUrl/validateBaseUrl.ts @@ -0,0 +1,8 @@ +export const validateBaseUrl = (baseUrl: unknown) => { + if (typeof baseUrl !== 'string' && typeof baseUrl !== 'undefined') { + throw new Error('baseUrl'); + } + if (typeof baseUrl === 'string' && !baseUrl.startsWith('/')) { + throw new Error('baseUrl'); + } +}; diff --git a/bin/validateMockServerConfig/validateCors/validateCors.test.ts b/bin/validateMockServerConfig/validateCors/validateCors.test.ts new file mode 100644 index 00000000..e92c427b --- /dev/null +++ b/bin/validateMockServerConfig/validateCors/validateCors.test.ts @@ -0,0 +1,140 @@ +import { validateCors } from './validateCors'; + +describe('validateCors', () => { + test('Should correctly handle cors only with correct type', () => { + const correctCorsValues = [{ origin: 'string' }, undefined]; + correctCorsValues.forEach((correctCorsValue) => { + expect(() => validateCors(correctCorsValue)).not.toThrow(Error); + }); + + const incorrectCorsValues = ['string', true, 3000, null, [], () => {}]; + incorrectCorsValues.forEach((incorrectCorsValue) => { + expect(() => validateCors(incorrectCorsValue)).toThrow(new Error('cors')); + }); + }); + + test('Should correctly handle cors.origin only with correct type', () => { + const correctOrigins = ['string', /string/, ['string', /string/]]; + correctOrigins.forEach((correctOrigin) => { + expect(() => validateCors({ origin: correctOrigin })).not.toThrow(Error); + }); + + const incorrectOrigins = [true, 3000, null, undefined, {}]; + incorrectOrigins.forEach((incorrectOrigin) => { + expect(() => validateCors({ origin: incorrectOrigin })).toThrow(new Error('cors.origin')); + }); + + const incorrectArrayOrigins = [true, 3000, null, undefined, {}, [], () => {}]; + incorrectArrayOrigins.forEach((incorrectArrayOrigin) => { + expect(() => validateCors({ origin: [incorrectArrayOrigin] })).toThrow('cors.origin[0]'); + }); + }); + + test('Should correctly handle cors.methods only with correct type', () => { + const correctMethodsValues = [['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], [], undefined]; + correctMethodsValues.forEach((correctMethodsValue) => { + expect(() => + validateCors({ + origin: 'string', + methods: correctMethodsValue + }) + ).not.toThrow(Error); + }); + + const incorrectMethodsValues = ['string', true, 3000, null, {}, () => {}]; + incorrectMethodsValues.forEach((incorrectMethodsValue) => { + expect(() => + validateCors({ + origin: 'string', + methods: incorrectMethodsValue + }) + ).toThrow(new Error('cors.methods')); + }); + + const incorrectArrayMethodsValues = ['string', true, 3000, null, undefined, {}, [], () => {}]; + incorrectArrayMethodsValues.forEach((incorrectArrayMethodsValue) => { + expect(() => + validateCors({ + origin: 'string', + methods: [incorrectArrayMethodsValue] + }) + ).toThrow(new Error('cors.methods[0]')); + }); + }); + + test('Should correctly handle cors.headers only with correct type', () => { + const correctHeadersValues = [['string'], [], undefined]; + correctHeadersValues.forEach((correctHeadersValue) => { + expect(() => + validateCors({ + origin: 'string', + headers: correctHeadersValue + }) + ).not.toThrow(Error); + }); + + const incorrectHeadersValues = ['string', true, 3000, null, {}, () => {}]; + incorrectHeadersValues.forEach((incorrectHeadersValue) => { + expect(() => + validateCors({ + origin: 'string', + headers: incorrectHeadersValue + }) + ).toThrow(new Error('cors.headers')); + }); + + const incorrectArrayHeadersValues = [true, 3000, null, undefined, {}, [], () => {}]; + incorrectArrayHeadersValues.forEach((incorrectArrayHeadersValue) => { + expect(() => + validateCors({ + origin: 'string', + headers: [incorrectArrayHeadersValue] + }) + ).toThrow(new Error('cors.headers[0]')); + }); + }); + + test('Should correctly handle cors.credentials only with correct type', () => { + const correctCredentialsValues = [true, false, undefined]; + correctCredentialsValues.forEach((correctCredentialsValue) => { + expect(() => + validateCors({ + origin: 'string', + credentials: correctCredentialsValue + }) + ).not.toThrow(Error); + }); + + const incorrectCredentialsValues = ['string', 3000, null, {}, [], () => {}]; + incorrectCredentialsValues.forEach((incorrectCredentialsValue) => { + expect(() => + validateCors({ + origin: 'string', + credentials: incorrectCredentialsValue + }) + ).toThrow(new Error('cors.credentials')); + }); + }); + + test('Should correctly handle cors.maxAge only with correct type', () => { + const correctMaxAgeValues = [3000, undefined]; + correctMaxAgeValues.forEach((correctMaxAgeValue) => { + expect(() => + validateCors({ + origin: 'string', + maxAge: correctMaxAgeValue + }) + ).not.toThrow(Error); + }); + + const incorrectMaxAgeValues = ['string', true, null, {}, [], () => {}]; + incorrectMaxAgeValues.forEach((incorrectMaxAgeValue) => { + expect(() => + validateCors({ + origin: 'string', + maxAge: incorrectMaxAgeValue + }) + ).toThrow(new Error('cors.maxAge')); + }); + }); +}); diff --git a/bin/validateMockServerConfig/validateCors/validateCors.ts b/bin/validateMockServerConfig/validateCors/validateCors.ts new file mode 100644 index 00000000..8192b257 --- /dev/null +++ b/bin/validateMockServerConfig/validateCors/validateCors.ts @@ -0,0 +1,88 @@ +import { isPlainObject } from '../../../src/utils/helpers'; + +const validateOrigin = (origin: unknown) => { + const isOriginArray = Array.isArray(origin); + if (isOriginArray) { + origin.forEach((originElement, index) => { + const isOriginElementStringOrRegExp = + typeof originElement === 'string' || originElement instanceof RegExp; + if (!isOriginElementStringOrRegExp) { + throw new Error(`origin[${index}]`); + } + }); + return; + } + + const isOriginStringOrRegexp = typeof origin === 'string' || origin instanceof RegExp; + const isOriginFunction = typeof origin === 'function'; + if (!isOriginStringOrRegexp && !isOriginFunction) { + throw new Error('origin'); + } +}; + +const validateMethods = (methods: unknown) => { + const isMethodsArray = Array.isArray(methods); + if (isMethodsArray) { + const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; + methods.forEach((method, index) => { + // ✅ important: + // compare without 'toUpperCase' because 'Access-Control-Allow-Methods' value is case-sensitive + if (!allowedMethods.includes(method)) { + throw new Error(`methods[${index}]`); + } + }); + return; + } + + if (typeof methods !== 'undefined') { + throw new Error('methods'); + } +}; + +const validateHeaders = (headers: unknown) => { + const isHeadersArray = Array.isArray(headers); + if (isHeadersArray) { + headers.forEach((header, index) => { + if (typeof header !== 'string') { + throw new Error(`headers[${index}]`); + } + }); + return; + } + + if (typeof headers !== 'undefined') { + throw new Error('headers'); + } +}; + +const validateCredentials = (credentials: unknown) => { + if (typeof credentials !== 'boolean' && typeof credentials !== 'undefined') { + throw new Error('credentials'); + } +}; + +const validateMaxAge = (maxAge: unknown) => { + if (typeof maxAge !== 'number' && typeof maxAge !== 'undefined') { + throw new Error('maxAge'); + } +}; + +export const validateCors = (cors: unknown) => { + const isCorsObject = isPlainObject(cors); + if (isCorsObject) { + try { + validateOrigin(cors.origin); + validateMethods(cors.methods); + validateHeaders(cors.headers); + validateCredentials(cors.credentials); + validateMaxAge(cors.maxAge); + } catch (error: any) { + throw new Error(`cors.${error.message}`); + } + return; + } + + if (typeof cors !== 'undefined') { + throw new Error('cors'); + } +}; diff --git a/bin/validateMockServerConfig/validateGraphqlConfig/validateGraphqlConfig.test.ts b/bin/validateMockServerConfig/validateGraphqlConfig/validateGraphqlConfig.test.ts new file mode 100644 index 00000000..c481faa7 --- /dev/null +++ b/bin/validateMockServerConfig/validateGraphqlConfig/validateGraphqlConfig.test.ts @@ -0,0 +1,82 @@ +import { validateGraphqlConfig } from './validateGraphqlConfig'; + +describe('validateGraphqlConfig', () => { + test('Should correctly handle graphql config only with correct type', () => { + const correctGraphqlConfigs = [ + { configs: [{ operationType: 'query', operationName: 'user', routes: [] }] }, + undefined + ]; + correctGraphqlConfigs.forEach((correctGraphqlConfig) => { + expect(() => validateGraphqlConfig(correctGraphqlConfig)).not.toThrow(Error); + }); + + const incorrectGraphqlConfigs = ['string', true, 3000, null, [], () => {}]; + incorrectGraphqlConfigs.forEach((incorrectGraphqlConfig) => { + expect(() => validateGraphqlConfig(incorrectGraphqlConfig)).toThrow(new Error('graphql')); + }); + }); + + test('Should correctly handle operation type only with correct type', () => { + const correctOperationTypeValues = ['query', 'mutation']; + correctOperationTypeValues.forEach((correctOperationTypeValue) => { + expect(() => + validateGraphqlConfig({ + configs: [ + { + operationType: correctOperationTypeValue, + operationName: 'user', + routes: [] + } + ] + }) + ).not.toThrow(Error); + }); + + const incorrectOperationTypeValues = ['string', true, 3000, null, undefined, {}, [], () => {}]; + incorrectOperationTypeValues.forEach((incorrectOperationTypeValue) => { + expect(() => + validateGraphqlConfig({ + configs: [ + { + operationType: incorrectOperationTypeValue, + operationName: 'user', + routes: [] + } + ] + }) + ).toThrow('graphql.configs[0].operationType'); + }); + }); + + test('Should correctly handle operation name only with correct type', () => { + const correctOperationNameValues = ['user', /user/]; + correctOperationNameValues.forEach((correctOperationNameValue) => { + expect(() => + validateGraphqlConfig({ + configs: [ + { + operationType: 'query', + operationName: correctOperationNameValue, + routes: [] + } + ] + }) + ).not.toThrow(Error); + }); + + const incorrectOperationNameValues = [true, 3000, null, undefined, {}, [], () => {}]; + incorrectOperationNameValues.forEach((incorrectOperationNameValue) => { + expect(() => + validateGraphqlConfig({ + configs: [ + { + operationType: 'query', + operationName: incorrectOperationNameValue, + routes: [] + } + ] + }) + ).toThrow('graphql.configs[0].operationName'); + }); + }); +}); diff --git a/bin/validateMockServerConfig/validateGraphqlConfig/validateGraphqlConfig.ts b/bin/validateMockServerConfig/validateGraphqlConfig/validateGraphqlConfig.ts new file mode 100644 index 00000000..f3619f6f --- /dev/null +++ b/bin/validateMockServerConfig/validateGraphqlConfig/validateGraphqlConfig.ts @@ -0,0 +1,51 @@ +import { isPlainObject } from '../../../src/utils/helpers'; +import { validateBaseUrl } from '../validateBaseUrl/validateBaseUrl'; +import { validateInterceptors } from '../validateInterceptors/validateInterceptors'; + +import { validateRoutes } from './validateRoutes/validateRoutes'; + +const validateConfigs = (configs: unknown) => { + const isConfigsArray = Array.isArray(configs); + if (isConfigsArray) { + configs.forEach((config, index) => { + const { operationType, operationName } = config; + + if (operationType !== 'query' && operationType !== 'mutation') { + throw new Error(`configs[${index}].operationType`); + } + + const isOperationNameStringOrRegExp = + typeof operationName === 'string' || operationName instanceof RegExp; + if (!isOperationNameStringOrRegExp) { + throw new Error(`configs[${index}].operationName`); + } + + try { + validateRoutes(config.routes, operationType); + validateInterceptors(config.interceptors); + } catch (error: any) { + throw new Error(`configs[${index}].${error.message}`); + } + }); + return; + } + + throw new Error('configs'); +}; + +export const validateGraphqlConfig = (graphqlConfig: unknown) => { + const isGraphqlConfigObject = isPlainObject(graphqlConfig); + if (isGraphqlConfigObject) { + try { + validateBaseUrl(graphqlConfig.baseUrl); + validateConfigs(graphqlConfig.configs); + } catch (error: any) { + throw new Error(`graphql.${error.message}`); + } + return; + } + + if (typeof graphqlConfig !== 'undefined') { + throw new Error('graphql'); + } +}; diff --git a/bin/validateMockServerConfig/validateGraphqlConfig/validateRoutes/validateRoutes.test.ts b/bin/validateMockServerConfig/validateGraphqlConfig/validateRoutes/validateRoutes.test.ts new file mode 100644 index 00000000..0525465b --- /dev/null +++ b/bin/validateMockServerConfig/validateGraphqlConfig/validateRoutes/validateRoutes.test.ts @@ -0,0 +1,207 @@ +import { validateRoutes } from './validateRoutes'; + +describe('validateRoutes (graphql)', () => { + test('Should correctly handle routes only with correct type', () => { + expect(() => validateRoutes([{ data: null }], 'query')).not.toThrow(Error); + + const incorrectRouteArrayValues = ['string', true, 3000, null, undefined, {}, () => {}]; + incorrectRouteArrayValues.forEach((incorrectRouteArrayValue) => { + expect(() => validateRoutes(incorrectRouteArrayValue, 'query')).toThrow(new Error('routes')); + }); + + const incorrectRouteValues = ['string', true, 3000, null, undefined, {}, [], () => {}]; + incorrectRouteValues.forEach((incorrectRouteValue) => { + expect(() => validateRoutes([incorrectRouteValue], 'query')).toThrow(new Error('routes[0]')); + }); + }); + + test('Should correctly handle entities only with correct type', () => { + const correctEntitiesValues = [{}, { headers: { key: 'value' } }, undefined]; + correctEntitiesValues.forEach((correctEntitiesValue) => { + expect(() => + validateRoutes( + [ + { + entities: correctEntitiesValue, + data: null + } + ], + 'query' + ) + ).not.toThrow(Error); + }); + + const incorrectEntitiesValues = ['string', true, 3000, null, [], () => {}]; + incorrectEntitiesValues.forEach((incorrectEntitiesValue) => { + expect(() => + validateRoutes( + [ + { + entities: incorrectEntitiesValue, + data: null + } + ], + 'query' + ) + ).toThrow(new Error('routes[0].entities')); + }); + }); + + test('Should correctly handle query operation type entities only with correct type', () => { + const correctEntities = ['headers', 'query', 'variables']; + correctEntities.forEach((correctEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [correctEntity]: { key: 'value' } }, + data: null + } + ], + 'query' + ) + ).not.toThrow(Error); + }); + + const incorrectEntities = ['body', 'other']; + incorrectEntities.forEach((incorrectEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [incorrectEntity]: { key: 'value' } }, + data: null + } + ], + 'query' + ) + ).toThrow(new Error(`routes[0].entities.${incorrectEntity}`)); + }); + }); + + test('Should correctly handle mutation operation type entities only with correct type', () => { + const correctEntities = ['headers', 'query', 'variables']; + correctEntities.forEach((correctEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [correctEntity]: { key: 'value' } }, + data: null + } + ], + 'mutation' + ) + ).not.toThrow(Error); + }); + + const incorrectEntities = ['body', 'other']; + incorrectEntities.forEach((incorrectEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [incorrectEntity]: { key: 'value' } }, + data: null + } + ], + 'mutation' + ) + ).toThrow(new Error(`routes[0].entities.${incorrectEntity}`)); + }); + }); + + test('Should correctly handle headers entity only with correct type', () => { + const correctHeadersObjectValues = ['string']; + correctHeadersObjectValues.forEach((correctHeadersObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { headers: { key: correctHeadersObjectValue } }, + data: null + } + ], + 'query' + ) + ).not.toThrow(Error); + }); + + const incorrectHeadersValues = [true, 3000, null, undefined, [], () => {}]; + incorrectHeadersValues.forEach((incorrectHeaderValue) => { + expect(() => + validateRoutes( + [ + { + entities: { headers: incorrectHeaderValue }, + data: null + } + ], + 'query' + ) + ).toThrow(new Error('routes[0].entities.headers')); + }); + + const incorrectHeadersObjectValues = [true, 3000, null, undefined, {}, [], () => {}]; + incorrectHeadersObjectValues.forEach((incorrectHeadersObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { headers: { key: incorrectHeadersObjectValue } }, + data: null + } + ], + 'query' + ) + ).toThrow(new Error('routes[0].entities.headers.key')); + }); + }); + + test('Should correctly handle query entity only with correct type', () => { + const correctQueryObjectValues = ['string']; + correctQueryObjectValues.forEach((correctQueryObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { query: { key: correctQueryObjectValue } }, + data: null + } + ], + 'query' + ) + ).not.toThrow(Error); + }); + + const incorrectQueryValues = ['string', true, 3000, null, undefined, [], () => {}]; + incorrectQueryValues.forEach((incorrectQueryValue) => { + expect(() => + validateRoutes( + [ + { + entities: { query: incorrectQueryValue }, + data: null + } + ], + 'query' + ) + ).toThrow(new Error('routes[0].entities.query')); + }); + + const incorrectQueryObjectValues = [true, 3000, null, undefined, {}, [], () => {}]; + incorrectQueryObjectValues.forEach((incorrectQueryObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { query: { key: incorrectQueryObjectValue } }, + data: null + } + ], + 'query' + ) + ).toThrow(new Error('routes[0].entities.query.key')); + }); + }); +}); diff --git a/bin/validateMockServerConfig/validateGraphqlConfig/validateRoutes/validateRoutes.ts b/bin/validateMockServerConfig/validateGraphqlConfig/validateRoutes/validateRoutes.ts new file mode 100644 index 00000000..22d450da --- /dev/null +++ b/bin/validateMockServerConfig/validateGraphqlConfig/validateRoutes/validateRoutes.ts @@ -0,0 +1,81 @@ +import type { GraphQLOperationsEntities, GraphQLOperationType } from '../../../../src'; +import { isPlainObject } from '../../../../src/utils/helpers'; +import { validateInterceptors } from '../../validateInterceptors/validateInterceptors'; + +type AllowedEntitiesByOperationType = { + [Key in keyof GraphQLOperationsEntities]: GraphQLOperationsEntities[Key][]; +}; +const ALLOWED_ENTITIES_BY_OPERATION_TYPE: AllowedEntitiesByOperationType = { + query: ['headers', 'query', 'variables'], + mutation: ['headers', 'query', 'variables'] +}; + +const validateHeadersOrQuery = (headersOrQuery: unknown, entity: string) => { + const isHeadersOrQueryObject = isPlainObject(headersOrQuery); + if (isHeadersOrQueryObject) { + Object.entries(headersOrQuery).forEach(([headerOrQueryKey, headerOrQueryValue]) => { + if (typeof headerOrQueryValue !== 'string') { + throw new Error(`${entity}.${headerOrQueryKey}`); + } + }); + return; + } + + throw new Error(entity); +}; + +const validateEntities = (entities: unknown, operationType: GraphQLOperationType) => { + const isEntitiesObject = isPlainObject(entities); + if (isEntitiesObject) { + Object.keys(entities).forEach((entity) => { + const isEntityAllowed = ALLOWED_ENTITIES_BY_OPERATION_TYPE[operationType].includes( + entity as any + ); + if (!isEntityAllowed) { + throw new Error(`entities.${entity}`); + } + + if (entity === 'headers' || entity === 'query') { + try { + const headersOrQuery = entities[entity]; + return validateHeadersOrQuery(headersOrQuery, entity); + } catch (error: any) { + throw new Error(`entities.${error.message}`); + } + } + }); + return; + } + + if (typeof entities !== 'undefined') { + throw new Error('entities'); + } +}; + +export const validateRoutes = (routes: unknown, operationType: GraphQLOperationType) => { + const isRoutesArray = Array.isArray(routes); + if (isRoutesArray) { + routes.forEach((route, index) => { + const isRouteObject = isPlainObject(route); + if (isRouteObject) { + const isRouteHasDataProperty = 'data' in route; + if (!isRouteHasDataProperty) { + throw new Error(`routes[${index}]`); + } + + try { + validateEntities(route.entities, operationType); + validateInterceptors(route.interceptors); + } catch (error: any) { + throw new Error(`routes[${index}].${error.message}`); + } + return; + } + + throw new Error(`routes[${index}]`); + }); + return; + } + + throw new Error('routes'); +}; diff --git a/bin/validateMockServerConfig/validateInterceptors/validateInterceptors.test.ts b/bin/validateMockServerConfig/validateInterceptors/validateInterceptors.test.ts new file mode 100644 index 00000000..53be0c4e --- /dev/null +++ b/bin/validateMockServerConfig/validateInterceptors/validateInterceptors.test.ts @@ -0,0 +1,51 @@ +import { validateInterceptors } from './validateInterceptors'; + +describe('validateInterceptors', () => { + test('Should correctly handle interceptors only with correct type', () => { + const correctInterceptorsValues = [{ request: () => {}, response: () => {} }, undefined]; + correctInterceptorsValues.forEach((correctInterceptorsValue) => { + expect(() => validateInterceptors(correctInterceptorsValue)).not.toThrow(Error); + }); + + const incorrectInterceptorsValues = ['string', true, 3000, null, [], () => {}]; + incorrectInterceptorsValues.forEach((incorrectInterceptorsValue) => { + expect(() => validateInterceptors(incorrectInterceptorsValue)).toThrow( + new Error('interceptors') + ); + }); + }); + + test('Should correctly handle interceptors request and response only with correctType', () => { + const correctRequestOrResponseInterceptorsValues = [() => {}, undefined]; + correctRequestOrResponseInterceptorsValues.forEach( + (correctRequestOrResponseInterceptorsValue) => { + expect(() => + validateInterceptors({ + request: correctRequestOrResponseInterceptorsValue + }) + ).not.toThrow(Error); + expect(() => + validateInterceptors({ + response: correctRequestOrResponseInterceptorsValue + }) + ).not.toThrow(Error); + } + ); + + const incorrectRequestOrResponseInterceptorsValues = ['string', true, 3000, null, {}, []]; + incorrectRequestOrResponseInterceptorsValues.forEach( + (incorrectRequestOrResponseInterceptorsValue) => { + expect(() => + validateInterceptors({ + request: incorrectRequestOrResponseInterceptorsValue + }) + ).toThrow(new Error('interceptors.request')); + expect(() => + validateInterceptors({ + response: incorrectRequestOrResponseInterceptorsValue + }) + ).toThrow(new Error('interceptors.response')); + } + ); + }); +}); diff --git a/bin/validateMockServerConfig/validateInterceptors/validateInterceptors.ts b/bin/validateMockServerConfig/validateInterceptors/validateInterceptors.ts new file mode 100644 index 00000000..d96ef60c --- /dev/null +++ b/bin/validateMockServerConfig/validateInterceptors/validateInterceptors.ts @@ -0,0 +1,19 @@ +import { isPlainObject } from '../../../src/utils/helpers'; + +export const validateInterceptors = (interceptors: unknown) => { + const isInterceptorsObject = isPlainObject(interceptors); + if (isInterceptorsObject) { + const { request, response } = interceptors; + if (typeof request !== 'function' && typeof request !== 'undefined') { + throw new Error('interceptors.request'); + } + if (typeof response !== 'function' && typeof response !== 'undefined') { + throw new Error('interceptors.response'); + } + return; + } + + if (typeof interceptors !== 'undefined') { + throw new Error('interceptors'); + } +}; diff --git a/bin/validateMockServerConfig/validateMockServerConfig.ts b/bin/validateMockServerConfig/validateMockServerConfig.ts new file mode 100644 index 00000000..4b58dd9b --- /dev/null +++ b/bin/validateMockServerConfig/validateMockServerConfig.ts @@ -0,0 +1,38 @@ +import { isPlainObject } from '../../src/utils/helpers'; + +import { validateBaseUrl } from './validateBaseUrl/validateBaseUrl'; +import { validateCors } from './validateCors/validateCors'; +import { validateGraphqlConfig } from './validateGraphqlConfig/validateGraphqlConfig'; +import { validateInterceptors } from './validateInterceptors/validateInterceptors'; +import { validatePort } from './validatePort/validatePort'; +import { validateRestConfig } from './validateRestConfig/validateRestConfig'; +import { validateStaticPath } from './validateStaticPath/validateStaticPath'; + +export const validateMockServerConfig = (mockServerConfig: unknown) => { + if (!isPlainObject(mockServerConfig)) { + throw new Error( + 'configuration should be plain object; see our doc (https://www.npmjs.com/package/mock-config-server) for more information' + ); + } + + if (!mockServerConfig.rest && !mockServerConfig.graphql) { + throw new Error( + 'configuration should contain at least one of these configs: rest | graphql; see our doc (https://www.npmjs.com/package/mock-config-server) for more information' + ); + } + + try { + if (mockServerConfig.rest) validateRestConfig(mockServerConfig.rest); + if (mockServerConfig.graphql) validateGraphqlConfig(mockServerConfig.graphql); + + validateBaseUrl(mockServerConfig.baseUrl); + validatePort(mockServerConfig.port); + validateStaticPath(mockServerConfig.staticPath); + validateInterceptors(mockServerConfig.interceptors); + validateCors(mockServerConfig.cors); + } catch (error: any) { + throw new Error( + `Validation Error: configuration.${error.message} does not match the API schema. Click here to see correct type: https://github.com/siberiacancode/mock-config-server` + ); + } +}; diff --git a/bin/validateMockServerConfig/validatePort/validatePort.test.ts b/bin/validateMockServerConfig/validatePort/validatePort.test.ts new file mode 100644 index 00000000..442605b3 --- /dev/null +++ b/bin/validateMockServerConfig/validatePort/validatePort.test.ts @@ -0,0 +1,15 @@ +import { validatePort } from './validatePort'; + +describe('validatePort', () => { + test('Should correctly handle port only with correct type', () => { + const correctPortValues = [3000, undefined]; + correctPortValues.forEach((correctPortValue) => { + expect(() => validatePort(correctPortValue)).not.toThrow(Error); + }); + }); + + const incorrectPortValues = ['string', true, null, {}, [], () => {}]; + incorrectPortValues.forEach((incorrectPortValue) => { + expect(() => validatePort(incorrectPortValue)).toThrow(new Error('port')); + }); +}); diff --git a/bin/validateMockServerConfig/validatePort/validatePort.ts b/bin/validateMockServerConfig/validatePort/validatePort.ts new file mode 100644 index 00000000..0574e04d --- /dev/null +++ b/bin/validateMockServerConfig/validatePort/validatePort.ts @@ -0,0 +1,5 @@ +export const validatePort = (port: unknown) => { + if (typeof port !== 'number' && typeof port !== 'undefined') { + throw new Error('port'); + } +}; diff --git a/bin/validateMockServerConfig/validateRestConfig/validateRestConfig.test.ts b/bin/validateMockServerConfig/validateRestConfig/validateRestConfig.test.ts new file mode 100644 index 00000000..2d909518 --- /dev/null +++ b/bin/validateMockServerConfig/validateRestConfig/validateRestConfig.test.ts @@ -0,0 +1,91 @@ +import { validateRestConfig } from './validateRestConfig'; + +describe('validateRestConfig', () => { + test('Should correctly handle rest config only with correct type', () => { + const correctRestConfigs = [ + { configs: [{ path: '/path', method: 'get', routes: [] }] }, + undefined + ]; + correctRestConfigs.forEach((correctRestConfig) => { + expect(() => validateRestConfig(correctRestConfig)).not.toThrow(Error); + }); + + const incorrectRestConfigs = ['string', true, 3000, null, [], () => {}]; + incorrectRestConfigs.forEach((incorrectRestConfig) => { + expect(() => validateRestConfig(incorrectRestConfig)).toThrow(new Error('rest')); + }); + }); + + test('Should correctly handle config path only with correct type', () => { + const correctConfigPathValues = ['/pathWithLeadingSlash', /\/path/]; + correctConfigPathValues.forEach((correctConfigPathValue) => { + expect(() => + validateRestConfig({ + configs: [ + { + path: correctConfigPathValue, + method: 'get', + routes: [] + } + ] + }) + ).not.toThrow(Error); + }); + + const incorrectConfigPathValues = [ + 'pathWithoutLeadingSlash', + true, + 3000, + null, + undefined, + {}, + [], + () => {} + ]; + incorrectConfigPathValues.forEach((incorrectConfigPathValue) => { + expect(() => + validateRestConfig({ + configs: [ + { + path: incorrectConfigPathValue, + method: 'get', + routes: [] + } + ] + }) + ).toThrow('rest.configs[0].path'); + }); + }); + + test('Should correctly handle config method only with correct type', () => { + const correctConfigMethodValues = ['get', 'post', 'put', 'patch', 'delete']; + correctConfigMethodValues.forEach((correctConfigMethodValue) => { + expect(() => + validateRestConfig({ + configs: [ + { + path: '/path', + method: correctConfigMethodValue, + routes: [] + } + ] + }) + ).not.toThrow(Error); + }); + + const incorrectConfigMethodValues = ['string', true, 3000, null, undefined, {}, [], () => {}]; + incorrectConfigMethodValues.forEach((incorrectConfigMethodValue) => { + expect(() => + validateRestConfig({ + configs: [ + { + path: '/path', + method: incorrectConfigMethodValue, + routes: [] + } + ] + }) + ).toThrow('rest.configs[0].method'); + }); + }); +}); diff --git a/bin/validateMockServerConfig/validateRestConfig/validateRestConfig.ts b/bin/validateMockServerConfig/validateRestConfig/validateRestConfig.ts new file mode 100644 index 00000000..fd749681 --- /dev/null +++ b/bin/validateMockServerConfig/validateRestConfig/validateRestConfig.ts @@ -0,0 +1,55 @@ +import type { RestMethod } from '../../../src'; +import { isPlainObject } from '../../../src/utils/helpers'; +import { validateBaseUrl } from '../validateBaseUrl/validateBaseUrl'; +import { validateInterceptors } from '../validateInterceptors/validateInterceptors'; + +import { validateRoutes } from './validateRoutes/validateRoutes'; + +const validateConfigs = (configs: unknown) => { + const isConfigsArray = Array.isArray(configs); + if (isConfigsArray) { + configs.forEach((config, index) => { + const { path, method } = config; + + const isPathStringWithLeadingSlash = typeof path === 'string' && path.startsWith('/'); + if (!isPathStringWithLeadingSlash && !(path instanceof RegExp)) { + throw new Error(`configs[${index}].path`); + } + + // ✅ important: + // compare without 'toLowerCase' because Express methods names is case-sensitive + const allowedMethods = ['get', 'post', 'delete', 'put', 'patch']; + const isMethodAllowed = typeof method === 'string' && allowedMethods.includes(method); + if (!isMethodAllowed) { + throw new Error(`configs[${index}].method`); + } + + try { + validateRoutes(config.routes, method as RestMethod); + validateInterceptors(config.interceptors); + } catch (error: any) { + throw new Error(`configs[${index}].${error.message}`); + } + }); + return; + } + + throw new Error('configs'); +}; + +export const validateRestConfig = (restConfig: unknown) => { + const isRestConfigObject = isPlainObject(restConfig); + if (isRestConfigObject) { + try { + validateBaseUrl(restConfig.baseUrl); + validateConfigs(restConfig.configs); + } catch (error: any) { + throw new Error(`rest.${error.message}`); + } + return; + } + + if (typeof restConfig !== 'undefined') { + throw new Error('rest'); + } +}; diff --git a/bin/validateMockServerConfig/validateRestConfig/validateRoutes/validateRoutes.test.ts b/bin/validateMockServerConfig/validateRestConfig/validateRoutes/validateRoutes.test.ts new file mode 100644 index 00000000..bb0106e7 --- /dev/null +++ b/bin/validateMockServerConfig/validateRestConfig/validateRoutes/validateRoutes.test.ts @@ -0,0 +1,331 @@ +import { validateRoutes } from './validateRoutes'; + +describe('validateRoutes (rest)', () => { + test('Should correctly handle routes only with correct type', () => { + expect(() => validateRoutes([{ data: null }], 'get')).not.toThrow(Error); + + const incorrectRouteArrayValues = ['string', true, 3000, null, undefined, {}, () => {}]; + incorrectRouteArrayValues.forEach((incorrectRouteArrayValue) => { + expect(() => validateRoutes(incorrectRouteArrayValue, 'get')).toThrow(new Error('routes')); + }); + + const incorrectRouteValues = ['string', true, 3000, null, undefined, {}, [], () => {}]; + incorrectRouteValues.forEach((incorrectRouteValue) => { + expect(() => validateRoutes([incorrectRouteValue], 'get')).toThrow(new Error('routes[0]')); + }); + }); + + test('Should correctly handle entities only with correct type', () => { + const correctEntitiesValues = [{}, { headers: { key: 'value' } }, undefined]; + correctEntitiesValues.forEach((correctEntitiesValue) => { + expect(() => + validateRoutes( + [ + { + entities: correctEntitiesValue, + data: null + } + ], + 'get' + ) + ).not.toThrow(Error); + }); + + const incorrectEntitiesValues = ['string', true, 3000, null, [], () => {}]; + incorrectEntitiesValues.forEach((incorrectEntitiesValue) => { + expect(() => + validateRoutes( + [ + { + entities: incorrectEntitiesValue, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities')); + }); + }); + + test('Should correctly handle get|delete method entities only with correct type', () => { + const correctEntities = ['headers', 'params', 'query']; + correctEntities.forEach((correctEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [correctEntity]: { key: 'value' } }, + data: null + } + ], + 'get' + ) + ).not.toThrow(Error); + expect(() => + validateRoutes( + [ + { + entities: { [correctEntity]: { key: 'value' } }, + data: null + } + ], + 'delete' + ) + ).not.toThrow(Error); + }); + + const incorrectEntities = ['body', 'other']; + incorrectEntities.forEach((incorrectEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [incorrectEntity]: { key: 'value' } }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error(`routes[0].entities.${incorrectEntity}`)); + expect(() => + validateRoutes( + [ + { + entities: { [incorrectEntity]: { key: 'value' } }, + data: null + } + ], + 'delete' + ) + ).toThrow(new Error(`routes[0].entities.${incorrectEntity}`)); + }); + }); + + test('Should correctly handle post|put|patch method entities only with correct type', () => { + const correctEntities = ['headers', 'params', 'query', 'body']; + correctEntities.forEach((correctEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [correctEntity]: { key: 'value' } }, + data: null + } + ], + 'post' + ) + ).not.toThrow(Error); + expect(() => + validateRoutes( + [ + { + entities: { [correctEntity]: { key: 'value' } }, + data: null + } + ], + 'put' + ) + ).not.toThrow(Error); + expect(() => + validateRoutes( + [ + { + entities: { [correctEntity]: { key: 'value' } }, + data: null + } + ], + 'patch' + ) + ).not.toThrow(Error); + }); + + const incorrectEntities = ['other']; + incorrectEntities.forEach((incorrectEntity) => { + expect(() => + validateRoutes( + [ + { + entities: { [incorrectEntity]: { key: 'value' } }, + data: null + } + ], + 'post' + ) + ).toThrow(new Error(`routes[0].entities.${incorrectEntity}`)); + expect(() => + validateRoutes( + [ + { + entities: { [incorrectEntity]: { key: 'value' } }, + data: null + } + ], + 'put' + ) + ).toThrow(new Error(`routes[0].entities.${incorrectEntity}`)); + expect(() => + validateRoutes( + [ + { + entities: { [incorrectEntity]: { key: 'value' } }, + data: null + } + ], + 'patch' + ) + ).toThrow(new Error(`routes[0].entities.${incorrectEntity}`)); + }); + }); + + test('Should correctly handle headers entity only with correct type', () => { + const correctHeadersObjectValues = ['value']; + correctHeadersObjectValues.forEach((correctHeadersObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { headers: { key: correctHeadersObjectValue } }, + data: null + } + ], + 'get' + ) + ).not.toThrow(Error); + }); + + const incorrectHeadersValues = ['string', true, 3000, null, undefined, [], () => {}]; + incorrectHeadersValues.forEach((incorrectHeaderValue) => { + expect(() => + validateRoutes( + [ + { + entities: { headers: incorrectHeaderValue }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities.headers')); + }); + + const incorrectHeadersObjectValues = [true, 3000, null, undefined, {}, [], () => {}]; + incorrectHeadersObjectValues.forEach((incorrectHeadersObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { headers: { key: incorrectHeadersObjectValue } }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities.headers.key')); + }); + }); + + test('Should correctly handle params entity only with correct type', () => { + const correctParamsObjectValues = ['value']; + correctParamsObjectValues.forEach((correctParamsObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { params: { key: correctParamsObjectValue } }, + data: null + } + ], + 'get' + ) + ).not.toThrow(Error); + }); + + const incorrectParamsValues = ['string', true, 3000, null, undefined, [], () => {}]; + incorrectParamsValues.forEach((incorrectParamValue) => { + expect(() => + validateRoutes( + [ + { + entities: { params: incorrectParamValue }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities.params')); + }); + + const incorrectParamsObjectValues = [true, 3000, null, undefined, {}, [], () => {}]; + incorrectParamsObjectValues.forEach((incorrectParamsObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { params: { key: incorrectParamsObjectValue } }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities.params.key')); + }); + }); + + test('Should correctly handle query entity only with correct type', () => { + const correctQueryObjectValues = ['value', ['value1', 'value2']]; + correctQueryObjectValues.forEach((correctQueryObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { query: { key: correctQueryObjectValue } }, + data: null + } + ], + 'get' + ) + ).not.toThrow(Error); + }); + + const incorrectQueryValues = ['string', true, 3000, null, undefined, [], () => {}]; + incorrectQueryValues.forEach((incorrectQueryValue) => { + expect(() => + validateRoutes( + [ + { + entities: { query: incorrectQueryValue }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities.query')); + }); + + const incorrectQueryObjectValues = [true, 3000, null, undefined, () => {}]; + incorrectQueryObjectValues.forEach((incorrectQueryObjectValue) => { + expect(() => + validateRoutes( + [ + { + entities: { query: { key: incorrectQueryObjectValue } }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities.query.key')); + expect(() => + validateRoutes( + [ + { + entities: { query: { key: [incorrectQueryObjectValue] } }, + data: null + } + ], + 'get' + ) + ).toThrow(new Error('routes[0].entities.query.key[0]')); + }); + }); +}); diff --git a/bin/validateMockServerConfig/validateRestConfig/validateRoutes/validateRoutes.ts b/bin/validateMockServerConfig/validateRestConfig/validateRoutes/validateRoutes.ts new file mode 100644 index 00000000..7a6ba949 --- /dev/null +++ b/bin/validateMockServerConfig/validateRestConfig/validateRoutes/validateRoutes.ts @@ -0,0 +1,115 @@ +import type { RestMethod, RestMethodsEntities } from '../../../../src'; +import { isPlainObject } from '../../../../src/utils/helpers'; +import { validateInterceptors } from '../../validateInterceptors/validateInterceptors'; + +type AllowedEntitiesByMethod = { + [Key in keyof RestMethodsEntities]: RestMethodsEntities[Key][]; +}; +const ALLOWED_ENTITIES_BY_METHOD: AllowedEntitiesByMethod = { + get: ['headers', 'query', 'params'], + delete: ['headers', 'query', 'params'], + post: ['headers', 'query', 'params', 'body'], + put: ['headers', 'query', 'params', 'body'], + patch: ['headers', 'query', 'params', 'body'] +}; + +const validateHeadersOrParams = (headersOrParams: unknown, entity: string) => { + const isHeadersOrParamsObject = isPlainObject(headersOrParams); + if (isHeadersOrParamsObject) { + Object.entries(headersOrParams).forEach(([headerOrParamKey, headerOrParamValue]) => { + if (typeof headerOrParamValue !== 'string') { + throw new Error(`${entity}.${headerOrParamKey}`); + } + }); + return; + } + + throw new Error(entity); +}; + +const validateQuery = (query: unknown, entity: string) => { + const isQueryObject = isPlainObject(query); + if (isQueryObject) { + Object.entries(query).forEach(([queryKey, queryValue]) => { + const isQueryValueArray = Array.isArray(queryValue); + if (isQueryValueArray) { + queryValue.forEach((queryValueElement, index) => { + if (typeof queryValueElement !== 'string') { + throw new Error(`${entity}.${queryKey}[${index}]`); + } + }); + return; + } + + if (typeof queryValue !== 'string') { + throw new Error(`${entity}.${queryKey}`); + } + }); + return; + } + + throw new Error(entity); +}; + +const validateEntities = (entities: unknown, method: RestMethod) => { + const isEntitiesObject = isPlainObject(entities); + if (isEntitiesObject) { + Object.keys(entities).forEach((entity) => { + const isEntityAllowed = ALLOWED_ENTITIES_BY_METHOD[method].includes(entity as any); + if (!isEntityAllowed) { + throw new Error(`entities.${entity}`); + } + + if (entity === 'headers' || entity === 'params') { + try { + const headersOrParams = entities[entity]; + return validateHeadersOrParams(headersOrParams, entity); + } catch (error: any) { + throw new Error(`entities.${error.message}`); + } + } + + if (entity === 'query') { + try { + const query = entities[entity]; + return validateQuery(query, entity); + } catch (error: any) { + throw new Error(`entities.${error.message}`); + } + } + }); + return; + } + + if (typeof entities !== 'undefined') { + throw new Error('entities'); + } +}; + +export const validateRoutes = (routes: unknown, method: RestMethod) => { + const isRoutesArray = Array.isArray(routes); + if (isRoutesArray) { + routes.forEach((route, index) => { + const isRouteObject = isPlainObject(route); + if (isRouteObject) { + const isRouteHasDataProperty = 'data' in route; + if (!isRouteHasDataProperty) { + throw new Error(`routes[${index}]`); + } + + try { + validateEntities(route.entities, method); + validateInterceptors(route.interceptors); + } catch (error: any) { + throw new Error(`routes[${index}].${error.message}`); + } + return; + } + + throw new Error(`routes[${index}]`); + }); + return; + } + + throw new Error('routes'); +}; diff --git a/bin/validateMockServerConfig/validateStaticPath/validateStaticPath.test.ts b/bin/validateMockServerConfig/validateStaticPath/validateStaticPath.test.ts new file mode 100644 index 00000000..8148cf93 --- /dev/null +++ b/bin/validateMockServerConfig/validateStaticPath/validateStaticPath.test.ts @@ -0,0 +1,129 @@ +import { validateStaticPath } from './validateStaticPath'; + +describe('validateStaticPath', () => { + test('Should correctly handle static path (except arrays and plain objects) only with correct type', () => { + const correctStaticPaths = ['/stringWithLeadingSlash', undefined]; + correctStaticPaths.forEach((correctStaticPath) => { + expect(() => validateStaticPath(correctStaticPath)).not.toThrow(Error); + }); + + const incorrectStaticPaths = ['stringWithoutLeadingSlash', true, 3000, null, () => {}]; + incorrectStaticPaths.forEach((incorrectStaticPath) => { + expect(() => validateStaticPath(incorrectStaticPath)).toThrow(new Error('staticPath')); + }); + }); + + test('Should correctly handle plain object static path only with correct type', () => { + const correctObjectStaticPaths = [ + { prefix: '/stringWithLeadingSlash', path: '/stringWithLeadingSlash' } + ]; + correctObjectStaticPaths.forEach((correctObjectStaticPath) => { + expect(() => validateStaticPath(correctObjectStaticPath)).not.toThrow(Error); + }); + + const incorrectPrefixObjectStaticPaths = [ + 'stringWithoutLeadingSlash', + true, + 3000, + null, + undefined, + {}, + [], + () => {} + ]; + incorrectPrefixObjectStaticPaths.forEach((incorrectPrefixObjectStaticPath) => { + expect(() => + validateStaticPath({ + prefix: incorrectPrefixObjectStaticPath, + path: '/stringWithLeadingSlash' + }) + ).toThrow(new Error('staticPath.prefix')); + }); + + const incorrectPathObjectStaticPaths = [ + 'stringWithoutLeadingSlash', + true, + 3000, + null, + undefined, + {}, + [], + () => {} + ]; + incorrectPathObjectStaticPaths.forEach((incorrectPathObjectStaticPath) => { + expect(() => + validateStaticPath({ + prefix: '/stringWithLeadingSlash', + path: incorrectPathObjectStaticPath + }) + ).toThrow(new Error('staticPath.path')); + }); + }); + + test('Should correctly handle array static path only with correct type', () => { + const correctArrayStaticPaths = [ + '/stringWithLeadingSlash', + { prefix: '/stringWithLeadingSlash', path: '/stringWithLeadingSlash' } + ]; + correctArrayStaticPaths.forEach((correctArrayStaticPath) => { + expect(() => validateStaticPath([correctArrayStaticPath])).not.toThrow(Error); + }); + + const incorrectArrayStaticPaths = [ + 'stringWithoutLeadingSlash', + true, + 3000, + null, + undefined, + [], + () => {} + ]; + incorrectArrayStaticPaths.forEach((incorrectArrayStaticPath) => { + expect(() => validateStaticPath([incorrectArrayStaticPath])).toThrow( + new Error('staticPath[0]') + ); + }); + + const incorrectArrayPrefixObjectStaticPaths = [ + 'stringWithoutLeadingSlash', + true, + 3000, + null, + undefined, + {}, + [], + () => {} + ]; + incorrectArrayPrefixObjectStaticPaths.forEach((incorrectArrayPrefixObjectStaticPath) => { + expect(() => + validateStaticPath([ + { + prefix: incorrectArrayPrefixObjectStaticPath, + path: '/stringWithLeadingSlash' + } + ]) + ).toThrow(new Error('staticPath[0].prefix')); + }); + + const incorrectArrayPathObjectStaticPaths = [ + 'stringWithoutLeadingSlash', + true, + 3000, + null, + undefined, + {}, + [], + () => {} + ]; + incorrectArrayPathObjectStaticPaths.forEach((incorrectArrayPathObjectStaticPath) => { + expect(() => + validateStaticPath([ + { + prefix: '/stringWithLeadingSlash', + path: incorrectArrayPathObjectStaticPath + } + ]) + ).toThrow(new Error('staticPath[0].path')); + }); + }); +}); diff --git a/bin/validateMockServerConfig/validateStaticPath/validateStaticPath.ts b/bin/validateMockServerConfig/validateStaticPath/validateStaticPath.ts new file mode 100644 index 00000000..7f20940c --- /dev/null +++ b/bin/validateMockServerConfig/validateStaticPath/validateStaticPath.ts @@ -0,0 +1,44 @@ +import { isPlainObject } from '../../../src/utils/helpers'; + +export const validateStaticPath = (staticPath: unknown) => { + const isStaticPathArray = Array.isArray(staticPath); + if (isStaticPathArray) { + staticPath.forEach((staticPathElement, index) => { + const isStaticPathElementObject = isPlainObject(staticPathElement); + if (isStaticPathElementObject) { + const { prefix, path } = staticPathElement; + if (typeof prefix !== 'string' || !prefix.startsWith('/')) { + throw new Error(`staticPath[${index}].prefix`); + } + if (typeof path !== 'string' || !path.startsWith('/')) { + throw new Error(`staticPath[${index}].path`); + } + return; + } + + if (typeof staticPathElement !== 'string' || !staticPathElement.startsWith('/')) { + throw new Error(`staticPath[${index}]`); + } + }); + return; + } + + const isStaticPathObject = isPlainObject(staticPath); + if (isStaticPathObject) { + const { prefix, path } = staticPath; + if (typeof prefix !== 'string' || !prefix.startsWith('/')) { + throw new Error('staticPath.prefix'); + } + if (typeof path !== 'string' || !path.startsWith('/')) { + throw new Error('staticPath.path'); + } + return; + } + + if (typeof staticPath !== 'string' && typeof staticPath !== 'undefined') { + throw new Error('staticPath'); + } + if (typeof staticPath === 'string' && !staticPath.startsWith('/')) { + throw new Error('staticPath'); + } +}; diff --git a/jest.config.js b/jest.config.js index e87892fa..c5885c8f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,13 @@ const jestConfig = { preset: 'ts-jest', - testEnvironment: 'node' + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { tsconfig: './tsconfig.dev.json' } + ], + } }; module.exports = jestConfig; diff --git a/package.json b/package.json index 5521ae7d..1a0f587d 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "node": ">=14" }, "scripts": { - "build": "rimraf dist && tsc -p ./tsconfig.json", + "build": "rimraf dist && tsc -p tsconfig.production.json", "prepare": "yarn build", "test": "jest", "lint": "eslint . --ext ts --no-error-on-unmatched-pattern", @@ -49,35 +49,38 @@ }, "dependencies": { "@types/body-parser": "^1.19.2", - "@types/express": "^4.17.15", + "@types/express": "^4.17.17", "@types/flat": "^5.0.2", + "ansi-colors": "^4.1.3", "body-parser": "^1.20.0", - "esbuild": "^0.16.6", + "ejs": "^3.1.8", + "esbuild": "^0.17.8", "express": "^4.18.1", - "flat": "^5.0.2" + "flat": "^5.0.2", + "graphql": "^16.6.0" }, "devDependencies": { - "@types/jest": "^29.2.4", + "@types/jest": "^29.4.0", "@types/supertest": "^2.0.12", - "@typescript-eslint/eslint-plugin": "^5.31.0", - "@typescript-eslint/parser": "^5.31.0", - "eslint": "^8.15.0", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "eslint": "^8.34.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.4.1", - "eslint-plugin-import": "^2.26.0", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-simple-import-sort": "^7.0.0", + "eslint-plugin-simple-import-sort": "^10.0.0", "husky": "^8.0.1", - "jest": "^29.3.1", - "lint-staged": "^13.0.3", + "jest": "^29.4.2", + "lint-staged": "^13.1.1", "nodemon": "^2.0.19", - "prettier": "^2.7.1", - "rimraf": "^3.0.2", + "prettier": "^2.8.3", + "rimraf": "4.1.2", "supertest": "^6.3.3", "ts-jest": "^29.0.3", - "typescript": "^4.7.4" + "typescript": "^4.9.5" }, "homepage": "https://github.com/siberiacancode/mock-config-server", "repository": { diff --git a/src/configs/isEntitiesEqual/isEntityValuesEqual.test.ts b/src/configs/isEntitiesEqual/isEntityValuesEqual.test.ts index c84521f8..4b0aae43 100644 --- a/src/configs/isEntitiesEqual/isEntityValuesEqual.test.ts +++ b/src/configs/isEntitiesEqual/isEntityValuesEqual.test.ts @@ -1,9 +1,12 @@ import { isEntityValuesEqual } from './isEntityValuesEqual'; describe('isEntityValuesEqual', () => { - test('Primitive values should compare independent of their types', () => { - expect(isEntityValuesEqual(13, '13')).toBe(true); + test('All Primitive values should compare independent of their types', () => { + expect(isEntityValuesEqual(12, '12')).toBe(true); expect(isEntityValuesEqual(true, 'true')).toBe(true); + expect(isEntityValuesEqual(null, 'null')).toBe(true); + expect(isEntityValuesEqual(undefined, 'undefined')).toBe(true); + expect(isEntityValuesEqual('string', 'string')).toBe(true); }); test('Arrays should be full equal with nested objects (independent of primitive values types)', () => { diff --git a/src/configs/isEntitiesEqual/isEntityValuesEqual.ts b/src/configs/isEntitiesEqual/isEntityValuesEqual.ts index b89086c4..895bf27d 100644 --- a/src/configs/isEntitiesEqual/isEntityValuesEqual.ts +++ b/src/configs/isEntitiesEqual/isEntityValuesEqual.ts @@ -1,12 +1,8 @@ import { flatten } from 'flat'; import { isPlainObject } from '../../utils/helpers'; -import type { Entities, EntitiesValues } from '../../utils/types'; -export const isEntityValuesEqual = ( - firstEntityValue: EntitiesValues[Entity], - secondEntityValue: EntitiesValues[Entity] -) => { +export const isEntityValuesEqual = (firstEntityValue: any, secondEntityValue: any) => { const isValuesArePlainObjects = isPlainObject(firstEntityValue) && isPlainObject(secondEntityValue); if (isValuesArePlainObjects) { @@ -37,5 +33,5 @@ export const isEntityValuesEqual = ( ); } - return firstEntityValue.toString() === secondEntityValue.toString(); + return `${firstEntityValue}` === `${secondEntityValue}`; }; diff --git a/src/cors/corsMiddleware/corsMiddleware.test.ts b/src/cors/corsMiddleware/corsMiddleware.test.ts index 9e4037c0..785e76a6 100644 --- a/src/cors/corsMiddleware/corsMiddleware.test.ts +++ b/src/cors/corsMiddleware/corsMiddleware.test.ts @@ -6,18 +6,21 @@ import type { Cors } from '../../utils/types'; import { corsMiddleware } from './corsMiddleware'; describe('corsMiddleware', () => { - test('Should set default cors for request if does not set custom cors settings', async () => { - const server = express(); + const testOrigin = 'https://test.com'; + test('Should set default cors for OPTIONS preflight request if does not set custom cors settings', async () => { + const server = express(); const cors: Cors = { - origin: ['https://test.com', 'https://uncorrectDomain.com'] + origin: testOrigin }; corsMiddleware(server, cors); - const response = await request(server).get('/').set({ origin: 'https://test.com' }); + + const response = await request(server).options('/').set({ origin: testOrigin }); expect(response.headers).toMatchObject({ 'access-control-allow-headers': '*', + 'access-control-expose-headers': '*', 'access-control-allow-methods': '*', 'access-control-allow-origin': 'https://test.com', 'access-control-max-age': '3600', @@ -25,84 +28,125 @@ describe('corsMiddleware', () => { }); }); - test('Should not set cors for request if origin does not match', async () => { + test(`Should set default cors for request if does not set custom cors settings`, async () => { const server = express(); - const cors: Cors = { - origin: 'https://uncorrectDomain.com' + origin: testOrigin }; corsMiddleware(server, cors); - const response = await request(server).get('/').set({ origin: 'https://test.com' }); - - expect(response.headers).not.toHaveProperty('access-control-allow-headers'); - expect(response.headers).not.toHaveProperty('access-control-allow-methods'); - expect(response.headers).not.toHaveProperty('access-control-allow-origin'); - expect(response.headers).not.toHaveProperty('access-control-max-age'); - expect(response.headers).not.toHaveProperty('access-control-allow-credentials'); - }); - - test('Should set allow headers to access-control-allow-headers', async () => { - const server = express(); - const cors: Cors = { - origin: 'https://test.com', - headers: ['header1', 'header2'] - }; - - corsMiddleware(server, cors); - const response = await request(server).get('/').set({ origin: 'https://test.com' }); + const response = await request(server).get('/').set({ origin: testOrigin }); expect(response.headers).toMatchObject({ - 'access-control-allow-headers': 'header1, header2' + 'access-control-allow-origin': 'https://test.com', + 'access-control-allow-credentials': 'true', + 'access-control-expose-headers': '*' }); - }); - - test('Should set methods to access-control-allow-methods', async () => { - const server = express(); - - const cors: Cors = { - origin: 'https://test.com', - methods: ['GET', 'POST'] - }; - corsMiddleware(server, cors); - const response = await request(server).get('/').set({ origin: 'https://test.com' }); - - expect(response.headers).toMatchObject({ - 'access-control-allow-methods': 'GET, POST' + expect(response.headers).not.toMatchObject({ + 'access-control-allow-headers': expect.any(String), + 'access-control-allow-methods': expect.any(String), + 'access-control-max-age': expect.any(String) }); }); - test('Should set methods to access-control-allow-credentials', async () => { - const server = express(); - - const cors: Cors = { - origin: 'https://test.com', - credentials: false - }; - - corsMiddleware(server, cors); - const response = await request(server).get('/').set({ origin: 'https://test.com' }); - - expect(response.headers).toMatchObject({ - 'access-control-allow-credentials': 'false' + const unsuitableOrigins = [ + 'https://uncorrectDomain.com', + [], + /https:\/\/uncorrectDomain.com/g, + () => 'https://uncorrectDomain.com', + () => Promise.resolve('https://uncorrectDomain.com') + ]; + + unsuitableOrigins.forEach((unsuitableOrigin) => { + test('Should not set default cors for OPTIONS preflight request if origin does not match', async () => { + const server = express(); + const cors: Cors = { + origin: unsuitableOrigin + }; + + corsMiddleware(server, cors); + + const response = await request(server).options('/').set({ origin: testOrigin }); + + expect(response.headers).not.toMatchObject({ + 'access-control-allow-headers': expect.any(String), + 'access-control-expose-headers': expect.any(String), + 'access-control-allow-methods': expect.any(String), + 'access-control-allow-origin': expect.any(String), + 'access-control-max-age': expect.any(String), + 'access-control-allow-credentials': expect.any(String) + }); }); - }); - test('Should set max-age to access-control-max-age', async () => { - const server = express(); + test(`Should not set cors for request if origin does not match`, async () => { + const server = express(); + const cors: Cors = { + origin: unsuitableOrigin + }; - const cors: Cors = { - origin: 'https://test.com', - maxAge: 10000 - }; + corsMiddleware(server, cors); - corsMiddleware(server, cors); - const response = await request(server).get('/').set({ origin: 'https://test.com' }); + const response = await request(server).get('/').set({ origin: testOrigin }); - expect(response.headers).toMatchObject({ - 'access-control-max-age': '10000' + expect(response.headers).not.toMatchObject({ + 'access-control-allow-origin': expect.any(String), + 'access-control-allow-credentials': expect.any(String), + 'access-control-expose-headers': expect.any(String) + }); }); }); + + const corsParamsAndHeaders: { params: Omit; headers: Record }[] = + [ + { + params: { allowedHeaders: ['header1', 'header2'] }, + headers: { + 'access-control-allow-headers': 'header1, header2' + } + }, + { + params: { exposedHeaders: ['header1', 'header2'] }, + headers: { + 'access-control-expose-headers': 'header1, header2' + } + }, + { + params: { maxAge: 10000 }, + headers: { + 'access-control-max-age': '10000' + } + }, + { + params: { methods: ['GET', 'POST'] }, + headers: { + 'access-control-allow-methods': 'GET, POST' + } + }, + { + params: { credentials: false }, + headers: { + 'access-control-allow-credentials': 'false' + } + } + ]; + + corsParamsAndHeaders.forEach(({ params, headers }) => + test(`Should set allow param(s) ${Object.keys(params).join(', ')} to header(s) ${Object.keys( + headers + ).join(', ')}`, async () => { + const server = express(); + const cors: Cors = { + origin: testOrigin, + ...params + }; + + corsMiddleware(server, cors); + + const response = await request(server).options('/').set({ origin: testOrigin }); + + expect(response.headers).toMatchObject(headers); + }) + ); }); diff --git a/src/cors/corsMiddleware/corsMiddleware.ts b/src/cors/corsMiddleware/corsMiddleware.ts index b4a29d9d..53f49562 100644 --- a/src/cors/corsMiddleware/corsMiddleware.ts +++ b/src/cors/corsMiddleware/corsMiddleware.ts @@ -1,40 +1,60 @@ import type { Express } from 'express'; import { DEFAULT } from '../../utils/constants'; -import type { Cors } from '../../utils/types'; +import type { Cors, CorsOrigin } from '../../utils/types'; import { getAllowedOrigins } from '../getOrigins/getAllowedOrigins'; -export const corsMiddleware = async (server: Express, cors: Cors) => { +export const corsMiddleware = (server: Express, cors: Cors) => { server.use(async (req, res, next) => { if (Array.isArray(cors.origin) && !cors.origin.length) { return next(); } - const allowedOrigins = await getAllowedOrigins(cors.origin); + let allowedOrigins: CorsOrigin[] = []; + + if (typeof cors.origin === 'function') { + const origins = await cors.origin(req); + allowedOrigins = getAllowedOrigins(origins); + } else { + allowedOrigins = getAllowedOrigins(cors.origin); + } + const { origin } = req.headers; if (!allowedOrigins?.length || !origin) { return next(); } - const isAllowedOrigin = allowedOrigins.some((allowedOrigin) => { - if (typeof allowedOrigin === 'string') { - return allowedOrigin.includes(origin); + const isRequestOriginAllowed = allowedOrigins.some((allowedOrigin) => { + if (allowedOrigin instanceof RegExp) { + return new RegExp(allowedOrigin).test(origin); } - return allowedOrigin.test(origin); + return allowedOrigin === origin; }); - if (isAllowedOrigin) { + if (isRequestOriginAllowed) { res.setHeader('Access-Control-Allow-Origin', origin); - res.setHeader('Access-Control-Allow-Methods', cors.methods ?? DEFAULT.CORS.METHODS); - res.setHeader('Access-Control-Allow-Headers', cors.headers ?? DEFAULT.CORS.HEADERS); res.setHeader( 'Access-Control-Allow-Credentials', `${cors.credentials ?? DEFAULT.CORS.CREDENTIALS}` ); - res.setHeader('Access-Control-Max-Age', cors.maxAge ?? DEFAULT.CORS.MAX_AGE); + res.setHeader( + 'Access-Control-Expose-Headers', + cors.exposedHeaders ?? DEFAULT.CORS.EXPOSED_HEADERS + ); + + if (req.method === 'OPTIONS') { + res.setHeader('Access-Control-Allow-Methods', cors.methods ?? DEFAULT.CORS.METHODS); + res.setHeader( + 'Access-Control-Allow-Headers', + cors.allowedHeaders ?? DEFAULT.CORS.ALLOWED_HEADERS + ); + res.setHeader('Access-Control-Max-Age', cors.maxAge ?? DEFAULT.CORS.MAX_AGE); + res.sendStatus(204); + return res.end(); + } } - next(); + return next(); }); }; diff --git a/src/cors/getOrigins/getAllowedOrigins.test.ts b/src/cors/getOrigins/getAllowedOrigins.test.ts index 756ac0c3..f8d921c5 100644 --- a/src/cors/getOrigins/getAllowedOrigins.test.ts +++ b/src/cors/getOrigins/getAllowedOrigins.test.ts @@ -1,28 +1,15 @@ import { getAllowedOrigins } from './getAllowedOrigins'; describe('getAllowedOrigins', () => { - test('Function should return array if get string or RegExp parameter', async () => { - expect(getAllowedOrigins(/origin/g)).resolves.toEqual([/origin/g]); - expect(getAllowedOrigins('https://origin.com')).resolves.toEqual(['https://origin.com']); + test('Function should return array if get string or RegExp parameter', () => { + expect(getAllowedOrigins(/origin/g)).toEqual([/origin/g]); + expect(getAllowedOrigins('https://origin.com')).toEqual(['https://origin.com']); }); test('Function should return array if get array parameter', () => { - expect(getAllowedOrigins([/origin/g, 'https://origin.com'])).resolves.toEqual([ + expect(getAllowedOrigins([/origin/g, 'https://origin.com'])).toEqual([ /origin/g, 'https://origin.com' ]); }); - - test('Function should return array if get function parameter', () => { - expect(getAllowedOrigins(() => [/origin/g, 'https://origin.com'])).resolves.toEqual([ - /origin/g, - 'https://origin.com' - ]); - }); - - test('Function should return array if get promise parameter', () => { - expect( - getAllowedOrigins(() => Promise.resolve([/origin/g, 'https://origin.com'])) - ).resolves.toEqual([/origin/g, 'https://origin.com']); - }); }); diff --git a/src/cors/getOrigins/getAllowedOrigins.ts b/src/cors/getOrigins/getAllowedOrigins.ts index aea35fcf..0380ce6d 100644 --- a/src/cors/getOrigins/getAllowedOrigins.ts +++ b/src/cors/getOrigins/getAllowedOrigins.ts @@ -1,6 +1,6 @@ -import type { Cors } from '../../utils/types'; +import type { CorsOrigin } from '../../utils/types'; -export const getAllowedOrigins = async (origin: Cors['origin']): Promise<(string | RegExp)[]> => { +export const getAllowedOrigins = (origin: CorsOrigin): (string | RegExp)[] => { if (Array.isArray(origin)) { return origin; } @@ -9,9 +9,5 @@ export const getAllowedOrigins = async (origin: Cors['origin']): Promise<(string return [origin]; } - if (typeof origin === 'function') { - return getAllowedOrigins(await origin()); - } - throw new Error('Invalid cors origin format'); }; diff --git a/src/cors/noCorsMiddleware/noCorsMiddleware.test.ts b/src/cors/noCorsMiddleware/noCorsMiddleware.test.ts index 73a7a12e..cc3b3b1d 100644 --- a/src/cors/noCorsMiddleware/noCorsMiddleware.test.ts +++ b/src/cors/noCorsMiddleware/noCorsMiddleware.test.ts @@ -4,18 +4,41 @@ import request from 'supertest'; import { noCorsMiddleware } from './noCorsMiddleware'; describe('noCorsMiddleware', () => { - test('Should set no cors settings for request', async () => { + test('Should set no cors settings for OPTIONS preflight request', async () => { const server = express(); noCorsMiddleware(server); - const response = await request(server).get('/'); + + const response = await request(server).options('/'); expect(response.headers).toMatchObject({ + 'access-control-allow-origin': '*', + 'access-control-allow-credentials': 'true', + 'access-control-expose-headers': '*', 'access-control-allow-headers': '*', 'access-control-allow-methods': '*', + 'access-control-max-age': '3600' + }); + expect(response.statusCode).toBe(204); + }); + + test(`Should set no cors settings for request`, async () => { + const server = express(); + + noCorsMiddleware(server); + + const response = await request(server).get('/'); + + expect(response.headers).toMatchObject({ 'access-control-allow-origin': '*', - 'access-control-max-age': '3600', - 'access-control-allow-credentials': 'true' + 'access-control-allow-credentials': 'true', + 'access-control-expose-headers': '*' + }); + + expect(response.headers).not.toMatchObject({ + 'access-control-allow-headers': expect.any(String), + 'access-control-allow-methods': expect.any(String), + 'access-control-max-age': expect.any(String) }); }); }); diff --git a/src/cors/noCorsMiddleware/noCorsMiddleware.ts b/src/cors/noCorsMiddleware/noCorsMiddleware.ts index 2ac42144..652c929a 100644 --- a/src/cors/noCorsMiddleware/noCorsMiddleware.ts +++ b/src/cors/noCorsMiddleware/noCorsMiddleware.ts @@ -2,13 +2,19 @@ import type { Express } from 'express'; import { DEFAULT } from '../../utils/constants'; -export const noCorsMiddleware = async (server: Express) => { - server.use(async (_req, res, next) => { +export const noCorsMiddleware = (server: Express) => { + server.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', DEFAULT.CORS.ORIGIN); - res.setHeader('Access-Control-Allow-Methods', DEFAULT.CORS.METHODS); - res.setHeader('Access-Control-Allow-Headers', DEFAULT.CORS.HEADERS); res.setHeader('Access-Control-Allow-Credentials', `${DEFAULT.CORS.CREDENTIALS}`); - res.setHeader('Access-Control-Max-Age', DEFAULT.CORS.MAX_AGE); + res.setHeader('Access-Control-Expose-Headers', DEFAULT.CORS.EXPOSED_HEADERS); + + if (req.method === 'OPTIONS') { + res.setHeader('Access-Control-Allow-Methods', DEFAULT.CORS.METHODS); + res.setHeader('Access-Control-Allow-Headers', DEFAULT.CORS.ALLOWED_HEADERS); + res.setHeader('Access-Control-Max-Age', DEFAULT.CORS.MAX_AGE); + res.sendStatus(204); + return res.end(); + } return next(); }); diff --git a/src/graphql/createGraphQLRoutes/createGraphQLRoutes.test.ts b/src/graphql/createGraphQLRoutes/createGraphQLRoutes.test.ts new file mode 100644 index 00000000..1ce289b5 --- /dev/null +++ b/src/graphql/createGraphQLRoutes/createGraphQLRoutes.test.ts @@ -0,0 +1,499 @@ +import express from 'express'; +import path from 'path'; +import request from 'supertest'; + +import type { MockServerConfig } from '../../utils/types'; + +import { createGraphQLRoutes } from './createGraphQLRoutes'; + +describe('createGraphQLRoutes', () => { + const createServer = ( + mockServerConfig: Pick + ) => { + const server = express(); + const routerBase = express.Router(); + const routerWithRoutes = createGraphQLRoutes( + routerBase, + mockServerConfig.graphql?.configs ?? [], + mockServerConfig.interceptors + ); + + const graphqlBaseUrl = path.join( + mockServerConfig.baseUrl ?? '/', + mockServerConfig.graphql?.baseUrl ?? '/' + ); + + server.use(express.json()); + server.use(graphqlBaseUrl, routerWithRoutes); + return server; + }; + + test('Should match config by entities "includes" behavior', async () => { + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const postResponse = await request(server) + .post('/') + .send({ query: 'query GetUsers { users { name } }' }) + .set({ key1: 'value1', key2: 'value2' }) + .query({ key1: 'value1', key2: 'value2' }); + + expect(postResponse.statusCode).toBe(200); + expect(postResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + + const getResponse = await request(server) + .get('/') + .set({ key1: 'value1', key2: 'value2' }) + .query({ + query: 'query GetUsers { users { name } }', + key1: 'value1', + key2: 'value2' + }); + + expect(getResponse.statusCode).toBe(200); + expect(getResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + }); + + test('Should return 400 and description text for invalid query', async () => { + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const postResponse = await request(server).post('/').send({ query: 'invalid query' }); + + expect(postResponse.statusCode).toBe(400); + expect(postResponse.body).toBe('Query is invalid, you must use a valid GraphQL query'); + + const getResponse = await request(server).get('/').query({ + query: 'invalid query' + }); + + expect(postResponse.statusCode).toBe(400); + expect(getResponse.body).toBe('Query is invalid, you must use a valid GraphQL query'); + }); + + test('Should match config by entities "includes" behavior with operationName regexp', async () => { + const server = createServer({ + graphql: { + configs: [ + { + operationName: /^Get(.+?)sers$/g, + operationType: 'query', + routes: [ + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const postResponse = await request(server) + .post('/') + .send({ query: 'query GetUsers { users { name } }' }) + .set({ key1: 'value1', key2: 'value2' }) + .query({ key1: 'value1', key2: 'value2' }); + + expect(postResponse.statusCode).toBe(200); + expect(postResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + + const getResponse = await request(server) + .get('/') + .set({ key1: 'value1', key2: 'value2' }) + .query({ + query: 'query GetUsers { users { name } }', + key1: 'value1', + key2: 'value2' + }); + + expect(getResponse.statusCode).toBe(200); + expect(getResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + }); + + test('Should give priority to more specific route config', async () => { + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1', key2: 'value2' } + }, + data: { name: 'John', surname: 'Smith' } + } + ] + } + ] + } + }); + + const postResponse = await request(server) + .post('/') + .send({ query: 'query GetUsers { users { name } }' }) + .set({ key1: 'value1', key2: 'value2' }) + .query({ key1: 'value1', key2: 'value2' }); + + expect(postResponse.statusCode).toBe(200); + expect(postResponse.body).toStrictEqual({ name: 'John', surname: 'Smith' }); + + const getResponse = await request(server) + .get('/') + .set({ key1: 'value1', key2: 'value2' }) + .query({ + query: 'query GetUsers { users { name } }', + key1: 'value1', + key2: 'value2' + }); + + expect(getResponse.statusCode).toBe(200); + expect(getResponse.body).toStrictEqual({ name: 'John', surname: 'Smith' }); + }); + + test('Should return 404 and description text for no matched request configs', async () => { + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + headers: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const postResponse = await request(server) + .post('/') + .send({ query: 'query GetUsers { users { name } }' }) + .set({ key2: 'value2' }); + + expect(postResponse.statusCode).toBe(404); + + const getResponse = await request(server).get('/').set({ key2: 'value2' }).query({ + query: 'query GetUsers { users { name } }' + }); + + expect(getResponse.statusCode).toBe(404); + }); + + test('Should compare non plain object variables by full equal behavior', async () => { + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + variables: [ + { + key1: 'value1', + key2: { nestedKey1: 'nestedValue1' } + } + ] + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const successPostResponse = await request(server) + .post('/') + .set('Content-Type', 'application/json') + .send({ + query: 'query GetUsers { users { name } }', + variables: [{ key1: 'value1', key2: { nestedKey1: 'nestedValue1' } }] + }); + + expect(successPostResponse.statusCode).toBe(200); + expect(successPostResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + + const successGetResponse = await request(server) + .get('/') + .set('Content-Type', 'application/json') + .query({ + query: 'query GetUsers { users { name } }', + variables: '[{ "key1": "value1", "key2": { "nestedKey1": "nestedValue1" } }]' + }); + + expect(successGetResponse.statusCode).toBe(200); + expect(successGetResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + + const failedPostResponse = await request(server) + .post('/') + .set('Content-Type', 'application/json') + .send({ + query: 'query GetUsers { users { name } }', + variables: [ + { key1: 'value1', key2: { nestedKey1: 'nestedValue1', nestedKey2: 'nestedValue2' } } + ] + }); + + expect(failedPostResponse.statusCode).toBe(404); + + const failedGetResponse = await request(server) + .get('/') + .set('Content-Type', 'application/json') + .query({ + query: 'query GetUsers { users { name } }', + variables: + '[{ "key1": "value1", "key2": { "nestedKey1": "nestedValue1", "nestedKey2": "nestedValue2" } }]' + }); + + expect(failedGetResponse.statusCode).toBe(404); + }); + + test('Should compare plain object variables by "includes" behavior', async () => { + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + variables: { + key1: 'value1', + key2: { nestedKey1: 'nestedValue1' } + } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const postResponse = await request(server) + .post('/') + .set('Content-Type', 'application/json') + .send({ + query: 'query GetUsers { users { name } }', + variables: { + key1: 'value1', + key2: { nestedKey1: 'nestedValue1', nestedKey2: 'nestedValue2' } + } + }); + + expect(postResponse.statusCode).toBe(200); + expect(postResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + + const getResponse = await request(server) + .get('/') + .set('Content-Type', 'application/json') + .query({ + query: 'query GetUsers { users { name } }', + variables: + '{ "key1": "value1", "key2": { "nestedKey1": "nestedValue1", "nestedKey2": "nestedValue2" } }' + }); + + expect(getResponse.statusCode).toBe(200); + expect(getResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + }); + + test('Should call request interceptors in order: request -> server', async () => { + const requestInterceptor = jest.fn(); + const serverInterceptor = jest.fn(); + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + variables: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Doe' } + } + ], + interceptors: { request: requestInterceptor } + }, + { + operationName: 'CreateUser', + operationType: 'mutation', + routes: [ + { + entities: { + variables: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Smith' } + } + ] + } + ] + }, + interceptors: { request: serverInterceptor } + }); + + await request(server).get('/').set('Content-Type', 'application/json').query({ + query: 'query GetUsers { users { name } }', + variables: '{ "key1": "value1", "key2": "value2" }' + }); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.invocationCallOrder[0]).toBeLessThan( + serverInterceptor.mock.invocationCallOrder[0] + ); + + await request(server) + .post('/') + .set('Content-Type', 'application/json') + .send({ + query: 'mutation CreateUser($name: String!) { createUser(name: $name) { name } }', + variables: { key1: 'value1', key2: 'value2' } + }); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(2); + }); + + test('Should call response interceptors in order: route -> request -> server', async () => { + const routeInterceptor = jest.fn(); + const requestInterceptor = jest.fn(); + const serverInterceptor = jest.fn(); + const server = createServer({ + graphql: { + configs: [ + { + operationName: 'GetUsers', + operationType: 'query', + routes: [ + { + entities: { + variables: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Doe' }, + interceptors: { response: routeInterceptor } + } + ], + interceptors: { response: requestInterceptor } + }, + { + operationName: 'CreateUser', + operationType: 'mutation', + routes: [ + { + entities: { + variables: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Smith' } + } + ] + } + ] + }, + interceptors: { response: serverInterceptor } + }); + + await request(server).get('/').set('Content-Type', 'application/json').query({ + query: 'query GetUsers { users { name } }', + variables: '{ "key1": "value1", "key2": "value2" }' + }); + expect(routeInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(1); + expect(routeInterceptor.mock.invocationCallOrder[0]).toBeLessThan( + requestInterceptor.mock.invocationCallOrder[0] + ); + expect(requestInterceptor.mock.invocationCallOrder[0]).toBeLessThan( + serverInterceptor.mock.invocationCallOrder[0] + ); + + await request(server) + .post('/') + .set('Content-Type', 'application/json') + .send({ + query: 'mutation CreateUser($name: String!) { createUser(name: $name) { name } }', + variables: { key1: 'value1', key2: 'value2' } + }); + expect(routeInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(2); + + await request(server) + .post('/') + .set('Content-Type', 'application/json') + .send({ + query: 'query GetSettings { settings { notifications } }', + variables: { key1: 'value1', key2: 'value2' } + }); + expect(routeInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(2); + }); +}); diff --git a/src/graphql/createGraphQLRoutes/createGraphQLRoutes.ts b/src/graphql/createGraphQLRoutes/createGraphQLRoutes.ts new file mode 100644 index 00000000..0cae6e61 --- /dev/null +++ b/src/graphql/createGraphQLRoutes/createGraphQLRoutes.ts @@ -0,0 +1,105 @@ +import { IRouter, NextFunction, Request, Response } from 'express'; + +import { isEntityValuesEqual } from '../../configs/isEntitiesEqual/isEntityValuesEqual'; +import { callRequestInterceptors } from '../../routes/callRequestInterceptors/callRequestInterceptors'; +import { callResponseInterceptors } from '../../routes/callResponseInterceptors/callResponseInterceptors'; +import type { + GraphQLEntities, + GraphQLRequestConfig, + Interceptors, + PlainObject, + VariablesValue +} from '../../utils/types'; +import { getGraphQLInput } from '../getGraphQLInput/getGraphQLInput'; +import { parseQuery } from '../parseQuery/parseQuery'; +import { prepareGraphQLRequestConfigs } from '../prepareGraphQLRequestConfigs/prepareGraphQLRequestConfigs'; + +export const createGraphQLRoutes = ( + router: IRouter, + configs: GraphQLRequestConfig[], + interceptors?: Interceptors +) => { + const preparedGraphQLRequestConfig = prepareGraphQLRequestConfigs(configs); + + const graphqlMiddleware = (request: Request, response: Response, next: NextFunction) => { + const graphQLInput = getGraphQLInput(request); + if (!graphQLInput || !graphQLInput.query) { + return response.status(400).json('Query is missing, you must pass a valid GraphQL query'); + } + + const query = parseQuery(graphQLInput.query); + + if (!query) { + return response.status(400).json('Query is invalid, you must use a valid GraphQL query'); + } + + if (!query.operationName || !query.operationType) { + return response + .status(400) + .json( + `You should to specify operationName and operationType for ${request.method}:${request.baseUrl}${request.path}` + ); + } + + const matchedRequestConfig = preparedGraphQLRequestConfig.find((requestConfig) => { + if (requestConfig.operationName instanceof RegExp) { + return ( + new RegExp(requestConfig.operationName).test(query.operationName) && + requestConfig.operationType === query.operationType + ); + } + + return ( + requestConfig.operationName === query.operationName && + requestConfig.operationType === query.operationType + ); + }); + + if (!matchedRequestConfig) { + return next(); + } + + callRequestInterceptors({ + request, + interceptors: { + requestInterceptor: matchedRequestConfig.interceptors?.request, + serverInterceptor: interceptors?.request + } + }); + + const matchedRouteConfig = matchedRequestConfig.routes.find(({ entities }) => { + if (!entities) return true; + return (Object.entries(entities) as [GraphQLEntities, PlainObject | VariablesValue][]).every( + ([entity, entityValue]) => { + if (entity === 'variables') { + return isEntityValuesEqual(entityValue, graphQLInput.variables); + } + + return isEntityValuesEqual(entityValue, request[entity]); + } + ); + }); + + if (!matchedRouteConfig) { + return next(); + } + + const data = callResponseInterceptors({ + data: matchedRouteConfig.data, + request, + response, + interceptors: { + routeInterceptor: matchedRouteConfig.interceptors?.response, + requestInterceptor: matchedRequestConfig.interceptors?.response, + serverInterceptor: interceptors?.response + } + }); + + return response.status(response.statusCode).json(data); + }; + + router.route('/').get(graphqlMiddleware); + router.route('/').post(graphqlMiddleware); + + return router; +}; diff --git a/src/graphql/getGraphQLInput/getGraphQLInput.test.ts b/src/graphql/getGraphQLInput/getGraphQLInput.test.ts new file mode 100644 index 00000000..28a22f6f --- /dev/null +++ b/src/graphql/getGraphQLInput/getGraphQLInput.test.ts @@ -0,0 +1,95 @@ +import type { Request } from 'express'; + +import { getGraphQLInput } from './getGraphQLInput'; + +describe('getGraphQLInput', () => { + test('Should get right graphQL input from GET request', () => { + const mockRequest = { + method: 'GET', + query: { + query: `query GetCharacters { characters { name } }`, + variables: '{"limit": 10}' + } + } as unknown as Request; + + const graphQLInput = getGraphQLInput(mockRequest); + + expect(graphQLInput).toStrictEqual({ + query: 'query GetCharacters { characters { name } }', + variables: { limit: 10 } + }); + }); + + test('Should get right graphQL input from GET request with empty variables', () => { + const mockRequest = { + method: 'GET', + query: { + query: `query GetCharacters { characters { name } }` + } + } as unknown as Request; + + const graphQLInput = getGraphQLInput(mockRequest); + + expect(graphQLInput).toStrictEqual({ + query: 'query GetCharacters { characters { name } }', + variables: {} + }); + }); + + test('Should get right graphQL input from POST request', () => { + const mockRequest = { + method: 'POST', + body: { + query: `query GetCharacters { characters { name } }`, + variables: { limit: 10 } + } + } as unknown as Request; + + const graphQLInput = getGraphQLInput(mockRequest); + + expect(graphQLInput).toStrictEqual({ + query: 'query GetCharacters { characters { name } }', + variables: { limit: 10 } + }); + }); + + test('Should get right graphQL input from POST with empty variables', () => { + const mockRequest = { + method: 'POST', + body: { + query: `query GetCharacters { characters { name } }` + } + } as unknown as Request; + + const graphQLInput = getGraphQLInput(mockRequest); + + expect(graphQLInput).toStrictEqual({ + query: 'query GetCharacters { characters { name } }', + variables: {} + }); + }); + + test('Should get error if request is not GET or POST', () => { + const deleteMockRequest = { + method: 'DELETE', + query: { + query: `query GetCharacters { characters { name } }` + } + } as unknown as Request; + + expect(() => getGraphQLInput(deleteMockRequest)).toThrow( + 'Not allowed request method for graphql request' + ); + + const putMockRequest = { + method: 'PUT', + body: { + query: `query GetCharacters { characters { name } }` + } + } as unknown as Request; + + expect(() => getGraphQLInput(putMockRequest)).toThrow( + 'Not allowed request method for graphql request' + ); + }); +}); diff --git a/src/graphql/getGraphQLInput/getGraphQLInput.ts b/src/graphql/getGraphQLInput/getGraphQLInput.ts new file mode 100644 index 00000000..12d7d169 --- /dev/null +++ b/src/graphql/getGraphQLInput/getGraphQLInput.ts @@ -0,0 +1,25 @@ +import type { Request } from 'express'; + +import type { GraphQLInput } from '../../utils/types/graphql'; + +export const getGraphQLInput = (request: Request): GraphQLInput => { + if (request.method === 'GET') { + const { query, variables } = request.query; + + return { + query: query?.toString(), + variables: JSON.parse((variables as string) ?? '{}') + }; + } + + if (request.method === 'POST') { + const { query, variables } = request.body; + + return { + query, + variables: variables ?? {} + }; + } + + throw new Error('Not allowed request method for graphql request'); +}; diff --git a/src/graphql/parseGraphQLRequest/parseGraphQLRequest.ts b/src/graphql/parseGraphQLRequest/parseGraphQLRequest.ts new file mode 100644 index 00000000..c3d444c2 --- /dev/null +++ b/src/graphql/parseGraphQLRequest/parseGraphQLRequest.ts @@ -0,0 +1,11 @@ +import { Request } from 'express'; + +import { getGraphQLInput } from '../getGraphQLInput/getGraphQLInput'; +import { parseQuery } from '../parseQuery/parseQuery'; + +export const parseGraphQLRequest = (request: Request): ReturnType => { + const graphQLInput = getGraphQLInput(request); + if (!graphQLInput.query) return null; + + return parseQuery(graphQLInput.query); +}; diff --git a/src/graphql/parseQuery/parseQuery.test.ts b/src/graphql/parseQuery/parseQuery.test.ts new file mode 100644 index 00000000..a08c72ac --- /dev/null +++ b/src/graphql/parseQuery/parseQuery.test.ts @@ -0,0 +1,32 @@ +import { parseQuery } from './parseQuery'; + +describe('parseQuery', () => { + test('Should parse graphQL query', () => { + const parsedQuery = parseQuery('query GetCharacters { characters { name } }'); + + expect(parsedQuery).toStrictEqual({ + operationType: 'query', + operationName: 'GetCharacters' + }); + }); + + test('Should parse graphQL mutation', () => { + const parsedQuery = parseQuery( + 'mutation CreateCharacters($name: String!) { createCharacters(name: $name) { name } }' + ); + + expect(parsedQuery).toStrictEqual({ + operationType: 'mutation', + operationName: 'CreateCharacters' + }); + }); + + test('Should parse graphQL query with empty operationName', () => { + const parsedQuery = parseQuery('query { characters { name } }'); + + expect(parsedQuery).toStrictEqual({ + operationType: 'query', + operationName: '' + }); + }); +}); diff --git a/src/graphql/parseQuery/parseQuery.ts b/src/graphql/parseQuery/parseQuery.ts new file mode 100644 index 00000000..564009e5 --- /dev/null +++ b/src/graphql/parseQuery/parseQuery.ts @@ -0,0 +1,27 @@ +import type { DocumentNode, OperationDefinitionNode, OperationTypeNode } from 'graphql'; +import { parse } from 'graphql'; + +interface ParseDocumentNodeResult { + operationType: OperationTypeNode; + operationName: string; +} + +const parseDocumentNode = (node: DocumentNode): ParseDocumentNodeResult => { + const operationDefinition = node.definitions.find( + (definition) => definition.kind === 'OperationDefinition' + ) as OperationDefinitionNode; + + return { + operationType: operationDefinition.operation, + operationName: operationDefinition.name?.value ?? '' + }; +}; + +export const parseQuery = (query: string) => { + try { + const document = parse(query); + return parseDocumentNode(document); + } catch { + return null; + } +}; diff --git a/src/graphql/prepareGraphQLRequestConfigs/prepareGraphQLRequestConfigs.test.ts b/src/graphql/prepareGraphQLRequestConfigs/prepareGraphQLRequestConfigs.test.ts new file mode 100644 index 00000000..34bf79e3 --- /dev/null +++ b/src/graphql/prepareGraphQLRequestConfigs/prepareGraphQLRequestConfigs.test.ts @@ -0,0 +1,160 @@ +import type { GraphQLRequestConfig } from '../../utils/types'; + +import { prepareGraphQLRequestConfigs } from './prepareGraphQLRequestConfigs'; + +describe('prepareGraphQLRequestConfigs', () => { + test('Should not sort routes if they does not contain entities', () => { + const GraphQLRequestConfigs: GraphQLRequestConfig[] = [ + { + operationName: 'GetUser', + operationType: 'query', + routes: [ + { + data: { name: 'John', surname: 'Doe' } + }, + { + data: { name: 'John', surname: 'Smith' } + }, + { + data: { name: 'John', surname: 'John' } + } + ] + } + ]; + expect(prepareGraphQLRequestConfigs(GraphQLRequestConfigs)).toStrictEqual( + GraphQLRequestConfigs + ); + }); + + test('Should sort routes by their specificity of entities', () => { + const GraphQLRequestConfigs: GraphQLRequestConfig[] = [ + { + operationName: 'GetUser', + operationType: 'query', + routes: [ + { + entities: { + headers: { + header1: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + headers: { + header1: 'value', + header2: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + headers: { + header1: 'value' + }, + query: { + query1: 'value', + query2: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ]; + const expectedGraphQLRequestConfigs: GraphQLRequestConfig[] = [ + { + operationName: 'GetUser', + operationType: 'query', + routes: [ + { + entities: { + headers: { + header1: 'value' + }, + query: { + query1: 'value', + query2: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + headers: { + header1: 'value', + header2: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + headers: { + header1: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ]; + expect(prepareGraphQLRequestConfigs(GraphQLRequestConfigs)).toStrictEqual( + expectedGraphQLRequestConfigs + ); + }); + + test('Should set not object variables weight equals to one', () => { + const GraphQLRequestConfigs: GraphQLRequestConfig[] = [ + { + operationName: 'GetUser', + operationType: 'query', + routes: [ + { + entities: { + variables: ['value', 'value', 'value'] + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + headers: { + header1: 'value', + header2: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ]; + const expectedGraphQLRequestConfigs: GraphQLRequestConfig[] = [ + { + operationName: 'GetUser', + operationType: 'query', + routes: [ + { + entities: { + headers: { + header1: 'value', + header2: 'value' + } + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + variables: ['value', 'value', 'value'] + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ]; + expect(prepareGraphQLRequestConfigs(GraphQLRequestConfigs)).toStrictEqual( + expectedGraphQLRequestConfigs + ); + }); +}); diff --git a/src/graphql/prepareGraphQLRequestConfigs/prepareGraphQLRequestConfigs.ts b/src/graphql/prepareGraphQLRequestConfigs/prepareGraphQLRequestConfigs.ts new file mode 100644 index 00000000..77e8aed3 --- /dev/null +++ b/src/graphql/prepareGraphQLRequestConfigs/prepareGraphQLRequestConfigs.ts @@ -0,0 +1,27 @@ +import { isPlainObject } from '../../utils/helpers'; +import type { GraphQLRequestConfig, GraphQLRouteConfig } from '../../utils/types'; + +const calculateRouteConfigWeight = (graphQLRouteConfig: GraphQLRouteConfig) => { + const { entities } = graphQLRouteConfig; + if (!entities) return 0; + + let routeConfigWeight = 0; + const { headers, query, variables } = entities; + if (headers) routeConfigWeight += Object.keys(headers).length; + if (query) routeConfigWeight += Object.keys(query).length; + if (variables) routeConfigWeight += isPlainObject(variables) ? Object.keys(variables).length : 1; + + return routeConfigWeight; +}; + +export const prepareGraphQLRequestConfigs = (requestConfigs: GraphQLRequestConfig[]) => { + requestConfigs.forEach((requestConfig) => { + requestConfig.routes.sort( + (first, second) => + // ✅ important: + // Lift more specific configs for correct working of routes + calculateRouteConfigWeight(second) - calculateRouteConfigWeight(first) + ); + }); + return requestConfigs; +}; diff --git a/src/notFound/notFoundMiddleware.test.ts b/src/notFound/notFoundMiddleware.test.ts new file mode 100644 index 00000000..10ea8a09 --- /dev/null +++ b/src/notFound/notFoundMiddleware.test.ts @@ -0,0 +1,120 @@ +import express from 'express'; +import path from 'path'; +import request from 'supertest'; + +import { createGraphQLRoutes } from '../graphql/createGraphQLRoutes/createGraphQLRoutes'; +import { createRestRoutes } from '../rest/createRestRoutes/createRestRoutes'; +import type { MockServerConfig } from '../utils/types'; + +import { notFoundMiddleware } from './notFoundMiddleware'; + +describe('notFoundMiddleware', () => { + const baseUrl: MockServerConfig['baseUrl'] = '/base'; + + const rest: MockServerConfig['rest'] = { + baseUrl: '/rest', + configs: [ + { + path: '/posts', + method: 'get', + routes: [{ data: {} }] + }, + { + path: '/posts/:postId', + method: 'get', + routes: [{ data: {}, entities: { params: { postId: 1 } } }] + }, + + { + path: '/developers', + method: 'get', + routes: [{ data: {} }] + }, + { + path: '/developers/:developerId', + method: 'get', + routes: [{ data: {}, entities: { params: { developerId: 1 } } }] + } + ] + }; + + const graphql: MockServerConfig['graphql'] = { + baseUrl: '/graphql', + configs: [ + { + operationName: 'GetPosts', + operationType: 'query', + routes: [{ data: {} }] + }, + { + operationName: 'GetDevelopers', + operationType: 'query', + routes: [{ data: {} }] + } + ] + }; + + const createServer = ( + mockServerConfig: Pick + ) => { + const { baseUrl, rest, interceptors, graphql } = mockServerConfig; + + const server = express(); + + const serverBaseUrl = baseUrl ?? '/'; + + const restBaseUrl = path.join(serverBaseUrl, rest?.baseUrl ?? '/'); + const routerWithRestRoutes = createRestRoutes( + express.Router(), + rest?.configs ?? [], + interceptors + ); + server.use(restBaseUrl, routerWithRestRoutes); + + const graphqlBaseUrl = path.join(serverBaseUrl, graphql?.baseUrl ?? '/'); + const routerWithGraphqlRoutes = createGraphQLRoutes( + express.Router(), + graphql?.configs ?? [], + interceptors + ); + server.use(graphqlBaseUrl, routerWithGraphqlRoutes); + + server.set('view engine', 'ejs'); + server.use(express.json()); + + notFoundMiddleware({ + server, + mockServerConfig + }); + + return server; + }; + + test('Should send correct REST suggestions', async () => { + const server = createServer({ + baseUrl, + rest, + graphql + }); + + const response = await request(server).get('/bas/rst/pstss'); + + expect(response.statusCode).toBe(404); + expect(response.text).toContain('

REST

'); + expect(response.text).toContain('/base/rest/posts'); + }); + + test('Should send correct GraphQL suggestions', async () => { + const server = createServer({ + baseUrl, + rest, + graphql + }); + + const response = await request(server).get('/bse/graql?query=query posts { posts }'); + + expect(response.statusCode).toBe(404); + expect(response.text).toContain('

GraphQL

'); + expect(response.text).toContain('/base/graphql/GetPosts'); + }); +}); diff --git a/src/notFound/notFoundMiddleware.ts b/src/notFound/notFoundMiddleware.ts new file mode 100644 index 00000000..4d1f23bd --- /dev/null +++ b/src/notFound/notFoundMiddleware.ts @@ -0,0 +1,64 @@ +import type { Express, Request, Response } from 'express'; + +import { parseGraphQLRequest } from '../graphql/parseGraphQLRequest/parseGraphQLRequest'; +import type { MockServerConfig, RestMethod } from '../utils/types'; + +import { getGraphqlUrlSuggestions, getRestUrlSuggestions } from './urlSuggestions'; + +interface NotFoundMiddlewareParams { + server: Express; + mockServerConfig: Pick; +} + +export const notFoundMiddleware = ({ server, mockServerConfig }: NotFoundMiddlewareParams) => { + const { baseUrl: serverBaseUrl, rest, graphql } = mockServerConfig; + + const operationNames = graphql?.configs.map(({ operationName }) => operationName) ?? []; + const graphqlPatternUrlMeaningfulStrings = Array.from( + operationNames.reduce((acc, operationName) => { + if (typeof operationName === 'string') + acc.add(`${serverBaseUrl}${graphql?.baseUrl}/${operationName}`); + return acc; + }, new Set()) + ); + + const restPaths = rest?.configs.map(({ path }) => path) ?? []; + const patternUrls = Array.from( + restPaths.reduce((acc, patternPath) => { + if (typeof patternPath === 'string') + acc.add(`${serverBaseUrl}${rest?.baseUrl}${patternPath}`); + return acc; + }, new Set()) + ); + + server.use((request: Request, response: Response) => { + const url = new URL(`${request.protocol}://${request.get('host')}${request.originalUrl}`); + + let graphqlUrlSuggestions: string[] = []; + if (graphql) { + const graphqlQuery = parseGraphQLRequest(request); + + if (graphqlQuery) { + graphqlUrlSuggestions = getGraphqlUrlSuggestions({ + url, + graphqlPatternUrlMeaningfulStrings + }); + } + } + + let restUrlSuggestions: string[] = []; + if (rest) { + restUrlSuggestions = getRestUrlSuggestions({ + url, + patternUrls + }); + } + + response.status(404).render('notFound', { + requestMethod: request.method as RestMethod, + url: `${url.pathname}${url.search}`, + restUrlSuggestions, + graphqlUrlSuggestions + }); + }); +}; diff --git a/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/getGraphqlUrlSuggestions.test.ts b/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/getGraphqlUrlSuggestions.test.ts new file mode 100644 index 00000000..37eaa628 --- /dev/null +++ b/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/getGraphqlUrlSuggestions.test.ts @@ -0,0 +1,20 @@ +import { getGraphqlUrlSuggestions } from './getGraphqlUrlSuggestions'; + +describe('getGraphqlUrlSuggestions', () => { + test('Should correctly return suggestions', () => { + const graphqlPatternUrlMeaningfulStrings: string[] = ['/GetDevelopers', '/CreateDeveloper']; + expect( + getGraphqlUrlSuggestions({ + url: new URL('http://localhost:31299/?query=query%20Getdevoper%20{%20developers%20}'), + graphqlPatternUrlMeaningfulStrings + }) + ).toEqual(['/GetDevelopers', '/CreateDeveloper']); + + expect( + getGraphqlUrlSuggestions({ + url: new URL('http://localhost:31299/base/re/pos?query=query%20devel%20{%20developers%20}'), + graphqlPatternUrlMeaningfulStrings + }) + ).toEqual([]); + }); +}); diff --git a/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/getGraphqlUrlSuggestions.ts b/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/getGraphqlUrlSuggestions.ts new file mode 100644 index 00000000..f2b293d2 --- /dev/null +++ b/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/getGraphqlUrlSuggestions.ts @@ -0,0 +1,32 @@ +import { getLevenshteinDistance } from '../getLevenshteinDistance/getLevenshteinDistance'; + +interface GetGraphqlUrlSuggestionsParams { + url: URL; + graphqlPatternUrlMeaningfulStrings: string[]; +} + +export const getGraphqlUrlSuggestions = ({ + url, + graphqlPatternUrlMeaningfulStrings +}: GetGraphqlUrlSuggestionsParams) => { + // ✅ important: operationName is always second word in 'query' query param + const operationName = url.searchParams.get('query')?.split(' ')[1]; + const actualUrlMeaningfulString = `${url.pathname}/${operationName}`; + + const graphqlUrlSuggestions = graphqlPatternUrlMeaningfulStrings.reduce( + (acc, patternUrlMeaningfulString) => { + const distance = getLevenshteinDistance( + actualUrlMeaningfulString, + patternUrlMeaningfulString + ); + + const tolerance = Math.floor(patternUrlMeaningfulString.length / 2); + if (distance <= tolerance) acc.push(patternUrlMeaningfulString); + + return acc; + }, + [] as string[] + ); + + return graphqlUrlSuggestions; +}; diff --git a/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/index.ts b/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/index.ts new file mode 100644 index 00000000..b865e6ca --- /dev/null +++ b/src/notFound/urlSuggestions/getGraphqlUrlSuggestions/index.ts @@ -0,0 +1 @@ +export * from './getGraphqlUrlSuggestions'; diff --git a/src/notFound/urlSuggestions/getLevenshteinDistance/getLevenshteinDistance.test.ts b/src/notFound/urlSuggestions/getLevenshteinDistance/getLevenshteinDistance.test.ts new file mode 100644 index 00000000..2bd49a68 --- /dev/null +++ b/src/notFound/urlSuggestions/getLevenshteinDistance/getLevenshteinDistance.test.ts @@ -0,0 +1,12 @@ +import { getLevenshteinDistance } from './getLevenshteinDistance'; + +describe('getLevenshteinDistance', () => { + test('Should correct return Levenshtein distance', () => { + expect(getLevenshteinDistance('users', 'users')).toEqual(0); + expect(getLevenshteinDistance('psts', 'posts')).toEqual(1); + expect(getLevenshteinDistance('postss', 'posts')).toEqual(1); + expect(getLevenshteinDistance('users', 'Users')).toEqual(1); + expect(getLevenshteinDistance('1234', '1234567')).toEqual(3); + expect(getLevenshteinDistance('', '1234')).toEqual(4); + }); +}); diff --git a/src/notFound/urlSuggestions/getLevenshteinDistance/getLevenshteinDistance.ts b/src/notFound/urlSuggestions/getLevenshteinDistance/getLevenshteinDistance.ts new file mode 100644 index 00000000..bc16e3ab --- /dev/null +++ b/src/notFound/urlSuggestions/getLevenshteinDistance/getLevenshteinDistance.ts @@ -0,0 +1,79 @@ +/* eslint-disable */ + +const min = (d0: number, d1: number, d2: number, bx: number, ay: number) => + d0 < d1 || d2 < d1 ? (d0 > d2 ? d2 + 1 : d0 + 1) : bx === ay ? d1 : d1 + 1; + +export const getLevenshteinDistance = (a: string, b: string) => { + if (a === b) { + return 0; + } + if (a.length > b.length) { + const tmp = a; + a = b; + b = tmp; + } + let la = a.length; + let lb = b.length; + while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) { + la--; + lb--; + } + let offset = 0; + while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) { + offset++; + } + la -= offset; + lb -= offset; + if (la === 0 || lb < 3) { + return lb; + } + let x = 0; + let y: number; + let d0: number; + let d1: number; + let d2: number; + let d3: number; + let dd = 0; + let dy: number; + let ay: number; + let bx0: number; + let bx1: number; + let bx2: number; + let bx3: number; + const vector = []; + for (y = 0; y < la; y++) { + vector.push(y + 1); + vector.push(a.charCodeAt(offset + y)); + } + const len = vector.length - 1; + for (; x < lb - 3; ) { + bx0 = b.charCodeAt(offset + (d0 = x)); + bx1 = b.charCodeAt(offset + (d1 = x + 1)); + bx2 = b.charCodeAt(offset + (d2 = x + 2)); + bx3 = b.charCodeAt(offset + (d3 = x + 3)); + dd = x += 4; + for (y = 0; y < len; y += 2) { + dy = vector[y]; + ay = vector[y + 1]; + d0 = min(dy, d0, d1, bx0, ay); + d1 = min(d0, d1, d2, bx1, ay); + d2 = min(d1, d2, d3, bx2, ay); + dd = min(d2, d3, dd, bx3, ay); + vector[y] = dd; + d3 = d2; + d2 = d1; + d1 = d0; + d0 = dy; + } + } + for (; x < lb; ) { + bx0 = b.charCodeAt(offset + (d0 = x)); + dd = ++x; + for (y = 0; y < len; y += 2) { + dy = vector[y]; + vector[y] = dd = min(dy, d0, dd, bx0, vector[y + 1]); + d0 = dy; + } + } + return dd; +}; diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/getRestUrlSuggestions.test.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/getRestUrlSuggestions.test.ts new file mode 100644 index 00000000..1e646396 --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/getRestUrlSuggestions.test.ts @@ -0,0 +1,64 @@ +import { getRestUrlSuggestions } from './getRestUrlSuggestions'; + +describe('getRestUrlSuggestions', () => { + test('Should correctly return suggestions', () => { + const patternUrls: string[] = [ + '/posts', + '/posts/:postId', + '/posts/:postId/comments/:commentId' + ]; + expect( + getRestUrlSuggestions({ + url: new URL('http://localhost:31299/posts/5/comments/2'), + patternUrls + }) + ).toEqual(['/posts/5/comments/2']); + expect( + getRestUrlSuggestions({ + url: new URL('http://localhost:31299/psts/5/commennts/2'), + patternUrls + }) + ).toEqual(['/posts/5/comments/2']); + expect( + getRestUrlSuggestions({ + url: new URL('http://localhost:31299/post/5/omments/2'), + patternUrls + }) + ).toEqual(['/posts/5/comments/2']); + expect( + getRestUrlSuggestions({ + url: new URL('http://localhost:31299/ps/5/cots/2'), + patternUrls + }) + ).toEqual([]); + }); + + test('Should return patterns with same query params as provided', () => { + const patternUrls: string[] = [ + '/users', + '/users/:userId', + '/user', + '/comments', + '/login', + '/logout' + ]; + expect( + getRestUrlSuggestions({ + url: new URL('http://localhost:31299/login?remember=true'), + patternUrls + }) + ).toEqual(['/login?remember=true', '/logout?remember=true']); + expect( + getRestUrlSuggestions({ + url: new URL('http://localhost:31299/users/5?firstParam=1&secondParam=2'), + patternUrls + }) + ).toEqual(['/users/5?firstParam=1&secondParam=2']); + expect( + getRestUrlSuggestions({ + url: new URL('http://localhost:31299/users/5?backurl=someUrl?action=success'), + patternUrls + }) + ).toEqual(['/users/5?backurl=someUrl?action=success']); + }); +}); diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/getRestUrlSuggestions.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/getRestUrlSuggestions.ts new file mode 100644 index 00000000..d96d71c0 --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/getRestUrlSuggestions.ts @@ -0,0 +1,45 @@ +import { getUrlParts } from '../../../utils/helpers'; +import { getLevenshteinDistance } from '../getLevenshteinDistance/getLevenshteinDistance'; + +import { getActualRestUrlMeaningfulString, getPatternRestUrlMeaningfulString } from './helpers'; + +interface GetRestUrlSuggestionsParams { + url: URL; + patternUrls: string[]; +} + +export const getRestUrlSuggestions = ({ url, patternUrls }: GetRestUrlSuggestionsParams) => { + const actualUrlParts = getUrlParts(url.pathname); + + const restUrlSuggestions = patternUrls.reduce((acc, patternUrl) => { + const patternUrlParts = getUrlParts(patternUrl); + // ✅ important: ignore patterns with different amount of parts + if (patternUrlParts.length !== actualUrlParts.length) return acc; + + const actualUrlMeaningfulString = getActualRestUrlMeaningfulString( + actualUrlParts, + patternUrlParts + ); + const patternUrlMeaningfulString = getPatternRestUrlMeaningfulString(patternUrlParts); + + const tolerance = Math.floor(patternUrlMeaningfulString.length / 2); + const distance = getLevenshteinDistance(actualUrlMeaningfulString, patternUrlMeaningfulString); + + if (distance <= tolerance) { + // replace param names in pattern with param values from actual url + const urlSuggestion = patternUrlParts + .map((_patternUrlPart, index) => { + if (patternUrlParts[index].startsWith(':')) return actualUrlParts[index]; + return patternUrlParts[index]; + }) + .join('/'); + const suggestionWithQueryParams = `/${urlSuggestion}${url.search}`; + + acc.push(suggestionWithQueryParams); + } + + return acc; + }, [] as string[]); + + return restUrlSuggestions; +}; diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getActualRestUrlMeaningfulString/getActualRestUrlMeaningfulString.test.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getActualRestUrlMeaningfulString/getActualRestUrlMeaningfulString.test.ts new file mode 100644 index 00000000..e1400119 --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getActualRestUrlMeaningfulString/getActualRestUrlMeaningfulString.test.ts @@ -0,0 +1,12 @@ +import { getActualRestUrlMeaningfulString } from './getActualRestUrlMeaningfulString'; + +describe('getActualRestUrlMeaningfulString', () => { + test('Should correctly return actual rest meaningful string', () => { + expect( + getActualRestUrlMeaningfulString( + ['base', 'rest', 'posts', '2', 'comments', '5'], + ['base', 'rest', 'posts', ':postId', 'comments', ':commentId'] + ) + ).toEqual('baserestpostscomments'); + }); +}); diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getActualRestUrlMeaningfulString/getActualRestUrlMeaningfulString.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getActualRestUrlMeaningfulString/getActualRestUrlMeaningfulString.ts new file mode 100644 index 00000000..3086b77d --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getActualRestUrlMeaningfulString/getActualRestUrlMeaningfulString.ts @@ -0,0 +1,7 @@ +export const getActualRestUrlMeaningfulString = ( + actualUrlParts: string[], + patternUrlParts: string[] +) => + actualUrlParts + .filter((_actualUrlPart, index) => !patternUrlParts[index].startsWith(':')) + .join(''); diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getPatternRestUrlMeaningfulString/getPatternRestUrlMeaningfulString.test.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getPatternRestUrlMeaningfulString/getPatternRestUrlMeaningfulString.test.ts new file mode 100644 index 00000000..8e332034 --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getPatternRestUrlMeaningfulString/getPatternRestUrlMeaningfulString.test.ts @@ -0,0 +1,10 @@ +import { getPatternRestUrlMeaningfulString } from './getPatternRestUrlMeaningfulString'; + +describe('getPatternRestUrlMeaningfulString', () => { + test('Should correct return rest url pattern meaningful string', () => { + expect( + getPatternRestUrlMeaningfulString(['rest', 'posts', ':postId', 'comments', ':commentId']) + ).toEqual('restpostscomments'); + expect(getPatternRestUrlMeaningfulString(['users'])).toEqual('users'); + }); +}); diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getPatternRestUrlMeaningfulString/getPatternRestUrlMeaningfulString.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getPatternRestUrlMeaningfulString/getPatternRestUrlMeaningfulString.ts new file mode 100644 index 00000000..e71d7b13 --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/getPatternRestUrlMeaningfulString/getPatternRestUrlMeaningfulString.ts @@ -0,0 +1,2 @@ +export const getPatternRestUrlMeaningfulString = (patternUrlParts: string[]) => + patternUrlParts.filter((urlPatternPart) => !urlPatternPart.startsWith(':')).join(''); diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/index.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/index.ts new file mode 100644 index 00000000..58c49cd7 --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './getActualRestUrlMeaningfulString/getActualRestUrlMeaningfulString'; +export * from './getPatternRestUrlMeaningfulString/getPatternRestUrlMeaningfulString'; diff --git a/src/notFound/urlSuggestions/getRestUrlSuggestions/index.ts b/src/notFound/urlSuggestions/getRestUrlSuggestions/index.ts new file mode 100644 index 00000000..7f9e48eb --- /dev/null +++ b/src/notFound/urlSuggestions/getRestUrlSuggestions/index.ts @@ -0,0 +1 @@ +export * from './getRestUrlSuggestions'; diff --git a/src/notFound/urlSuggestions/index.ts b/src/notFound/urlSuggestions/index.ts new file mode 100644 index 00000000..e2704779 --- /dev/null +++ b/src/notFound/urlSuggestions/index.ts @@ -0,0 +1,2 @@ +export * from './getGraphqlUrlSuggestions'; +export * from './getRestUrlSuggestions'; diff --git a/src/rest/createRestRoutes/createRestRoutes.test.ts b/src/rest/createRestRoutes/createRestRoutes.test.ts new file mode 100644 index 00000000..061961e6 --- /dev/null +++ b/src/rest/createRestRoutes/createRestRoutes.test.ts @@ -0,0 +1,358 @@ +import express from 'express'; +import path from 'path'; +import request from 'supertest'; + +import type { MockServerConfig } from '../../utils/types'; + +import { createRestRoutes } from './createRestRoutes'; + +describe('createRestRoutes', () => { + const createServer = ( + mockServerConfig: Pick + ) => { + const server = express(); + const routerBase = express.Router(); + const routerWithRoutes = createRestRoutes( + routerBase, + mockServerConfig.rest?.configs ?? [], + mockServerConfig.interceptors + ); + + const restBaseUrl = path.join( + mockServerConfig.baseUrl ?? '/', + mockServerConfig.rest?.baseUrl ?? '/' + ); + + server.use(express.json()); + server.use(restBaseUrl, routerWithRoutes); + return server; + }; + + test('Should match config by entities "includes" behavior', async () => { + const server = createServer({ + rest: { + configs: [ + { + path: '/users', + method: 'get', + routes: [ + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const response = await request(server) + .get('/users') + .set({ key1: 'value1', key2: 'value2' }) + .query({ key1: 'value1', key2: 'value2' }); + + expect(response.statusCode).toBe(200); + expect(response.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + }); + + test('Should match config by entities "includes" behavior with path regexp', async () => { + const server = createServer({ + rest: { + configs: [ + { + path: /^\/us(.+?)rs$/, + method: 'get', + routes: [ + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const response = await request(server) + .get('/users') + .set({ key1: 'value1', key2: 'value2' }) + .query({ key1: 'value1', key2: 'value2' }); + + expect(response.statusCode).toBe(200); + expect(response.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + }); + + test('Should give priority to more specific route config', async () => { + const server = createServer({ + rest: { + configs: [ + { + path: '/users', + method: 'get', + routes: [ + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + }, + { + entities: { + headers: { key1: 'value1', key2: 'value2' }, + query: { key1: 'value1', key2: 'value2' } + }, + data: { name: 'John', surname: 'Smith' } + } + ] + } + ] + } + }); + + const response = await request(server) + .get('/users') + .set({ key1: 'value1', key2: 'value2' }) + .query({ key1: 'value1', key2: 'value2' }); + + expect(response.statusCode).toBe(200); + expect(response.body).toStrictEqual({ name: 'John', surname: 'Smith' }); + }); + + test('Should return 404 and description text for no matched request configs', async () => { + const server = createServer({ + rest: { + configs: [ + { + path: '/users', + method: 'get', + routes: [ + { + entities: { + headers: { key1: 'value1' } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const response = await request(server).get('/users').set({ key2: 'value2' }); + + expect(response.statusCode).toBe(404); + }); + + test('Should compare non plain object body by full equal behavior', async () => { + const server = createServer({ + rest: { + configs: [ + { + path: '/users', + method: 'post', + routes: [ + { + entities: { + body: [ + { + key1: 'value1', + key2: { nestedKey1: 'nestedValue1' } + } + ] + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const successResponse = await request(server) + .post('/users') + .set('Content-Type', 'application/json') + .send([{ key1: 'value1', key2: { nestedKey1: 'nestedValue1' } }]); + expect(successResponse.statusCode).toBe(200); + expect(successResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + + const failedResponse = await request(server) + .post('/users') + .set('Content-Type', 'application/json') + .send([{ key1: 'value1', key2: { nestedKey1: 'nestedValue1', nestedKey2: 'nestedValue2' } }]); + expect(failedResponse.statusCode).toBe(404); + }); + + test('Should compare plain object body by "includes" behavior', async () => { + const server = createServer({ + rest: { + configs: [ + { + path: '/users', + method: 'post', + routes: [ + { + entities: { + body: { + key1: 'value1', + key2: { nestedKey1: 'nestedValue1' } + } + }, + data: { name: 'John', surname: 'Doe' } + } + ] + } + ] + } + }); + + const response = await request(server) + .post('/users') + .set('Content-Type', 'application/json') + .send({ key1: 'value1', key2: { nestedKey1: 'nestedValue1', nestedKey2: 'nestedValue2' } }); + + expect(response.statusCode).toBe(200); + expect(response.body).toStrictEqual({ name: 'John', surname: 'Doe' }); + }); + + test('Should call request interceptors in order: request -> server', async () => { + const requestInterceptor = jest.fn(); + const serverInterceptor = jest.fn(); + const server = createServer({ + rest: { + configs: [ + { + path: '/users', + method: 'post', + routes: [ + { + entities: { + body: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Doe' } + } + ], + interceptors: { request: requestInterceptor } + }, + { + path: '/settings', + method: 'post', + routes: [ + { + entities: { + body: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Smith' } + } + ] + } + ] + }, + interceptors: { request: serverInterceptor } + }); + + await request(server) + .post('/users') + .set('Content-Type', 'application/json') + .send({ key1: 'value1', key2: 'value2' }); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.invocationCallOrder[0]).toBeLessThan( + serverInterceptor.mock.invocationCallOrder[0] + ); + + await request(server) + .post('/settings') + .set('Content-Type', 'application/json') + .send({ key1: 'value1', key2: 'value2' }); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(2); + }); + + test('Should call response interceptors in order: route -> request -> server', async () => { + const routeInterceptor = jest.fn(); + const requestInterceptor = jest.fn(); + const serverInterceptor = jest.fn(); + const server = createServer({ + rest: { + configs: [ + { + path: '/users', + method: 'post', + routes: [ + { + entities: { + body: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Doe' }, + interceptors: { response: routeInterceptor } + } + ], + interceptors: { response: requestInterceptor } + }, + { + path: '/settings', + method: 'post', + routes: [ + { + entities: { + body: { + key1: 'value1', + key2: 'value2' + } + }, + data: { name: 'John', surname: 'Smith' } + } + ] + } + ] + }, + interceptors: { response: serverInterceptor } + }); + + await request(server) + .post('/users') + .set('Content-Type', 'application/json') + .send({ key1: 'value1', key2: 'value2' }); + expect(routeInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(1); + expect(routeInterceptor.mock.invocationCallOrder[0]).toBeLessThan( + requestInterceptor.mock.invocationCallOrder[0] + ); + expect(requestInterceptor.mock.invocationCallOrder[0]).toBeLessThan( + serverInterceptor.mock.invocationCallOrder[0] + ); + + await request(server) + .post('/settings') + .set('Content-Type', 'application/json') + .send({ key1: 'value1', key2: 'value2' }); + expect(routeInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(2); + + await request(server) + .post('/messages') + .set('Content-Type', 'application/json') + .send({ key1: 'value1', key2: 'value2' }); + expect(routeInterceptor.mock.calls.length).toBe(1); + expect(requestInterceptor.mock.calls.length).toBe(1); + expect(serverInterceptor.mock.calls.length).toBe(2); + }); +}); diff --git a/src/rest/createRestRoutes/createRestRoutes.ts b/src/rest/createRestRoutes/createRestRoutes.ts new file mode 100644 index 00000000..b7199b57 --- /dev/null +++ b/src/rest/createRestRoutes/createRestRoutes.ts @@ -0,0 +1,55 @@ +import { IRouter } from 'express'; + +import { isEntityValuesEqual } from '../../configs/isEntitiesEqual/isEntityValuesEqual'; +import { callRequestInterceptors } from '../../routes/callRequestInterceptors/callRequestInterceptors'; +import { callResponseInterceptors } from '../../routes/callResponseInterceptors/callResponseInterceptors'; +import type { + Interceptors, + RestEntities, + RestEntitiesValue, + RestRequestConfig +} from '../../utils/types'; +import { prepareRestRequestConfigs } from '../prepareRestRequestConfigs/prepareRestRequestConfigs'; + +export const createRestRoutes = ( + router: IRouter, + configs: RestRequestConfig[], + interceptors?: Interceptors +) => { + prepareRestRequestConfigs(configs).forEach((requestConfig) => { + router.route(requestConfig.path)[requestConfig.method]((request, response, next) => { + callRequestInterceptors({ + request, + interceptors: { + requestInterceptor: requestConfig.interceptors?.request, + serverInterceptor: interceptors?.request + } + }); + + const matchedRouteConfig = requestConfig.routes.find(({ entities }) => { + if (!entities) return true; + return (Object.entries(entities) as [RestEntities, RestEntitiesValue][]).every( + ([entity, entityValue]) => isEntityValuesEqual(entityValue, request[entity]) + ); + }); + + if (!matchedRouteConfig) { + return next(); + } + + const data = callResponseInterceptors({ + data: matchedRouteConfig.data, + request, + response, + interceptors: { + routeInterceptor: matchedRouteConfig.interceptors?.response, + requestInterceptor: requestConfig.interceptors?.response, + serverInterceptor: interceptors?.response + } + }); + return response.status(response.statusCode).json(data); + }); + }); + + return router; +}; diff --git a/src/configs/prepareRequestConfigs/prepareRequestConfigs.test.ts b/src/rest/prepareRestRequestConfigs/prepareRestRequestConfigs.test.ts similarity index 80% rename from src/configs/prepareRequestConfigs/prepareRequestConfigs.test.ts rename to src/rest/prepareRestRequestConfigs/prepareRestRequestConfigs.test.ts index fc39ce3b..22212954 100644 --- a/src/configs/prepareRequestConfigs/prepareRequestConfigs.test.ts +++ b/src/rest/prepareRestRequestConfigs/prepareRestRequestConfigs.test.ts @@ -1,10 +1,10 @@ -import type { RequestConfig } from '../../utils/types'; +import type { RestRequestConfig } from '../../utils/types'; -import { prepareRequestConfigs } from './prepareRequestConfigs'; +import { prepareRestRequestConfigs } from './prepareRestRequestConfigs'; -describe('prepareRequestConfigs', () => { +describe('prepareRestRequestConfigs', () => { test('Should not sort routes if they does not contain entities', () => { - const requestConfigs: RequestConfig[] = [ + const restRequestConfigs: RestRequestConfig[] = [ { path: '/user', method: 'get', @@ -21,11 +21,11 @@ describe('prepareRequestConfigs', () => { ] } ]; - expect(prepareRequestConfigs(requestConfigs)).toStrictEqual(requestConfigs); + expect(prepareRestRequestConfigs(restRequestConfigs)).toStrictEqual(restRequestConfigs); }); test('Should sort routes by their specificity of entities', () => { - const requestConfigs: RequestConfig[] = [ + const restRequestConfigs: RestRequestConfig[] = [ { path: '/user', method: 'get', @@ -62,7 +62,7 @@ describe('prepareRequestConfigs', () => { ] } ]; - const expectedRequestConfigs: RequestConfig[] = [ + const expectedRestRequestConfigs: RestRequestConfig[] = [ { path: '/user', method: 'get', @@ -99,11 +99,11 @@ describe('prepareRequestConfigs', () => { ] } ]; - expect(prepareRequestConfigs(requestConfigs)).toStrictEqual(expectedRequestConfigs); + expect(prepareRestRequestConfigs(restRequestConfigs)).toStrictEqual(expectedRestRequestConfigs); }); test('Should set not object body weight equals to one', () => { - const requestConfigs: RequestConfig[] = [ + const restRequestConfigs: RestRequestConfig[] = [ { path: '/user', method: 'post', @@ -126,7 +126,7 @@ describe('prepareRequestConfigs', () => { ] } ]; - const expectedRequestConfigs: RequestConfig[] = [ + const expectedRestRequestConfigs: RestRequestConfig[] = [ { path: '/user', method: 'post', @@ -149,6 +149,6 @@ describe('prepareRequestConfigs', () => { ] } ]; - expect(prepareRequestConfigs(requestConfigs)).toStrictEqual(expectedRequestConfigs); + expect(prepareRestRequestConfigs(restRequestConfigs)).toStrictEqual(expectedRestRequestConfigs); }); }); diff --git a/src/configs/prepareRequestConfigs/prepareRequestConfigs.ts b/src/rest/prepareRestRequestConfigs/prepareRestRequestConfigs.ts similarity index 72% rename from src/configs/prepareRequestConfigs/prepareRequestConfigs.ts rename to src/rest/prepareRestRequestConfigs/prepareRestRequestConfigs.ts index db8dfcb3..f992b33b 100644 --- a/src/configs/prepareRequestConfigs/prepareRequestConfigs.ts +++ b/src/rest/prepareRestRequestConfigs/prepareRestRequestConfigs.ts @@ -1,8 +1,8 @@ import { isPlainObject } from '../../utils/helpers'; -import type { RequestConfig, RestMethod, RouteConfig } from '../../utils/types'; +import type { RestMethod, RestRequestConfig, RestRouteConfig } from '../../utils/types'; -const calculateRouteConfigWeight = (routeConfig: RouteConfig) => { - const { entities } = routeConfig; +const calculateRouteConfigWeight = (restRouteConfig: RestRouteConfig) => { + const { entities } = restRouteConfig; if (!entities) return 0; let routeConfigWeight = 0; @@ -15,7 +15,7 @@ const calculateRouteConfigWeight = (routeConfig: RouteConfig) => { return routeConfigWeight; }; -export const prepareRequestConfigs = (requestConfigs: RequestConfig[]) => { +export const prepareRestRequestConfigs = (requestConfigs: RestRequestConfig[]) => { requestConfigs.forEach((requestConfig) => { requestConfig.routes.sort( (first, second) => diff --git a/src/routes/callResponseInterceptors/callResponseInterceptors.ts b/src/routes/callResponseInterceptors/callResponseInterceptors.ts index 05858674..ea0707a9 100644 --- a/src/routes/callResponseInterceptors/callResponseInterceptors.ts +++ b/src/routes/callResponseInterceptors/callResponseInterceptors.ts @@ -14,7 +14,9 @@ interface CallResponseInterceptorsParams { }; } -export const callResponseInterceptors = (params: CallResponseInterceptorsParams) => { +export const callResponseInterceptors = ( + params: CallResponseInterceptorsParams +) => { const { data, request, response, interceptors } = params; const setDelay = async (delay: number) => { await sleep(delay === Infinity ? 100000 : delay); diff --git a/src/routes/createRoutes/createRoutes.test.ts b/src/routes/createRoutes/createRoutes.test.ts deleted file mode 100644 index 46929ff6..00000000 --- a/src/routes/createRoutes/createRoutes.test.ts +++ /dev/null @@ -1,306 +0,0 @@ -import express from 'express'; -import request from 'supertest'; - -import type { MockServerConfig } from '../../utils/types'; - -import { createRoutes } from './createRoutes'; - -describe('createRoutes', () => { - const createServer = ( - mockServerConfig: Pick - ) => { - const server = express(); - const routerBase = express.Router(); - const routerWithRoutes = createRoutes(routerBase, mockServerConfig); - - server.use(express.json()); - server.use(mockServerConfig.baseUrl ?? '/', routerWithRoutes); - return server; - }; - - test('Should match config by entities "includes" behavior', async () => { - const server = createServer({ - configs: [ - { - path: '/users', - method: 'get', - routes: [ - { - entities: { - headers: { key1: 'value1', key2: 'value2' }, - query: { key1: 'value1' } - }, - data: { name: 'John', surname: 'Doe' } - } - ] - } - ] - }); - - const response = await request(server) - .get('/users') - .set({ key1: 'value1', key2: 'value2' }) - .query({ key1: 'value1', key2: 'value2' }); - - expect(response.statusCode).toBe(200); - expect(response.body).toStrictEqual({ name: 'John', surname: 'Doe' }); - }); - - test('Should give priority to more specific route config', async () => { - const server = createServer({ - configs: [ - { - path: '/users', - method: 'get', - routes: [ - { - entities: { - headers: { key1: 'value1', key2: 'value2' }, - query: { key1: 'value1' } - }, - data: { name: 'John', surname: 'Doe' } - }, - { - entities: { - headers: { key1: 'value1', key2: 'value2' }, - query: { key1: 'value1', key2: 'value2' } - }, - data: { name: 'John', surname: 'Smith' } - } - ] - } - ] - }); - - const response = await request(server) - .get('/users') - .set({ key1: 'value1', key2: 'value2', key3: 'value3' }) - .query({ key1: 'value1', key2: 'value2', key3: 'value3' }); - - expect(response.statusCode).toBe(200); - expect(response.body).toStrictEqual({ name: 'John', surname: 'Smith' }); - }); - - test('Should return 404 and description text for no matched request entities', async () => { - const server = createServer({ - configs: [ - { - path: '/users', - method: 'get', - routes: [ - { - entities: { - headers: { key1: 'value1' } - }, - data: { name: 'John', surname: 'Doe' } - } - ] - } - ] - }); - - const response = await request(server).get('/users').set({ key2: 'value2' }); - - expect(response.statusCode).toBe(404); - expect(response.body).toBe('No data for GET:/users'); - }); - - test('Should compare non plain object body by full equal behavior', async () => { - const server = createServer({ - configs: [ - { - path: '/users', - method: 'post', - routes: [ - { - entities: { - body: [ - { - key1: 'value1', - key2: { nestedKey1: 'nestedValue1' } - } - ] - }, - data: { name: 'John', surname: 'Doe' } - } - ] - } - ] - }); - - const successResponse = await request(server) - .post('/users') - .set('Content-Type', 'application/json') - .send([{ key1: 'value1', key2: { nestedKey1: 'nestedValue1' } }]); - expect(successResponse.statusCode).toBe(200); - expect(successResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' }); - - const failedResponse = await request(server) - .post('/users') - .set('Content-Type', 'application/json') - .send([{ key1: 'value1', key2: { nestedKey1: 'nestedValue1', nestedKey2: 'nestedValue2' } }]); - expect(failedResponse.statusCode).toBe(404); - expect(failedResponse.body).toBe('No data for POST:/users'); - }); - - test('Should compare plain object body by "includes" behavior', async () => { - const server = createServer({ - configs: [ - { - path: '/users', - method: 'post', - routes: [ - { - entities: { - body: { - key1: 'value1', - key2: { nestedKey1: 'nestedValue1' } - } - }, - data: { name: 'John', surname: 'Doe' } - } - ] - } - ] - }); - - const response = await request(server) - .post('/users') - .set('Content-Type', 'application/json') - .send({ key1: 'value1', key2: { nestedKey1: 'nestedValue1', nestedKey2: 'nestedValue2' } }); - - expect(response.statusCode).toBe(200); - expect(response.body).toStrictEqual({ name: 'John', surname: 'Doe' }); - }); - - test('Should call request interceptors in order: request -> server', async () => { - const requestInterceptor = jest.fn(); - const serverInterceptor = jest.fn(); - const server = createServer({ - configs: [ - { - path: '/users', - method: 'post', - routes: [ - { - entities: { - body: { - key1: 'value1', - key2: 'value2' - } - }, - data: { name: 'John', surname: 'Doe' } - } - ], - interceptors: { request: requestInterceptor } - }, - { - path: '/settings', - method: 'post', - routes: [ - { - entities: { - body: { - key1: 'value1', - key2: 'value2' - } - }, - data: { name: 'John', surname: 'Smith' } - } - ] - } - ], - interceptors: { request: serverInterceptor } - }); - - await request(server) - .post('/users') - .set('Content-Type', 'application/json') - .send({ key1: 'value1', key2: 'value2' }); - expect(requestInterceptor.mock.calls.length).toBe(1); - expect(serverInterceptor.mock.calls.length).toBe(1); - expect(requestInterceptor.mock.invocationCallOrder[0]).toBeLessThan( - serverInterceptor.mock.invocationCallOrder[0] - ); - - await request(server) - .post('/settings') - .set('Content-Type', 'application/json') - .send({ key1: 'value1', key2: 'value2' }); - expect(requestInterceptor.mock.calls.length).toBe(1); - expect(serverInterceptor.mock.calls.length).toBe(2); - }); - - test('Should call response interceptors in order: route -> request -> server', async () => { - const routeInterceptor = jest.fn(); - const requestInterceptor = jest.fn(); - const serverInterceptor = jest.fn(); - const server = createServer({ - configs: [ - { - path: '/users', - method: 'post', - routes: [ - { - entities: { - body: { - key1: 'value1', - key2: 'value2' - } - }, - data: { name: 'John', surname: 'Doe' }, - interceptors: { response: routeInterceptor } - } - ], - interceptors: { response: requestInterceptor } - }, - { - path: '/settings', - method: 'post', - routes: [ - { - entities: { - body: { - key1: 'value1', - key2: 'value2' - } - }, - data: { name: 'John', surname: 'Smith' } - } - ] - } - ], - interceptors: { response: serverInterceptor } - }); - - await request(server) - .post('/users') - .set('Content-Type', 'application/json') - .send({ key1: 'value1', key2: 'value2' }); - expect(routeInterceptor.mock.calls.length).toBe(1); - expect(requestInterceptor.mock.calls.length).toBe(1); - expect(serverInterceptor.mock.calls.length).toBe(1); - expect(routeInterceptor.mock.invocationCallOrder[0]).toBeLessThan( - requestInterceptor.mock.invocationCallOrder[0] - ); - expect(requestInterceptor.mock.invocationCallOrder[0]).toBeLessThan( - serverInterceptor.mock.invocationCallOrder[0] - ); - - await request(server) - .post('/settings') - .set('Content-Type', 'application/json') - .send({ key1: 'value1', key2: 'value2' }); - expect(routeInterceptor.mock.calls.length).toBe(1); - expect(requestInterceptor.mock.calls.length).toBe(1); - expect(serverInterceptor.mock.calls.length).toBe(2); - - await request(server) - .post('/messages') - .set('Content-Type', 'application/json') - .send({ key1: 'value1', key2: 'value2' }); - expect(routeInterceptor.mock.calls.length).toBe(1); - expect(requestInterceptor.mock.calls.length).toBe(1); - expect(serverInterceptor.mock.calls.length).toBe(2); - }); -}); diff --git a/src/routes/createRoutes/createRoutes.ts b/src/routes/createRoutes/createRoutes.ts deleted file mode 100644 index be7318b1..00000000 --- a/src/routes/createRoutes/createRoutes.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { IRouter } from 'express'; - -import { isEntityValuesEqual } from '../../configs/isEntitiesEqual/isEntityValuesEqual'; -import { prepareRequestConfigs } from '../../configs/prepareRequestConfigs/prepareRequestConfigs'; -import type { BodyValue, Entities, MockServerConfig, PlainObject } from '../../utils/types'; -import { callRequestInterceptors } from '../callRequestInterceptors/callRequestInterceptors'; -import { callResponseInterceptors } from '../callResponseInterceptors/callResponseInterceptors'; - -export const createRoutes = ( - router: IRouter, - mockServerConfig: Pick -) => { - prepareRequestConfigs(mockServerConfig.configs).forEach((requestConfig) => { - router.route(requestConfig.path)[requestConfig.method]((request, response) => { - callRequestInterceptors({ - request, - interceptors: { - requestInterceptor: requestConfig.interceptors?.request, - serverInterceptor: mockServerConfig.interceptors?.request - } - }); - - const matchedRouteConfig = requestConfig.routes.find(({ entities }) => { - if (!entities) return true; - return (Object.entries(entities) as [Entities, PlainObject | BodyValue][]).every( - ([entity, entityValue]) => - isEntityValuesEqual(entityValue, request[entity]) - ); - }); - - if (!matchedRouteConfig) { - return response - .status(404) - .json(`No data for ${request.method}:${request.baseUrl}${request.path}`); - } - - const data = callResponseInterceptors({ - data: matchedRouteConfig.data, - request, - response, - interceptors: { - routeInterceptor: matchedRouteConfig.interceptors?.response, - requestInterceptor: requestConfig.interceptors?.response, - serverInterceptor: mockServerConfig.interceptors?.response - } - }); - return response.status(response.statusCode).json(data); - }); - }); - - return router; -}; diff --git a/src/server/createMockServer/createMockServer.ts b/src/server/createMockServer/createMockServer.ts index f2abada0..fb2c9a4b 100644 --- a/src/server/createMockServer/createMockServer.ts +++ b/src/server/createMockServer/createMockServer.ts @@ -1,20 +1,21 @@ import bodyParser from 'body-parser'; import type { Express } from 'express'; import express from 'express'; +import path from 'path'; import { corsMiddleware } from '../../cors/corsMiddleware/corsMiddleware'; import { noCorsMiddleware } from '../../cors/noCorsMiddleware/noCorsMiddleware'; -import { createRoutes } from '../../routes/createRoutes/createRoutes'; +import { createGraphQLRoutes } from '../../graphql/createGraphQLRoutes/createGraphQLRoutes'; +import { notFoundMiddleware } from '../../notFound/notFoundMiddleware'; +import { createRestRoutes } from '../../rest/createRestRoutes/createRestRoutes'; import { staticMiddleware } from '../../static/staticMiddleware/staticMiddleware'; import type { MockServerConfig } from '../../utils/types'; -export const createMockServer = ({ - cors, - staticPath, - ...mockServerConfig -}: Omit) => { +export const createMockServer = (mockServerConfig: Omit) => { + const { cors, staticPath, rest, graphql, interceptors } = mockServerConfig; const server: Express = express(); + server.set('view engine', 'ejs'); server.use(bodyParser.urlencoded({ extended: false })); server.use(bodyParser.json({ limit: '10mb' })); @@ -30,12 +31,28 @@ export const createMockServer = ({ staticMiddleware(server, baseUrl, staticPath); } - const routerBase = express.Router(); - const routerWithRoutes = createRoutes(routerBase, { - configs: mockServerConfig.configs, - interceptors: mockServerConfig.interceptors + if (rest) { + const routerWithRestRoutes = createRestRoutes(express.Router(), rest.configs, interceptors); + + const restBaseUrl = path.join(baseUrl, rest.baseUrl ?? '/'); + server.use(restBaseUrl, routerWithRestRoutes); + } + + if (graphql) { + const routerWithGraphQLRoutes = createGraphQLRoutes( + express.Router(), + graphql.configs, + interceptors + ); + + const graphqlBaseUrl = path.join(baseUrl, graphql.baseUrl ?? '/'); + server.use(graphqlBaseUrl, routerWithGraphQLRoutes); + } + + notFoundMiddleware({ + server, + mockServerConfig }); - server.use(baseUrl, routerWithRoutes); return server; }; diff --git a/src/server/startMockServer/startMockServer.ts b/src/server/startMockServer/startMockServer.ts index 328c92f5..1db06619 100644 --- a/src/server/startMockServer/startMockServer.ts +++ b/src/server/startMockServer/startMockServer.ts @@ -1,3 +1,5 @@ +import color from 'ansi-colors'; + import { DEFAULT } from '../../utils/constants'; import type { MockServerConfig } from '../../utils/types'; import { createMockServer } from '../createMockServer/createMockServer'; @@ -7,6 +9,6 @@ export const startMockServer = (mockServerConfig: MockServerConfig) => { const port = mockServerConfig.port ?? DEFAULT.PORT; mockServer.listen(port, () => { - console.log(`🎉 Mock Server is running at http://localhost:${port}`); + console.log(color.green(`🎉 Mock Server is running at http://localhost:${port}`)); }); }; diff --git a/src/static/staticMiddleware/staticMiddleware.ts b/src/static/staticMiddleware/staticMiddleware.ts index 9ec86c2a..aaebf096 100644 --- a/src/static/staticMiddleware/staticMiddleware.ts +++ b/src/static/staticMiddleware/staticMiddleware.ts @@ -12,10 +12,11 @@ export const staticMiddleware = (server: Express, baseUrl: BaseUrl, staticPath: staticPath.forEach((staticPath) => { const isPathObject = typeof staticPath === 'object'; if (isPathObject) { - return server.use( + server.use( path.join(baseUrl, staticPath.prefix), express.static(path.join(APP_PATH, staticPath.path)) ); + return; } server.use(baseUrl, express.static(path.join(APP_PATH, staticPath))); }); diff --git a/src/utils/constants/default.ts b/src/utils/constants/default.ts index df2d169b..a13aefab 100644 --- a/src/utils/constants/default.ts +++ b/src/utils/constants/default.ts @@ -3,7 +3,8 @@ export const DEFAULT = { CORS: { ORIGIN: '*', METHODS: '*', - HEADERS: '*', + ALLOWED_HEADERS: '*', + EXPOSED_HEADERS: '*', CREDENTIALS: true, MAX_AGE: 3600 } diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 7c5a3171..0041f836 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -1,2 +1,3 @@ export * from './isPlainObject'; export * from './sleep'; +export * from './url'; diff --git a/src/utils/helpers/isPlainObject.ts b/src/utils/helpers/isPlainObject.ts index 2ae527c1..748a49be 100644 --- a/src/utils/helpers/isPlainObject.ts +++ b/src/utils/helpers/isPlainObject.ts @@ -1,2 +1,2 @@ -export const isPlainObject = (value: any) => +export const isPlainObject = (value: any): value is Record => typeof value === 'object' && !Array.isArray(value) && value !== null; diff --git a/src/utils/helpers/url/getUrlParts/getUrlParts.test.ts b/src/utils/helpers/url/getUrlParts/getUrlParts.test.ts new file mode 100644 index 00000000..fd23cf84 --- /dev/null +++ b/src/utils/helpers/url/getUrlParts/getUrlParts.test.ts @@ -0,0 +1,8 @@ +import { getUrlParts } from './getUrlParts'; + +describe('getUrlParts', () => { + test('Should correct parse url parts', () => { + expect(getUrlParts('/part1/part2/')).toEqual(['part1', 'part2']); + expect(getUrlParts('//part1/part2///')).toEqual(['part1', 'part2']); + }); +}); diff --git a/src/utils/helpers/url/getUrlParts/getUrlParts.ts b/src/utils/helpers/url/getUrlParts/getUrlParts.ts new file mode 100644 index 00000000..87df5d6a --- /dev/null +++ b/src/utils/helpers/url/getUrlParts/getUrlParts.ts @@ -0,0 +1,3 @@ +import { removeLeadingAndTrailingSlashes } from '../removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes'; + +export const getUrlParts = (url: string) => removeLeadingAndTrailingSlashes(url).split('/'); diff --git a/src/utils/helpers/url/index.ts b/src/utils/helpers/url/index.ts new file mode 100644 index 00000000..d7883c42 --- /dev/null +++ b/src/utils/helpers/url/index.ts @@ -0,0 +1,2 @@ +export * from './getUrlParts/getUrlParts'; +export * from './removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes'; diff --git a/src/utils/helpers/url/removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes.test.ts b/src/utils/helpers/url/removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes.test.ts new file mode 100644 index 00000000..7b654cb5 --- /dev/null +++ b/src/utils/helpers/url/removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes.test.ts @@ -0,0 +1,10 @@ +import { removeLeadingAndTrailingSlashes } from './removeLeadingAndTrailingSlashes'; + +describe('removeLeadingAndTrailingSlashes', () => { + test('Should correct remove leading and trailing slashes', () => { + expect(removeLeadingAndTrailingSlashes('///base/users/1')).toEqual('base/users/1'); + expect(removeLeadingAndTrailingSlashes('//base/users/1///')).toEqual('base/users/1'); + expect(removeLeadingAndTrailingSlashes('base/users/1/')).toEqual('base/users/1'); + expect(removeLeadingAndTrailingSlashes('/base/users/1')).toEqual('base/users/1'); + }); +}); diff --git a/src/utils/helpers/url/removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes.ts b/src/utils/helpers/url/removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes.ts new file mode 100644 index 00000000..b89be64c --- /dev/null +++ b/src/utils/helpers/url/removeLeadingAndTrailingSlashes/removeLeadingAndTrailingSlashes.ts @@ -0,0 +1 @@ +export const removeLeadingAndTrailingSlashes = (string: string) => string.replace(/^\/+|\/+$/g, ''); diff --git a/src/utils/types/configs.ts b/src/utils/types/configs.ts index 284ccbf6..31b88a12 100644 --- a/src/utils/types/configs.ts +++ b/src/utils/types/configs.ts @@ -1,46 +1,89 @@ export type PlainObject = Record; export type PlainFunction = (...args: any[]) => any; -export type Entities = 'headers' | 'query' | 'params' | 'body'; export type BodyValue = any; +export type VariablesValue = any; +export type QueryValue = Record; +export type HeadersOrParamsValue = Record; -export type EntitiesValues = { - [Key in Entities]: Key extends 'body' ? BodyValue : PlainObject; +export type RestEntities = 'headers' | 'query' | 'params' | 'body'; +export type RestEntitiesValue = BodyValue | QueryValue | HeadersOrParamsValue; + +export type RestEntitiesValues = { + [Key in RestEntities]: Key extends 'body' + ? BodyValue + : Key extends 'query' + ? QueryValue + : Key extends 'headers' | 'params' + ? HeadersOrParamsValue + : never; }; -export interface HttpMethodsEntities { - get: Extract; - delete: Extract; - post: Entities; - put: Entities; - patch: Entities; +export interface RestMethodsEntities { + get: Extract; + delete: Extract; + post: RestEntities; + put: RestEntities; + patch: RestEntities; } -export interface RouteConfig { +export interface RestRouteConfig { entities?: { - [Key in HttpMethodsEntities[Method]]?: EntitiesValues[Key]; + [Key in RestMethodsEntities[Method]]?: RestEntitiesValues[Key]; }; data: any; interceptors?: Pick; } export type RestMethod = 'get' | 'post' | 'delete' | 'put' | 'patch'; -export interface RestRequestConfig { - path: string | RegExp; + +export interface BaseRestRequestConfig { + path: `/${string}` | RegExp; method: Method; - routes: RouteConfig[]; + routes: RestRouteConfig[]; interceptors?: import('./interceptors').Interceptors; } -export type RestGetRequestConfig = RestRequestConfig<'get'>; -export type RestPostRequestConfig = RestRequestConfig<'post'>; -export type RestPutRequestConfig = RestRequestConfig<'put'>; -export type RestDeleteRequestConfig = RestRequestConfig<'delete'>; -export type RestPatchRequestConfig = RestRequestConfig<'patch'>; +export type RestGetRequestConfig = BaseRestRequestConfig<'get'>; +export type RestPostRequestConfig = BaseRestRequestConfig<'post'>; +export type RestPutRequestConfig = BaseRestRequestConfig<'put'>; +export type RestDeleteRequestConfig = BaseRestRequestConfig<'delete'>; +export type RestPatchRequestConfig = BaseRestRequestConfig<'patch'>; -export type RequestConfig = +export type RestRequestConfig = | RestGetRequestConfig | RestPostRequestConfig | RestPutRequestConfig | RestDeleteRequestConfig | RestPatchRequestConfig; + +export type GraphQLEntities = 'headers' | 'query' | 'variables'; + +export type GraphQLEntitiesValues = { + [Key in GraphQLEntities]: Key extends 'variables' ? VariablesValue : PlainObject; +}; + +export interface GraphQLOperationsEntities { + query: GraphQLEntities; + mutation: GraphQLEntities; +} + +export type GraphQLOperationType = 'query' | 'mutation'; +export type GraphQLOperationName = string | RegExp; +export interface GraphQLRouteConfig { + entities?: { + [Key in GraphQLOperationsEntities[GraphQLOperationType]]?: GraphQLEntitiesValues[Key]; + }; + data: any; + interceptors?: Pick; +} + +export interface GraphQLQuery { + operationType: GraphQLOperationType; + operationName: GraphQLOperationName; +} + +export interface GraphQLRequestConfig extends GraphQLQuery { + routes: GraphQLRouteConfig[]; + interceptors?: import('./interceptors').Interceptors; +} diff --git a/src/utils/types/graphql.ts b/src/utils/types/graphql.ts new file mode 100644 index 00000000..a102f283 --- /dev/null +++ b/src/utils/types/graphql.ts @@ -0,0 +1,5 @@ +export type GraphQLVariables = Record; +export interface GraphQLInput { + query?: string; + variables: GraphQLVariables; +} diff --git a/src/utils/types/index.ts b/src/utils/types/index.ts index 9fc92bd4..3d4b5dbd 100644 --- a/src/utils/types/index.ts +++ b/src/utils/types/index.ts @@ -1,3 +1,4 @@ export * from './configs'; +export * from './graphql'; export * from './interceptors'; export * from './server'; diff --git a/src/utils/types/interceptors.ts b/src/utils/types/interceptors.ts index f43c5089..98c20f37 100644 --- a/src/utils/types/interceptors.ts +++ b/src/utils/types/interceptors.ts @@ -14,6 +14,6 @@ export interface InterceptorResponseParams { export type InterceptorResponse = (data: Data, params: InterceptorResponseParams) => any; export interface Interceptors { - response?: InterceptorResponse; request?: InterceptorRequest; + response?: InterceptorResponse; } diff --git a/src/utils/types/server.ts b/src/utils/types/server.ts index 4eafa459..1a60f20e 100644 --- a/src/utils/types/server.ts +++ b/src/utils/types/server.ts @@ -1,22 +1,30 @@ -export type StaticPathObject = { prefix: string; path: string }; -export type StaticPath = string | StaticPathObject | (StaticPathObject | string)[]; +export type StaticPathObject = { prefix: `/${string}`; path: `/${string}` }; +export type StaticPath = `/${string}` | StaticPathObject | (StaticPathObject | `/${string}`)[]; export type CorsHeader = string; export type CorsOrigin = string | RegExp | (RegExp | string)[]; export type Cors = { - origin: CorsOrigin | (() => Promise | CorsOrigin); + origin: CorsOrigin | ((request: import('express').Request) => Promise | CorsOrigin); methods?: Uppercase[]; - headers?: CorsHeader[]; + allowedHeaders?: CorsHeader[]; + exposedHeaders?: CorsHeader[]; credentials?: boolean; maxAge?: number; }; export type Port = number; -export type BaseUrl = string; +export type BaseUrl = `/${string}`; export interface MockServerConfig { - configs: import('./configs').RequestConfig[]; baseUrl?: BaseUrl; + rest?: { + baseUrl?: BaseUrl; + configs: import('./configs').RestRequestConfig[]; + }; + graphql?: { + baseUrl?: BaseUrl; + configs: import('./configs').GraphQLRequestConfig[]; + }; port?: Port; staticPath?: StaticPath; interceptors?: import('./interceptors').Interceptors; diff --git a/tsconfig.dev.json b/tsconfig.dev.json new file mode 100644 index 00000000..06e690e0 --- /dev/null +++ b/tsconfig.dev.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "esnext", // Specify ECMAScript target version + "lib": ["dom", "es2021"], // List of library files to be included in the compilation + "allowJs": true, // Allow JavaScript files to be compiled + "skipLibCheck": true, // Skip type checking of all declaration files + "esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs") + "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export + "strict": true, // Enable all strict type checking options + "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file. + "module": "commonjs", // Specify module code generation + "moduleResolution": "node", // Resolve modules using Node.js style + "isolatedModules": true, // Unconditionally emit imports for unresolved files + "resolveJsonModule": true, // Include modules imported with .json extension + "noUnusedLocals": true, // Report errors on unused locals + "noUnusedParameters": true, // Report errors on unused parameters + "noFallthroughCasesInSwitch": true // Report errors for fallthrough cases in switch statement + }, + "exclude": [ + "node_modules", + "jest.config.js" + ] +} + + diff --git a/tsconfig.json b/tsconfig.production.json similarity index 85% rename from tsconfig.json rename to tsconfig.production.json index 6eba0c5f..240d0726 100644 --- a/tsconfig.json +++ b/tsconfig.production.json @@ -1,7 +1,6 @@ { "compilerOptions": { "outDir": "dist", - "alwaysStrict": true, "declaration": true, "target": "esnext", // Specify ECMAScript target version "lib": ["dom", "es2021"], // List of library files to be included in the compilation @@ -13,18 +12,16 @@ "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file. "module": "commonjs", // Specify module code generation "moduleResolution": "node", // Resolve modules using Node.js style - "isolatedModules": false, // Unconditionally emit imports for unresolved files + "isolatedModules": true, // Unconditionally emit imports for unresolved files "resolveJsonModule": true, // Include modules imported with .json extension - "sourceMap": false, // Generate corrresponding .map file + "sourceMap": false, // Generate corresponding .map file "noUnusedLocals": true, // Report errors on unused locals "noUnusedParameters": true, // Report errors on unused parameters "noFallthroughCasesInSwitch": true // Report errors for fallthrough cases in switch statement }, "exclude": [ - "node_modules", - "./**/*.test.ts", + "node_modules", + "./**/*.test.ts", "jest.config.js" ] } - - diff --git a/views/notFound.ejs b/views/notFound.ejs new file mode 100644 index 00000000..38d49e41 --- /dev/null +++ b/views/notFound.ejs @@ -0,0 +1,42 @@ + + + +

404

+
+ Seems to be your config does not have data for '<%= requestMethod %> + <%= decodeURIComponent(url) %> request, or you have typo in it. +
+ + <% if (restUrlSuggestions.length || graphqlUrlSuggestions.length) { %> +

+ Maybe you are looking for one of these paths? +

+ <% } %> + + <% if (restUrlSuggestions.length) { %> +
+

REST

+ +
+ <% } %> + + <% if (graphqlUrlSuggestions.length) { %> +
+

GraphQL

+
    + <% graphqlUrlSuggestions.forEach((graphqlUrlSuggestion) => { %> +
  • + <%= decodeURIComponent(graphqlUrlSuggestion) %> +
  • + <% }) %> +
+
+ <% } %> + + diff --git a/yarn.lock b/yarn.lock index aa5d4939..85e58a0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -298,115 +298,115 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@esbuild/android-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" - integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== - -"@esbuild/android-arm@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" - integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== - -"@esbuild/android-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" - integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== - -"@esbuild/darwin-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" - integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== - -"@esbuild/darwin-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" - integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== - -"@esbuild/freebsd-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" - integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== - -"@esbuild/freebsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" - integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== - -"@esbuild/linux-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" - integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== - -"@esbuild/linux-arm@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" - integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== - -"@esbuild/linux-ia32@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" - integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== - -"@esbuild/linux-loong64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" - integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== - -"@esbuild/linux-mips64el@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" - integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== - -"@esbuild/linux-ppc64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" - integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== - -"@esbuild/linux-riscv64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" - integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== - -"@esbuild/linux-s390x@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" - integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== - -"@esbuild/linux-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" - integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== - -"@esbuild/netbsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" - integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== - -"@esbuild/openbsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" - integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== - -"@esbuild/sunos-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" - integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== - -"@esbuild/win32-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" - integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== - -"@esbuild/win32-ia32@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" - integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== - -"@esbuild/win32-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" - integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== +"@esbuild/android-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz#b3d5b65a3b2e073a6c7ee36b1f3c30c8f000315b" + integrity sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ== + +"@esbuild/android-arm@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.8.tgz#c41e496af541e175369d48164d0cf01a5f656cf6" + integrity sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w== + +"@esbuild/android-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.8.tgz#080fa67c29be77f5a3ca5ee4cc78d5bf927e3a3b" + integrity sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg== + +"@esbuild/darwin-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz#053622bf9a82f43d5c075b7818e02618f7b4a397" + integrity sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg== + +"@esbuild/darwin-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz#8a1aadb358d537d8efad817bb1a5bff91b84734b" + integrity sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw== + +"@esbuild/freebsd-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz#e6738d0081ba0721a5c6c674e84c6e7fcea61989" + integrity sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A== + +"@esbuild/freebsd-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz#1855e562f2b730f4483f6e94086e9e2597feb4c3" + integrity sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q== + +"@esbuild/linux-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz#481da38952721a3fdb77c17a36ceaacc4270b5c5" + integrity sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww== + +"@esbuild/linux-arm@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz#18127072b270bb6321c6d11be20bfd30e0d6ad17" + integrity sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA== + +"@esbuild/linux-ia32@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz#ee400af7b3bc69e8ca2e593ca35156ffb9abd54f" + integrity sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA== + +"@esbuild/linux-loong64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz#8c509d8a454693d39824b83b3f66c400872fce82" + integrity sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA== + +"@esbuild/linux-mips64el@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz#f2b0d36e63fb26bc3f95b203b6a80638292101ca" + integrity sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA== + +"@esbuild/linux-ppc64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz#1e628be003e036e90423716028cc884fe5ba25bd" + integrity sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw== + +"@esbuild/linux-riscv64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz#419a815cb4c3fb9f1b78ef5295f5b48b8bf6427a" + integrity sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw== + +"@esbuild/linux-s390x@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz#291c49ae5c3d11d226352755c0835911fe1a9e5c" + integrity sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw== + +"@esbuild/linux-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz#03199d91c76faf80bd54104f5cbf0a489bc39f6a" + integrity sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA== + +"@esbuild/netbsd-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz#b436d767e1b21852f9ed212e2bb57f77203b0ae2" + integrity sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q== + +"@esbuild/openbsd-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz#d1481d8539e21d4729cd04a0450a26c2c8789e89" + integrity sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg== + +"@esbuild/sunos-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz#2cfb8126e079b2c00fd1bf095541e9f5c47877e4" + integrity sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw== + +"@esbuild/win32-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz#7c6ecfd097ca23b82119753bf7072bbaefe51e3a" + integrity sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg== + +"@esbuild/win32-ia32@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz#cffec63c3cb0ef8563a04df4e09fa71056171d00" + integrity sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w== + +"@esbuild/win32-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz#200a0965cf654ac28b971358ecdca9cc5b44c335" + integrity sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg== "@eslint/eslintrc@^1.4.1": version "1.4.1" @@ -458,61 +458,61 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.3.1.tgz#3e3f876e4e47616ea3b1464b9fbda981872e9583" - integrity sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg== +"@jest/console@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.4.2.tgz#f78374905c2454764152904a344a2d5226b0ef09" + integrity sha512-0I/rEJwMpV9iwi9cDEnT71a5nNGK9lj8Z4+1pRAU2x/thVXCDnaTGrvxyK+cAqZTFVFCiR+hfVrP4l2m+dCmQg== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^29.3.1" - jest-util "^29.3.1" + jest-message-util "^29.4.2" + jest-util "^29.4.2" slash "^3.0.0" -"@jest/core@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.3.1.tgz#bff00f413ff0128f4debec1099ba7dcd649774a1" - integrity sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw== +"@jest/core@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.4.2.tgz#6e999b67bdc2df9d96ba9b142465bda71ee472c2" + integrity sha512-KGuoQah0P3vGNlaS/l9/wQENZGNKGoWb+OPxh3gz+YzG7/XExvYu34MzikRndQCdM2S0tzExN4+FL37i6gZmCQ== dependencies: - "@jest/console" "^29.3.1" - "@jest/reporters" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.2" + "@jest/reporters" "^29.4.2" + "@jest/test-result" "^29.4.2" + "@jest/transform" "^29.4.2" + "@jest/types" "^29.4.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" ci-info "^3.2.0" exit "^0.1.2" graceful-fs "^4.2.9" - jest-changed-files "^29.2.0" - jest-config "^29.3.1" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-resolve-dependencies "^29.3.1" - jest-runner "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" - jest-watcher "^29.3.1" + jest-changed-files "^29.4.2" + jest-config "^29.4.2" + jest-haste-map "^29.4.2" + jest-message-util "^29.4.2" + jest-regex-util "^29.4.2" + jest-resolve "^29.4.2" + jest-resolve-dependencies "^29.4.2" + jest-runner "^29.4.2" + jest-runtime "^29.4.2" + jest-snapshot "^29.4.2" + jest-util "^29.4.2" + jest-validate "^29.4.2" + jest-watcher "^29.4.2" micromatch "^4.0.4" - pretty-format "^29.3.1" + pretty-format "^29.4.2" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.3.1.tgz#eb039f726d5fcd14698acd072ac6576d41cfcaa6" - integrity sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag== +"@jest/environment@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.4.2.tgz#ee92c316ee2fbdf0bcd9d2db0ef42d64fea26b56" + integrity sha512-JKs3VUtse0vQfCaFGJRX1bir9yBdtasxziSyu+pIiEllAQOe4oQhdCYIf3+Lx+nGglFktSKToBnRJfD5QKp+NQ== dependencies: - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/fake-timers" "^29.4.2" + "@jest/types" "^29.4.2" "@types/node" "*" - jest-mock "^29.3.1" + jest-mock "^29.4.2" "@jest/expect-utils@^29.3.1": version "29.3.1" @@ -521,46 +521,53 @@ dependencies: jest-get-type "^29.2.0" -"@jest/expect@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.3.1.tgz#456385b62894349c1d196f2d183e3716d4c6a6cd" - integrity sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg== +"@jest/expect-utils@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.2.tgz#cd0065dfdd8e8a182aa350cc121db97b5eed7b3f" + integrity sha512-Dd3ilDJpBnqa0GiPN7QrudVs0cczMMHtehSo2CSTjm3zdHx0RcpmhFNVEltuEFeqfLIyWKFI224FsMSQ/nsJQA== dependencies: - expect "^29.3.1" - jest-snapshot "^29.3.1" + jest-get-type "^29.4.2" -"@jest/fake-timers@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.3.1.tgz#b140625095b60a44de820876d4c14da1aa963f67" - integrity sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A== +"@jest/expect@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.4.2.tgz#2d4a6a41b29380957c5094de19259f87f194578b" + integrity sha512-NUAeZVApzyaeLjfWIV/64zXjA2SS+NuUPHpAlO7IwVMGd5Vf9szTl9KEDlxY3B4liwLO31os88tYNHl6cpjtKQ== dependencies: - "@jest/types" "^29.3.1" - "@sinonjs/fake-timers" "^9.1.2" + expect "^29.4.2" + jest-snapshot "^29.4.2" + +"@jest/fake-timers@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.4.2.tgz#af43ee1a5720b987d0348f80df98f2cb17d45cd0" + integrity sha512-Ny1u0Wg6kCsHFWq7A/rW/tMhIedq2siiyHyLpHCmIhP7WmcAmd2cx95P+0xtTZlj5ZbJxIRQi4OPydZZUoiSQQ== + dependencies: + "@jest/types" "^29.4.2" + "@sinonjs/fake-timers" "^10.0.2" "@types/node" "*" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-util "^29.3.1" + jest-message-util "^29.4.2" + jest-mock "^29.4.2" + jest-util "^29.4.2" -"@jest/globals@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.3.1.tgz#92be078228e82d629df40c3656d45328f134a0c6" - integrity sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q== +"@jest/globals@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.4.2.tgz#73f85f5db0e17642258b25fd0b9fc89ddedb50eb" + integrity sha512-zCk70YGPzKnz/I9BNFDPlK+EuJLk21ur/NozVh6JVM86/YYZtZHqxFFQ62O9MWq7uf3vIZnvNA0BzzrtxD9iyg== dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/types" "^29.3.1" - jest-mock "^29.3.1" + "@jest/environment" "^29.4.2" + "@jest/expect" "^29.4.2" + "@jest/types" "^29.4.2" + jest-mock "^29.4.2" -"@jest/reporters@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.3.1.tgz#9a6d78c109608e677c25ddb34f907b90e07b4310" - integrity sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA== +"@jest/reporters@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.4.2.tgz#6abfa923941daae0acc76a18830ee9e79a22042d" + integrity sha512-10yw6YQe75zCgYcXgEND9kw3UZZH5tJeLzWv4vTk/2mrS1aY50A37F+XT2hPO5OqQFFnUWizXD8k1BMiATNfUw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.2" + "@jest/test-result" "^29.4.2" + "@jest/transform" "^29.4.2" + "@jest/types" "^29.4.2" "@jridgewell/trace-mapping" "^0.3.15" "@types/node" "*" chalk "^4.0.0" @@ -573,9 +580,9 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-message-util "^29.3.1" - jest-util "^29.3.1" - jest-worker "^29.3.1" + jest-message-util "^29.4.2" + jest-util "^29.4.2" + jest-worker "^29.4.2" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" @@ -588,55 +595,62 @@ dependencies: "@sinclair/typebox" "^0.24.1" -"@jest/source-map@^29.2.0": - version "29.2.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.2.0.tgz#ab3420c46d42508dcc3dc1c6deee0b613c235744" - integrity sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ== +"@jest/schemas@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.2.tgz#cf7cfe97c5649f518452b176c47ed07486270fc1" + integrity sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g== + dependencies: + "@sinclair/typebox" "^0.25.16" + +"@jest/source-map@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.2.tgz#f9815d59e25cd3d6828e41489cd239271018d153" + integrity sha512-tIoqV5ZNgYI9XCKXMqbYe5JbumcvyTgNN+V5QW4My033lanijvCD0D4PI9tBw4pRTqWOc00/7X3KVvUh+qnF4Q== dependencies: "@jridgewell/trace-mapping" "^0.3.15" callsites "^3.0.0" graceful-fs "^4.2.9" -"@jest/test-result@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.3.1.tgz#92cd5099aa94be947560a24610aa76606de78f50" - integrity sha512-qeLa6qc0ddB0kuOZyZIhfN5q0e2htngokyTWsGriedsDhItisW7SDYZ7ceOe57Ii03sL988/03wAcBh3TChMGw== +"@jest/test-result@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.4.2.tgz#34b0ba069f2e3072261e4884c8fb6bd15ed6fb8d" + integrity sha512-HZsC3shhiHVvMtP+i55MGR5bPcc3obCFbA5bzIOb8pCjwBZf11cZliJncCgaVUbC5yoQNuGqCkC0Q3t6EItxZA== dependencies: - "@jest/console" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.2" + "@jest/types" "^29.4.2" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.3.1.tgz#fa24b3b050f7a59d48f7ef9e0b782ab65123090d" - integrity sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA== +"@jest/test-sequencer@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.4.2.tgz#8b48e5bc4af80b42edacaf2a733d4f295edf28fb" + integrity sha512-9Z2cVsD6CcObIVrWigHp2McRJhvCxL27xHtrZFgNC1RwnoSpDx6fZo8QYjJmziFlW9/hr78/3sxF54S8B6v8rg== dependencies: - "@jest/test-result" "^29.3.1" + "@jest/test-result" "^29.4.2" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" + jest-haste-map "^29.4.2" slash "^3.0.0" -"@jest/transform@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.3.1.tgz#1e6bd3da4af50b5c82a539b7b1f3770568d6e36d" - integrity sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug== +"@jest/transform@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.4.2.tgz#b24b72dbab4c8675433a80e222d6a8ef4656fb81" + integrity sha512-kf1v5iTJHn7p9RbOsBuc/lcwyPtJaZJt5885C98omWz79NIeD3PfoiiaPSu7JyCyFzNOIzKhmMhQLUhlTL9BvQ== dependencies: "@babel/core" "^7.11.6" - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.2" "@jridgewell/trace-mapping" "^0.3.15" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^2.0.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" + jest-haste-map "^29.4.2" + jest-regex-util "^29.4.2" + jest-util "^29.4.2" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" - write-file-atomic "^4.0.1" + write-file-atomic "^4.0.2" "@jest/types@^29.3.1": version "29.3.1" @@ -650,6 +664,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.4.2": + version "29.4.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.2.tgz#8f724a414b1246b2bfd56ca5225d9e1f39540d82" + integrity sha512-CKlngyGP0fwlgC1BRUtPZSiWLBhyS9dKwKmyGxk8Z6M82LBEGB2aLQSg+U1MyLsU+M7UjnlLllBM2BLWKVm/Uw== + dependencies: + "@jest/schemas" "^29.4.2" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" @@ -728,19 +754,24 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== -"@sinonjs/commons@^1.7.0": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" - integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== +"@sinclair/typebox@^0.25.16": + version "0.25.21" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" + integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== + +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^9.1.2": - version "9.1.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" - integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== +"@sinonjs/fake-timers@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" + integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== dependencies: - "@sinonjs/commons" "^1.7.0" + "@sinonjs/commons" "^2.0.0" "@types/babel__core@^7.1.14": version "7.1.20" @@ -795,22 +826,22 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== -"@types/express-serve-static-core@^4.17.31": - version "4.17.32" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz#93dda387f5516af616d8d3f05f2c4c79d81e1b82" - integrity sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA== +"@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.15": - version "4.17.15" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" - integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== +"@types/express@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.31" + "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" "@types/serve-static" "*" @@ -845,10 +876,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.2.4": - version "29.2.5" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.5.tgz#c27f41a9d6253f288d1910d3c5f09484a56b73c0" - integrity sha512-H2cSxkKgVmqNHXP7TC2L/WUorrZu8ZigyRywfVzv6EyBlxj39n4C00hjXYQWsbwqgElaj/CiAeSRmk5GoaKTgw== +"@types/jest@^29.4.0": + version "29.4.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.0.tgz#a8444ad1704493e84dbf07bb05990b275b3b9206" + integrity sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -933,87 +964,88 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.31.0": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz#deee67e399f2cb6b4608c935777110e509d8018c" - integrity sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ== +"@typescript-eslint/eslint-plugin@^5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz#da3f2819633061ced84bb82c53bba45a6fe9963a" + integrity sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ== dependencies: - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/type-utils" "5.48.1" - "@typescript-eslint/utils" "5.48.1" + "@typescript-eslint/scope-manager" "5.51.0" + "@typescript-eslint/type-utils" "5.51.0" + "@typescript-eslint/utils" "5.51.0" debug "^4.3.4" + grapheme-splitter "^1.0.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" regexpp "^3.2.0" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.31.0": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.1.tgz#d0125792dab7e232035434ab8ef0658154db2f10" - integrity sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA== +"@typescript-eslint/parser@^5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.51.0.tgz#2d74626652096d966ef107f44b9479f02f51f271" + integrity sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA== dependencies: - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/scope-manager" "5.51.0" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/typescript-estree" "5.51.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz#39c71e4de639f5fe08b988005beaaf6d79f9d64d" - integrity sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ== +"@typescript-eslint/scope-manager@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz#ad3e3c2ecf762d9a4196c0fbfe19b142ac498990" + integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ== dependencies: - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/visitor-keys" "5.48.1" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/visitor-keys" "5.51.0" -"@typescript-eslint/type-utils@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz#5d94ac0c269a81a91ad77c03407cea2caf481412" - integrity sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ== +"@typescript-eslint/type-utils@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz#7af48005531700b62a20963501d47dfb27095988" + integrity sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ== dependencies: - "@typescript-eslint/typescript-estree" "5.48.1" - "@typescript-eslint/utils" "5.48.1" + "@typescript-eslint/typescript-estree" "5.51.0" + "@typescript-eslint/utils" "5.51.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.1.tgz#efd1913a9aaf67caf8a6e6779fd53e14e8587e14" - integrity sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg== +"@typescript-eslint/types@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.51.0.tgz#e7c1622f46c7eea7e12bbf1edfb496d4dec37c90" + integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw== -"@typescript-eslint/typescript-estree@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz#9efa8ee2aa471c6ab62e649f6e64d8d121bc2056" - integrity sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA== +"@typescript-eslint/typescript-estree@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz#0ec8170d7247a892c2b21845b06c11eb0718f8de" + integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA== dependencies: - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/visitor-keys" "5.48.1" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/visitor-keys" "5.51.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.1.tgz#20f2f4e88e9e2a0961cbebcb47a1f0f7da7ba7f9" - integrity sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA== +"@typescript-eslint/utils@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.51.0.tgz#074f4fabd5b12afe9c8aa6fdee881c050f8b4d47" + integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/scope-manager" "5.51.0" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/typescript-estree" "5.51.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz#79fd4fb9996023ef86849bf6f904f33eb6c8fccb" - integrity sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA== +"@typescript-eslint/visitor-keys@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz#c0147dd9a36c0de758aaebd5b48cae1ec59eba87" + integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ== dependencies: - "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/types" "5.51.0" eslint-visitor-keys "^3.3.0" abbrev@1: @@ -1057,6 +1089,11 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1149,7 +1186,7 @@ array.prototype.flat@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.0: +array.prototype.flatmap@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== @@ -1169,6 +1206,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1179,15 +1221,15 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -babel-jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" - integrity sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA== +babel-jest@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.2.tgz#b17b9f64be288040877cbe2649f91ac3b63b2ba6" + integrity sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ== dependencies: - "@jest/transform" "^29.3.1" + "@jest/transform" "^29.4.2" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.2.0" + babel-preset-jest "^29.4.2" chalk "^4.0.0" graceful-fs "^4.2.9" slash "^3.0.0" @@ -1203,10 +1245,10 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.2.0.tgz#23ee99c37390a98cfddf3ef4a78674180d823094" - integrity sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA== +babel-plugin-jest-hoist@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.2.tgz#22aa43e255230f02371ffef1cac7eedef58f60bc" + integrity sha512-5HZRCfMeWypFEonRbEkwWXtNS1sQK159LhRVyRuLzyfVBxDy/34Tr/rg4YVi0SScSJ4fqeaR/OIeceJ/LaQ0pQ== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -1231,12 +1273,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.2.0.tgz#3048bea3a1af222e3505e4a767a974c95a7620dc" - integrity sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA== +babel-preset-jest@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.4.2.tgz#f0b20c6a79a9f155515e72a2d4f537fe002a4e38" + integrity sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ== dependencies: - babel-plugin-jest-hoist "^29.2.0" + babel-plugin-jest-hoist "^29.4.2" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -1275,6 +1317,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1353,7 +1402,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1622,6 +1671,11 @@ diff-sequences@^29.3.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== +diff-sequences@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.2.tgz#711fe6bd8a5869fe2539cee4a5152425ff671fda" + integrity sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1653,6 +1707,13 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +ejs@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" @@ -1757,33 +1818,33 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild@^0.16.6: - version "0.16.17" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" - integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== +esbuild@^0.17.8: + version "0.17.8" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.8.tgz#f7f799abc7cdce3f0f2e3e0c01f120d4d55193b4" + integrity sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g== optionalDependencies: - "@esbuild/android-arm" "0.16.17" - "@esbuild/android-arm64" "0.16.17" - "@esbuild/android-x64" "0.16.17" - "@esbuild/darwin-arm64" "0.16.17" - "@esbuild/darwin-x64" "0.16.17" - "@esbuild/freebsd-arm64" "0.16.17" - "@esbuild/freebsd-x64" "0.16.17" - "@esbuild/linux-arm" "0.16.17" - "@esbuild/linux-arm64" "0.16.17" - "@esbuild/linux-ia32" "0.16.17" - "@esbuild/linux-loong64" "0.16.17" - "@esbuild/linux-mips64el" "0.16.17" - "@esbuild/linux-ppc64" "0.16.17" - "@esbuild/linux-riscv64" "0.16.17" - "@esbuild/linux-s390x" "0.16.17" - "@esbuild/linux-x64" "0.16.17" - "@esbuild/netbsd-x64" "0.16.17" - "@esbuild/openbsd-x64" "0.16.17" - "@esbuild/sunos-x64" "0.16.17" - "@esbuild/win32-arm64" "0.16.17" - "@esbuild/win32-ia32" "0.16.17" - "@esbuild/win32-x64" "0.16.17" + "@esbuild/android-arm" "0.17.8" + "@esbuild/android-arm64" "0.17.8" + "@esbuild/android-x64" "0.17.8" + "@esbuild/darwin-arm64" "0.17.8" + "@esbuild/darwin-x64" "0.17.8" + "@esbuild/freebsd-arm64" "0.17.8" + "@esbuild/freebsd-x64" "0.17.8" + "@esbuild/linux-arm" "0.17.8" + "@esbuild/linux-arm64" "0.17.8" + "@esbuild/linux-ia32" "0.17.8" + "@esbuild/linux-loong64" "0.17.8" + "@esbuild/linux-mips64el" "0.17.8" + "@esbuild/linux-ppc64" "0.17.8" + "@esbuild/linux-riscv64" "0.17.8" + "@esbuild/linux-s390x" "0.17.8" + "@esbuild/linux-x64" "0.17.8" + "@esbuild/netbsd-x64" "0.17.8" + "@esbuild/openbsd-x64" "0.17.8" + "@esbuild/sunos-x64" "0.17.8" + "@esbuild/win32-arm64" "0.17.8" + "@esbuild/win32-ia32" "0.17.8" + "@esbuild/win32-x64" "0.17.8" escalade@^3.1.1: version "3.1.1" @@ -1870,14 +1931,14 @@ eslint-module-utils@^2.7.4: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.26.0: - version "2.27.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.4.tgz#319c2f6f6580e1678d674a258ee5e981c10cc25b" - integrity sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA== +eslint-plugin-import@^2.27.5: + version "2.27.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" + integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== dependencies: array-includes "^3.1.6" array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.0" + array.prototype.flatmap "^1.3.1" debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.7" @@ -1898,10 +1959,10 @@ eslint-plugin-prettier@^4.2.1: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-simple-import-sort@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-7.0.0.tgz#a1dad262f46d2184a90095a60c66fef74727f0f8" - integrity sha512-U3vEDB5zhYPNfxT5TYR7u01dboFZp+HNpnGhkDB2g/2E4wZ/g1Q9Ton8UwCLfRV9yAKyYqDh62oHOamvkFxsvw== +eslint-plugin-simple-import-sort@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz#cc4ceaa81ba73252427062705b64321946f61351" + integrity sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw== eslint-scope@^5.1.1: version "5.1.1" @@ -1936,10 +1997,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.15.0: - version "8.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.32.0.tgz#d9690056bb6f1a302bd991e7090f5b68fbaea861" - integrity sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ== +eslint@^8.34.0: + version "8.34.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" + integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== dependencies: "@eslint/eslintrc" "^1.4.1" "@humanwhocodes/config-array" "^0.11.8" @@ -2064,7 +2125,7 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^29.0.0, expect@^29.3.1: +expect@^29.0.0: version "29.3.1" resolved "https://registry.yarnpkg.com/expect/-/expect-29.3.1.tgz#92877aad3f7deefc2e3f6430dd195b92295554a6" integrity sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA== @@ -2075,6 +2136,17 @@ expect@^29.0.0, expect@^29.3.1: jest-message-util "^29.3.1" jest-util "^29.3.1" +expect@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.2.tgz#2ae34eb88de797c64a1541ad0f1e2ea8a7a7b492" + integrity sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ== + dependencies: + "@jest/expect-utils" "^29.4.2" + jest-get-type "^29.4.2" + jest-matcher-utils "^29.4.2" + jest-message-util "^29.4.2" + jest-util "^29.4.2" + express@^4.18.1: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -2169,6 +2241,13 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +filelist@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -2426,6 +2505,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphql@^16.6.0: + version "16.6.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" + integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -2798,82 +2882,92 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" - integrity sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA== +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.1" + minimatch "^3.0.4" + +jest-changed-files@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.2.tgz#bee1fafc8b620d6251423d1978a0080546bc4376" + integrity sha512-Qdd+AXdqD16PQa+VsWJpxR3kN0JyOCX1iugQfx5nUgAsI4gwsKviXkpclxOK9ZnwaY2IQVHz+771eAvqeOlfuw== dependencies: execa "^5.0.0" p-limit "^3.1.0" -jest-circus@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.3.1.tgz#177d07c5c0beae8ef2937a67de68f1e17bbf1b4a" - integrity sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg== +jest-circus@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.4.2.tgz#2d00c04baefd0ee2a277014cd494d4b5970663ed" + integrity sha512-wW3ztp6a2P5c1yOc1Cfrt5ozJ7neWmqeXm/4SYiqcSriyisgq63bwFj1NuRdSR5iqS0CMEYwSZd89ZA47W9zUg== dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/environment" "^29.4.2" + "@jest/expect" "^29.4.2" + "@jest/test-result" "^29.4.2" + "@jest/types" "^29.4.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" is-generator-fn "^2.0.0" - jest-each "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" + jest-each "^29.4.2" + jest-matcher-utils "^29.4.2" + jest-message-util "^29.4.2" + jest-runtime "^29.4.2" + jest-snapshot "^29.4.2" + jest-util "^29.4.2" p-limit "^3.1.0" - pretty-format "^29.3.1" + pretty-format "^29.4.2" slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.3.1.tgz#e89dff427db3b1df50cea9a393ebd8640790416d" - integrity sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ== +jest-cli@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.4.2.tgz#94a2f913a0a7a49d11bee98ad88bf48baae941f4" + integrity sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q== dependencies: - "@jest/core" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/core" "^29.4.2" + "@jest/test-result" "^29.4.2" + "@jest/types" "^29.4.2" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-config "^29.4.2" + jest-util "^29.4.2" + jest-validate "^29.4.2" prompts "^2.0.1" yargs "^17.3.1" -jest-config@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.3.1.tgz#0bc3dcb0959ff8662957f1259947aedaefb7f3c6" - integrity sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg== +jest-config@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.4.2.tgz#15386dd9ed2f7059516915515f786b8836a98f07" + integrity sha512-919CtnXic52YM0zW4C1QxjG6aNueX1kBGthuMtvFtRTAxhKfJmiXC9qwHmi6o2josjbDz8QlWyY55F1SIVmCWA== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.3.1" - "@jest/types" "^29.3.1" - babel-jest "^29.3.1" + "@jest/test-sequencer" "^29.4.2" + "@jest/types" "^29.4.2" + babel-jest "^29.4.2" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.3.1" - jest-environment-node "^29.3.1" - jest-get-type "^29.2.0" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-runner "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-circus "^29.4.2" + jest-environment-node "^29.4.2" + jest-get-type "^29.4.2" + jest-regex-util "^29.4.2" + jest-resolve "^29.4.2" + jest-runner "^29.4.2" + jest-util "^29.4.2" + jest-validate "^29.4.2" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.3.1" + pretty-format "^29.4.2" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -2887,67 +2981,82 @@ jest-diff@^29.3.1: jest-get-type "^29.2.0" pretty-format "^29.3.1" -jest-docblock@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.2.0.tgz#307203e20b637d97cee04809efc1d43afc641e82" - integrity sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A== +jest-diff@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.2.tgz#b88502d5dc02d97f6512d73c37da8b36f49b4871" + integrity sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g== dependencies: - detect-newline "^3.0.0" + chalk "^4.0.0" + diff-sequences "^29.4.2" + jest-get-type "^29.4.2" + pretty-format "^29.4.2" -jest-each@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.3.1.tgz#bc375c8734f1bb96625d83d1ca03ef508379e132" - integrity sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA== +jest-docblock@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.2.tgz#c78a95eedf9a24c0a6cc16cf2abdc4b8b0f2531b" + integrity sha512-dV2JdahgClL34Y5vLrAHde3nF3yo2jKRH+GIYJuCpfqwEJZcikzeafVTGAjbOfKPG17ez9iWXwUYp7yefeCRag== dependencies: - "@jest/types" "^29.3.1" - chalk "^4.0.0" - jest-get-type "^29.2.0" - jest-util "^29.3.1" - pretty-format "^29.3.1" + detect-newline "^3.0.0" -jest-environment-node@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.3.1.tgz#5023b32472b3fba91db5c799a0d5624ad4803e74" - integrity sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag== +jest-each@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.2.tgz#e1347aff1303f4c35470827a62c029d389c5d44a" + integrity sha512-trvKZb0JYiCndc55V1Yh0Luqi7AsAdDWpV+mKT/5vkpnnFQfuQACV72IoRV161aAr6kAVIBpmYzwhBzm34vQkA== dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.2" + chalk "^4.0.0" + jest-get-type "^29.4.2" + jest-util "^29.4.2" + pretty-format "^29.4.2" + +jest-environment-node@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.4.2.tgz#0eab835b41e25fd0c1a72f62665fc8db08762ad2" + integrity sha512-MLPrqUcOnNBc8zTOfqBbxtoa8/Ee8tZ7UFW7hRDQSUT+NGsvS96wlbHGTf+EFAT9KC3VNb7fWEM6oyvmxtE/9w== + dependencies: + "@jest/environment" "^29.4.2" + "@jest/fake-timers" "^29.4.2" + "@jest/types" "^29.4.2" "@types/node" "*" - jest-mock "^29.3.1" - jest-util "^29.3.1" + jest-mock "^29.4.2" + jest-util "^29.4.2" jest-get-type@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== -jest-haste-map@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.3.1.tgz#af83b4347f1dae5ee8c2fb57368dc0bb3e5af843" - integrity sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A== +jest-get-type@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.2.tgz#7cb63f154bca8d8f57364d01614477d466fa43fe" + integrity sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg== + +jest-haste-map@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.4.2.tgz#9112df3f5121e643f1b2dcbaa86ab11b0b90b49a" + integrity sha512-WkUgo26LN5UHPknkezrBzr7lUtV1OpGsp+NfXbBwHztsFruS3gz+AMTTBcEklvi8uPzpISzYjdKXYZQJXBnfvw== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.2" "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.9" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" - jest-worker "^29.3.1" + jest-regex-util "^29.4.2" + jest-util "^29.4.2" + jest-worker "^29.4.2" micromatch "^4.0.4" walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-leak-detector@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz#95336d020170671db0ee166b75cd8ef647265518" - integrity sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA== +jest-leak-detector@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.4.2.tgz#8f05c6680e0cb46a1d577c0d3da9793bed3ea97b" + integrity sha512-Wa62HuRJmWXtX9F00nUpWlrbaH5axeYCdyRsOs/+Rb1Vb6+qWTlB5rKwCCRKtorM7owNwKsyJ8NRDUcZ8ghYUA== dependencies: - jest-get-type "^29.2.0" - pretty-format "^29.3.1" + jest-get-type "^29.4.2" + pretty-format "^29.4.2" jest-matcher-utils@^29.3.1: version "29.3.1" @@ -2959,6 +3068,16 @@ jest-matcher-utils@^29.3.1: jest-get-type "^29.2.0" pretty-format "^29.3.1" +jest-matcher-utils@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.2.tgz#08d0bf5abf242e3834bec92c7ef5071732839e85" + integrity sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg== + dependencies: + chalk "^4.0.0" + jest-diff "^29.4.2" + jest-get-type "^29.4.2" + pretty-format "^29.4.2" + jest-message-util@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.3.1.tgz#37bc5c468dfe5120712053dd03faf0f053bd6adb" @@ -2974,107 +3093,123 @@ jest-message-util@^29.3.1: slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e" - integrity sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA== +jest-message-util@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.2.tgz#309a2924eae6ca67cf7f25781a2af1902deee717" + integrity sha512-SElcuN4s6PNKpOEtTInjOAA8QvItu0iugkXqhYyguRvQoXapg5gN+9RQxLAkakChZA7Y26j6yUCsFWN+hlKD6g== dependencies: - "@jest/types" "^29.3.1" + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.4.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.4.2" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.4.2.tgz#e1054be66fb3e975d26d4528fcde6979e4759de8" + integrity sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g== + dependencies: + "@jest/types" "^29.4.2" "@types/node" "*" - jest-util "^29.3.1" + jest-util "^29.4.2" jest-pnp-resolver@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.2.0.tgz#82ef3b587e8c303357728d0322d48bbfd2971f7b" - integrity sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA== +jest-regex-util@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.2.tgz#19187cca35d301f8126cf7a021dd4dcb7b58a1ca" + integrity sha512-XYZXOqUl1y31H6VLMrrUL1ZhXuiymLKPz0BO1kEeR5xER9Tv86RZrjTm74g5l9bPJQXA/hyLdaVPN/sdqfteig== -jest-resolve-dependencies@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.3.1.tgz#a6a329708a128e68d67c49f38678a4a4a914c3bf" - integrity sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA== +jest-resolve-dependencies@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.2.tgz#6359db606f5967b68ca8bbe9dbc07a4306c12bf7" + integrity sha512-6pL4ptFw62rjdrPk7rRpzJYgcRqRZNsZTF1VxVTZMishbO6ObyWvX57yHOaNGgKoADtAHRFYdHQUEvYMJATbDg== dependencies: - jest-regex-util "^29.2.0" - jest-snapshot "^29.3.1" + jest-regex-util "^29.4.2" + jest-snapshot "^29.4.2" -jest-resolve@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.3.1.tgz#9a4b6b65387a3141e4a40815535c7f196f1a68a7" - integrity sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw== +jest-resolve@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.4.2.tgz#8831f449671d08d161fe493003f61dc9b55b808e" + integrity sha512-RtKWW0mbR3I4UdkOrW7552IFGLYQ5AF9YrzD0FnIOkDu0rAMlA5/Y1+r7lhCAP4nXSBTaE7ueeqj6IOwZpgoqw== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" + jest-haste-map "^29.4.2" jest-pnp-resolver "^1.2.2" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-util "^29.4.2" + jest-validate "^29.4.2" resolve "^1.20.0" - resolve.exports "^1.1.0" + resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.3.1.tgz#a92a879a47dd096fea46bb1517b0a99418ee9e2d" - integrity sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA== +jest-runner@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.4.2.tgz#2bcecf72303369df4ef1e6e983c22a89870d5125" + integrity sha512-wqwt0drm7JGjwdH+x1XgAl+TFPH7poowMguPQINYxaukCqlczAcNLJiK+OLxUxQAEWMdy+e6nHZlFHO5s7EuRg== dependencies: - "@jest/console" "^29.3.1" - "@jest/environment" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.4.2" + "@jest/environment" "^29.4.2" + "@jest/test-result" "^29.4.2" + "@jest/transform" "^29.4.2" + "@jest/types" "^29.4.2" "@types/node" "*" chalk "^4.0.0" emittery "^0.13.1" graceful-fs "^4.2.9" - jest-docblock "^29.2.0" - jest-environment-node "^29.3.1" - jest-haste-map "^29.3.1" - jest-leak-detector "^29.3.1" - jest-message-util "^29.3.1" - jest-resolve "^29.3.1" - jest-runtime "^29.3.1" - jest-util "^29.3.1" - jest-watcher "^29.3.1" - jest-worker "^29.3.1" + jest-docblock "^29.4.2" + jest-environment-node "^29.4.2" + jest-haste-map "^29.4.2" + jest-leak-detector "^29.4.2" + jest-message-util "^29.4.2" + jest-resolve "^29.4.2" + jest-runtime "^29.4.2" + jest-util "^29.4.2" + jest-watcher "^29.4.2" + jest-worker "^29.4.2" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.3.1.tgz#21efccb1a66911d6d8591276a6182f520b86737a" - integrity sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A== - dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/globals" "^29.3.1" - "@jest/source-map" "^29.2.0" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" +jest-runtime@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.4.2.tgz#d86b764c5b95d76cb26ed1f32644e99de5d5c134" + integrity sha512-3fque9vtpLzGuxT9eZqhxi+9EylKK/ESfhClv4P7Y9sqJPs58LjVhTt8jaMp/pRO38agll1CkSu9z9ieTQeRrw== + dependencies: + "@jest/environment" "^29.4.2" + "@jest/fake-timers" "^29.4.2" + "@jest/globals" "^29.4.2" + "@jest/source-map" "^29.4.2" + "@jest/test-result" "^29.4.2" + "@jest/transform" "^29.4.2" + "@jest/types" "^29.4.2" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" + jest-haste-map "^29.4.2" + jest-message-util "^29.4.2" + jest-mock "^29.4.2" + jest-regex-util "^29.4.2" + jest-resolve "^29.4.2" + jest-snapshot "^29.4.2" + jest-util "^29.4.2" + semver "^7.3.5" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.3.1.tgz#17bcef71a453adc059a18a32ccbd594b8cc4e45e" - integrity sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA== +jest-snapshot@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.4.2.tgz#ba1fb9abb279fd2c85109ff1757bc56b503bbb3a" + integrity sha512-PdfubrSNN5KwroyMH158R23tWcAXJyx4pvSvWls1dHoLCaUhGul9rsL3uVjtqzRpkxlkMavQjGuWG1newPgmkw== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" @@ -3082,23 +3217,23 @@ jest-snapshot@^29.3.1: "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/expect-utils" "^29.4.2" + "@jest/transform" "^29.4.2" + "@jest/types" "^29.4.2" "@types/babel__traverse" "^7.0.6" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.3.1" + expect "^29.4.2" graceful-fs "^4.2.9" - jest-diff "^29.3.1" - jest-get-type "^29.2.0" - jest-haste-map "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-util "^29.3.1" + jest-diff "^29.4.2" + jest-get-type "^29.4.2" + jest-haste-map "^29.4.2" + jest-matcher-utils "^29.4.2" + jest-message-util "^29.4.2" + jest-util "^29.4.2" natural-compare "^1.4.0" - pretty-format "^29.3.1" + pretty-format "^29.4.2" semver "^7.3.5" jest-util@^29.0.0, jest-util@^29.3.1: @@ -3113,51 +3248,63 @@ jest-util@^29.0.0, jest-util@^29.3.1: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.3.1.tgz#d56fefaa2e7d1fde3ecdc973c7f7f8f25eea704a" - integrity sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g== +jest-util@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.2.tgz#3db8580b295df453a97de4a1b42dd2578dabd2c2" + integrity sha512-wKnm6XpJgzMUSRFB7YF48CuwdzuDIHenVuoIb1PLuJ6F+uErZsuDkU+EiExkChf6473XcawBrSfDSnXl+/YG4g== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.2" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.4.2.tgz#3b3f8c4910ab9a3442d2512e2175df6b3f77b915" + integrity sha512-tto7YKGPJyFbhcKhIDFq8B5od+eVWD/ySZ9Tvcp/NGCvYA4RQbuzhbwYWtIjMT5W5zA2W0eBJwu4HVw34d5G6Q== + dependencies: + "@jest/types" "^29.4.2" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^29.2.0" + jest-get-type "^29.4.2" leven "^3.1.0" - pretty-format "^29.3.1" + pretty-format "^29.4.2" -jest-watcher@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.3.1.tgz#3341547e14fe3c0f79f9c3a4c62dbc3fc977fd4a" - integrity sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg== +jest-watcher@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.4.2.tgz#09c0f4c9a9c7c0807fcefb1445b821c6f7953b7c" + integrity sha512-onddLujSoGiMJt+tKutehIidABa175i/Ays+QvKxCqBwp7fvxP3ZhKsrIdOodt71dKxqk4sc0LN41mWLGIK44w== dependencies: - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/test-result" "^29.4.2" + "@jest/types" "^29.4.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.13.1" - jest-util "^29.3.1" + jest-util "^29.4.2" string-length "^4.0.1" -jest-worker@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.3.1.tgz#e9462161017a9bb176380d721cab022661da3d6b" - integrity sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw== +jest-worker@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.2.tgz#d9b2c3bafc69311d84d94e7fb45677fc8976296f" + integrity sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw== dependencies: "@types/node" "*" - jest-util "^29.3.1" + jest-util "^29.4.2" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.3.1.tgz#c130c0d551ae6b5459b8963747fed392ddbde122" - integrity sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA== +jest@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.4.2.tgz#4c2127d03a71dc187f386156ef155dbf323fb7be" + integrity sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg== dependencies: - "@jest/core" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/core" "^29.4.2" + "@jest/types" "^29.4.2" import-local "^3.0.2" - jest-cli "^29.3.1" + jest-cli "^29.4.2" js-sdsl@^4.1.4: version "4.2.0" @@ -3244,10 +3391,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lint-staged@^13.0.3: - version "13.1.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.1.0.tgz#d4c61aec939e789e489fa51987ec5207b50fd37e" - integrity sha512-pn/sR8IrcF/T0vpWLilih8jmVouMlxqXxKuAojmbiGX5n/gDnz+abdPptlj0vYnbfE0SQNl3CY/HwtM0+yfOVQ== +lint-staged@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.1.1.tgz#db61636850660e291a6885da65d8e79850bd8307" + integrity sha512-LLJLO0Kdbcv2a+CvSF4p1M7jBZOajKSMpBUvyR8+bXccsqPER0/NxTFQSpNHjqwV9kM3tkHczYerTB5wI+bksQ== dependencies: cli-truncate "^3.1.0" colorette "^2.0.19" @@ -3416,6 +3563,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" @@ -3726,10 +3880,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.7.1: - version "2.8.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.3.tgz#ab697b1d3dd46fb4626fbe2f543afe0cc98d8632" - integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw== +prettier@^2.8.3: + version "2.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" + integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== pretty-format@^29.0.0, pretty-format@^29.3.1: version "29.3.1" @@ -3740,6 +3894,15 @@ pretty-format@^29.0.0, pretty-format@^29.3.1: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.4.2: + version "29.4.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.2.tgz#64bf5ccc0d718c03027d94ac957bdd32b3fb2401" + integrity sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg== + dependencies: + "@jest/schemas" "^29.4.2" + ansi-styles "^5.0.0" + react-is "^18.0.0" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -3841,10 +4004,10 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve.exports@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" - integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== +resolve.exports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" + integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== resolve@^1.20.0, resolve@^1.22.1: version "1.22.1" @@ -3873,6 +4036,11 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rimraf@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.2.tgz#20dfbc98083bdfaa28b01183162885ef213dbf7c" + integrity sha512-BlIbgFryTbw3Dz6hyoWFhKk+unCcHMSkZGrTFVAx2WmttdBSonsdtRlwiuTbDqTKr+UlXIUqJVS4QT5tUzGENQ== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -4360,10 +4528,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^4.7.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@^4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== unbox-primitive@^1.0.2: version "1.0.2" @@ -4484,7 +4652,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^4.0.1: +write-file-atomic@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==