From 56bf43c2b2209ca516abe26c2fe7f89f1c987d5c Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Sun, 14 Jan 2024 20:53:06 +1100 Subject: [PATCH 01/13] Refactor how validators work, this achieves 3 objectives; 1. Types are way faster and are way less clunky to implement. 2. Validators are way more testable, and have a much higher coverage now. 3. We can customise error responses now. --- README.md | 4 +- docs/examples/simple/index.ts | 6 +- docs/examples/simple/petHandler.ts | 106 +++++------ src/constants.ts | 1 + src/document.spec.ts | 287 +---------------------------- src/requires/enhancers.spec.ts | 244 ++++++++++++++++++++++++ src/requires/enhancers.ts | 130 +++++++++++++ src/requires/index.ts | 15 +- src/requires/operation.ts | 40 ---- src/requires/requires.spec.ts | 217 ++++++++++++++++++++++ src/requires/requires.ts | 155 ++++++++++++++++ src/requires/requiresBody.ts | 28 --- src/requires/requiresMany.ts | 56 ------ src/requires/requiresParameter.ts | 101 ---------- src/requires/requiresResponse.ts | 64 ------- src/requires/validators.spec.ts | 278 ++++++++++++++++++++++++++++ src/requires/validators.ts | 88 +++++++++ src/types/haste.ts | 77 +++++++- src/types/index.ts | 76 ++++---- src/types/utilities.ts | 2 + 20 files changed, 1286 insertions(+), 689 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/requires/enhancers.spec.ts create mode 100644 src/requires/enhancers.ts delete mode 100644 src/requires/operation.ts create mode 100644 src/requires/requires.spec.ts create mode 100644 src/requires/requires.ts delete mode 100644 src/requires/requiresBody.ts delete mode 100644 src/requires/requiresMany.ts delete mode 100644 src/requires/requiresParameter.ts delete mode 100644 src/requires/requiresResponse.ts create mode 100644 src/requires/validators.spec.ts create mode 100644 src/requires/validators.ts diff --git a/README.md b/README.md index 03a0567..416d9ba 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,10 @@ full end-to-end examples of how express-haste works. ### Roadmap * [X] Request Handler typing. -* [ ] Improve test coverage. +* [X] Improve test coverage. * [X] Lint and Test checking in github actions. * [ ] Tests for typing (it's very fragile and hard to catch all edge cases manually). -* [ ] Explore whether typing can be made less complicated. +* [X] Explore whether typing can be made less complicated. * [ ] Ability to pass many parameters into one query, header, etc function call. ie; `query({q1: z.string(), q2: z.string()})`. * [ ] Ability to customize error response when the request fails. diff --git a/docs/examples/simple/index.ts b/docs/examples/simple/index.ts index b586fdc..05d4225 100644 --- a/docs/examples/simple/index.ts +++ b/docs/examples/simple/index.ts @@ -10,10 +10,10 @@ import express, { Router, json } from 'express'; import { document } from 'express-haste'; import cookieParser from 'cookie-parser'; import { getRedocHtml } from "./redoc"; -import { header } from 'express-haste'; import { UsernamePasswordAuth } from './schemas'; +import { requires } from '../../../src'; -const app = express(); +const app: express.Express = express(); app.use(json()); app.use(cookieParser()); @@ -26,7 +26,7 @@ app.use('/docs', docRouter) app.get('/pet/:id', getOnePetRequirements, getOnePet); // Require an authorization header -app.use(header('authorization', UsernamePasswordAuth)) +app.use(requires().header('authorization', UsernamePasswordAuth)) app.post('/pets/:id', createPetRequirements, createPetHandler); app.get('/pets', searchPetRequirements, searchPets); diff --git a/docs/examples/simple/petHandler.ts b/docs/examples/simple/petHandler.ts index cbfea4b..34b5f55 100644 --- a/docs/examples/simple/petHandler.ts +++ b/docs/examples/simple/petHandler.ts @@ -1,70 +1,64 @@ import { z } from 'zod'; -import { body, HasteRequestHandler, header, path, query, requires, response } from "express-haste"; +import { HasteRequestHandler, requires } from 'express-haste'; import { - AsyncCreationRequest, - JobAcceptedSchema, - PetId, - PetSchema, - PetWithIdSchema, UsernamePasswordAuth -} from "./schemas"; + AsyncCreationRequest, + JobAcceptedSchema, + PetId, + PetSchema, + PetWithIdSchema, +} from './schemas'; - -export const searchPetRequirements = requires( - query('id', PetId), - query('async', AsyncCreationRequest), - response('200', PetWithIdSchema), - response('202', JobAcceptedSchema), -); +export const searchPetRequirements = requires() + .body(z.object({})) + .query('id', PetId) + .query('async', AsyncCreationRequest) + .response('200', PetWithIdSchema) + .response('202', JobAcceptedSchema); export const searchPets: HasteRequestHandler = (req, res) => { - if (req.query.async) { - return res.status(202).json({ - status: 202, - title: 'accepted', - details: '/job/8b280029-dec0-4b75-9027-2c737a38c8a3', - }); - } - - res.status(200).json({ - id: req.query.id, - type: 'cat', - breed: 'burmese', - vaccinated: true, + if (req.query.async === true) { + return res.status(200).json({ + status: 202, + title: 'accepted', + details: '/job/8b280029-dec0-4b75-9027-2c737a38c8a3', }); + } + + res.status(200).json({ + id: req.query.id, + type: 'cat', + breed: 'burmese', + vaccinated: true, + }); }; -export const getOnePetRequirements = requires( - path('id', PetId), - response('200', PetWithIdSchema) -); +export const getOnePetRequirements = requires().response('200', PetWithIdSchema).path('id', PetId); export const getOnePet: HasteRequestHandler = (req, res) => { - res.status(200).json({ - id: req.params.id, - type: 'cat', - breed: 'burmese', - vaccinated: true, - }); + res.status(200).json({ + id: req.params.id, + type: 'cat', + breed: 'burmese', + vaccinated: true, + }); }; -export const createPetRequirements = requires( - body(PetSchema), - path('id', z.string()), - query('async', AsyncCreationRequest), - response('202', JobAcceptedSchema), - response('201', JobAcceptedSchema), -); - +export const createPetRequirements = requires() + .body(PetSchema) + .path('id', z.string()) + .query('async', AsyncCreationRequest) + .response('202', JobAcceptedSchema) + .response('201', JobAcceptedSchema); export const createPetHandler: HasteRequestHandler = (req, res) => { - if (req.query.async) { - return res.status(202).json({ - status: 202, - title: 'accepted', - details: '/job/8b280029-dec0-4b75-9027-2c737a38c8a3', - }); - } - res.status(200).json({ - status: 201, - title: 'created', - details: `${ req.body.type }/breeds/${ req.body.breed }`, + if (req.query.async) { + return res.status(202).json({ + status: 202, + title: 'accepted', + details: '/job/8b280029-dec0-4b75-9027-2c737a38c8a3', }); + } + res.status(201).json({ + status: 201, + title: 'created', + details: `${req.body.type}/breeds/${req.body.breed}`, + }); }; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..216b88b --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_CONTENT_TYPE = 'application/json'; diff --git a/src/document.spec.ts b/src/document.spec.ts index ff2321f..04c901a 100644 --- a/src/document.spec.ts +++ b/src/document.spec.ts @@ -1,286 +1 @@ -import { addRouteToDocument, BadRequest, document, isHasteOperation } from './document'; -import { z } from 'zod'; -import { body, header, query, response } from './requires'; -import { HasteOperation } from './types'; -import express from 'express'; - -describe('document', () => { - describe('isHasteOperation', () => { - it('should return true when is a haste operation', () => { - expect(isHasteOperation(query('key', z.string()))).toBe(true); - }); - - it('should return false for normal functions', () => { - expect(isHasteOperation(() => {})).toBe(false); - }); - }); - - describe('addRouteToDocument', () => { - beforeEach(() => jest.resetAllMocks()); - - const makeLayer = (handler: HasteOperation) => ({ - route: { - path: '/test', - methods: { get: true }, - stack: [ - { - handle: { - ...handler, - }, - }, - ], - }, - }); - - it('should call enhancer on a query operation and add a query', () => { - const op = query('key', z.string()); - const mockEnhancer = jest.spyOn(op, '_enhancer'); - const simpleLayer = makeLayer(op); - const spec = {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(addRouteToDocument(spec, simpleLayer as any)).toEqual({ - '/test': { - get: { - requestParams: { - query: expect.objectContaining({ - shape: op._effects.query, - }), - }, - responses: { - 400: BadRequest, - }, - }, - }, - }); - expect(mockEnhancer).toHaveBeenCalledTimes(1); - expect(mockEnhancer).toHaveBeenNthCalledWith(1, { - responses: { - 400: BadRequest, - }, - }); - }); - - it('should call enhancer on a body operation and add a request body', () => { - const op = body(z.string(), { - contentType: 'text/plain', - }); - const mockEnhancer = jest.spyOn(op, '_enhancer'); - const simpleLayer = makeLayer(op); - const spec = {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(addRouteToDocument(spec, simpleLayer as any)).toEqual({ - '/test': { - get: { - requestBody: { content: { 'text/plain': { schema: op._effects.body } } }, - responses: { - 400: BadRequest, - }, - }, - }, - }); - expect(mockEnhancer).toHaveBeenCalledTimes(1); - expect(mockEnhancer).toHaveBeenNthCalledWith(1, { - responses: { - 400: BadRequest, - }, - }); - }); - - it('should call enhancer on a response operation and add a response', () => { - const op = response('500', z.literal('You cannot do that'), { contentType: 'text/plain' }); - const mockEnhancer = jest.spyOn(op, '_enhancer'); - const simpleLayer = makeLayer(op); - const spec = {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(addRouteToDocument(spec, simpleLayer as any)).toEqual({ - '/test': { - get: { - responses: { - 400: BadRequest, - 500: { - content: { - 'text/plain': { - schema: op._effects.response[0].schema, - }, - }, - }, - }, - }, - }, - }); - expect(mockEnhancer).toHaveBeenCalledTimes(1); - expect(mockEnhancer).toHaveBeenNthCalledWith(1, { - responses: { - 400: BadRequest, - }, - }); - }); - it('should handle nested layers', () => { - const op1 = response('401', z.literal("can't touch this"), { contentType: 'text/plain' }); - const op2 = response('401', z.object({ message: z.literal("can't touch this") }), { - contentType: 'application/problem+json', - }); - const mockEnhancer1 = jest.spyOn(op1, '_enhancer'); - const mockEnhancer2 = jest.spyOn(op2, '_enhancer'); - - const spec = {}; - - expect( - addRouteToDocument(spec, { - route: { - path: '/root', - methods: { get: true }, - stack: [makeLayer(op1), makeLayer(op2)], - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ).toEqual({ - '/root': { - get: { - responses: { - 400: BadRequest, - 401: { - content: { - 'text/plain': { - schema: op1._effects.response[0].schema, - }, - 'application/problem+json': { - schema: op2._effects.response[0].schema, - }, - }, - }, - }, - }, - }, - }); - expect(mockEnhancer1).toHaveBeenCalledTimes(1); - expect(mockEnhancer2).toHaveBeenCalledTimes(1); - expect(mockEnhancer1).toHaveBeenNthCalledWith(1, { - responses: { - 400: BadRequest, - }, - }); - }); - }); - - describe('document', () => { - const expectedBadRequest = { - '400-bad-request': { - description: '400 BAD REQUEST', - content: { - 'application/problem+validation+json': { - schema: { - type: 'object', - properties: { - type: { - type: 'string', - default: 'about:blank', - description: 'A URI reference [RFC3986] that identifies the problem type.', - example: 'about:blank', - }, - title: { - type: 'string', - description: 'A short, human-readable summary of the problem type.', - example: 'Bad Request.', - }, - detail: { - type: 'string', - description: - 'A human-readable explanation specific to this occurrence of the problem.', - example: 'The request did not match the required Schema.', - }, - status: { - type: 'number', - description: 'The HTTP status of this request', - example: 400, - }, - instance: { type: 'string' }, - issues: { - type: 'array', - items: { - type: 'object', - properties: { - type: { - type: 'string', - default: 'https://zod.dev/error_handling?id=zodissuecode', - description: 'A URI reference [RFC3986] that identifies the problem type.', - }, - code: { - type: 'string', - description: 'A zod issue code, indicating what went wrong.', - }, - path: { - type: 'array', - items: { type: 'string' }, - description: 'A path/array pointing to the location of the problem.', - examples: [ - ['body', 'key of body'], - ['query', 'key of query'], - ['header', 'key of header'], - ['cookie', 'key of cookie'], - ['path', 'key of path'], - ], - }, - message: { - type: 'string', - description: - 'A human-readable description pointing to the source of the problem', - }, - }, - required: ['type', 'message'], - }, - }, - }, - required: ['type', 'title', 'detail', 'issues'], - }, - }, - }, - }, - }; - const expectedOut = { - openapi: '3.0.0', - info: { title: 'test', version: '0.0.1' }, - paths: { - '/test': { - get: { - parameters: [{ in: 'header', name: 'key', schema: { type: 'string' }, required: true }], - requestBody: { - content: { - 'application/somecustomformat+json': { - schema: { - type: 'object', - additionalProperties: { type: 'boolean' }, - }, - }, - }, - }, - responses: { - 400: { $ref: '#/components/responses/400-bad-request' }, - }, - }, - }, - }, - components: { - responses: { - ...expectedBadRequest, - }, - }, - }; - - it('should create the correct schema for when use() is called', () => { - const app = express(); - app.use(header('key', z.string())); - app.get( - '/test', - body(z.record(z.boolean()), { contentType: 'application/somecustomformat+json' }) - ); - - expect( - document(app, { - openApiVersion: '3.0.0', - info: { title: 'test', version: '0.0.1' }, - }) - ).toEqual(expectedOut); - }); - }); -}); +describe('document', () => {}); diff --git a/src/requires/enhancers.spec.ts b/src/requires/enhancers.spec.ts new file mode 100644 index 0000000..4274995 --- /dev/null +++ b/src/requires/enhancers.spec.ts @@ -0,0 +1,244 @@ +import { requires } from './requires'; +import { z, ZodObject } from 'zod'; +import { enhanceAll } from './enhancers'; +import { + ZodOpenApiOperationObject, + ZodOpenApiParameters, +} from 'zod-openapi/lib-types/create/document'; + +describe('enhancers', () => { + describe('body', () => { + const bodySchema = z.literal('example'); + it('should return an empty object when no effect', () => { + const body = requires(); + expect(enhanceAll(body._effects, {} as ZodOpenApiOperationObject)).toEqual({}); + }); + it('should return the correct schema for requires().body()', () => { + const body = requires().body(bodySchema); + expect(enhanceAll(body._effects, {} as ZodOpenApiOperationObject)).toEqual({ + requestBody: { + content: { + 'application/json': { + schema: bodySchema, + }, + }, + }, + }); + }); + it('should return the correct schema for requires().body() when a custom contentType is provided', () => { + const body = requires().body(bodySchema, { contentType: 'test/example' }); + expect(enhanceAll(body._effects, {} as ZodOpenApiOperationObject)).toEqual({ + requestBody: { + content: { + 'test/example': { + schema: bodySchema, + }, + }, + }, + }); + }); + it('should return the last schema when many body are provided', () => { + const body = requires() + .body(z.number(), { contentType: 'wrong/body' }) + .body(bodySchema, { contentType: 'test/example' }); + expect(enhanceAll(body._effects, {} as ZodOpenApiOperationObject)).toEqual({ + requestBody: { + content: { + 'test/example': { + schema: bodySchema, + }, + }, + }, + }); + }); + }); + describe('responses', () => { + const responseSchema = z.literal('example'); + it('should return an empty object when no effect', () => { + const response = requires(); + expect(enhanceAll(response._effects, {} as ZodOpenApiOperationObject)).toEqual({}); + }); + it('should return the correct schema for requires().response()', () => { + const response = requires().response('200', responseSchema); + expect(enhanceAll(response._effects, {} as ZodOpenApiOperationObject)).toEqual({ + responses: { + [200]: { + description: undefined, + content: { + 'application/json': { + schema: responseSchema, + }, + }, + }, + }, + }); + }); + it('should return the correct schema for requires().response() when a custom contentType is provided', () => { + const body = requires().response('202', responseSchema, { contentType: 'test/example' }); + expect(enhanceAll(body._effects, {} as ZodOpenApiOperationObject)).toEqual({ + responses: { + [202]: { + description: undefined, + content: { + 'test/example': { + schema: responseSchema, + }, + }, + }, + }, + }); + }); + it('should be able to add a description to the response requires()', () => { + const body = requires().response('400', responseSchema, { description: 'some example' }); + expect(enhanceAll(body._effects, {} as ZodOpenApiOperationObject)).toEqual({ + responses: { + [400]: { + description: 'some example', + content: { + 'application/json': { + schema: responseSchema, + }, + }, + }, + }, + }); + }); + it('should be able to add to existing responses in the spec', () => { + const body = requires().response('400', responseSchema, { description: 'some example' }); + expect( + enhanceAll(body._effects, { + responses: { + [100]: { + description: 'continue', + content: { + 'application/json': { + schema: responseSchema, + }, + }, + }, + [400]: { + description: 'An error', + content: { + 'application/problem+json': { + schema: responseSchema, + }, + }, + }, + }, + } as ZodOpenApiOperationObject) + ).toEqual({ + responses: { + [100]: { + description: 'continue', + content: { + 'application/json': { + schema: responseSchema, + }, + }, + }, + [400]: { + description: 'some example', + content: { + 'application/json': { + schema: responseSchema, + }, + 'application/problem+json': { + schema: responseSchema, + }, + }, + }, + }, + }); + }); + it('should be able to provide many responses in one requires', () => { + const body = requires() + .response('400', responseSchema, { description: 'some example' }) + .response('400', responseSchema, { + description: 'overrides 400 error description', + contentType: 'application/problem+json', + }) + .response('100', responseSchema, { description: 'continue' }); + expect(enhanceAll(body._effects, {} as ZodOpenApiOperationObject)).toEqual({ + responses: { + [100]: { + description: 'continue', + content: { + 'application/json': { + schema: responseSchema, + }, + }, + }, + [400]: { + description: 'overrides 400 error description', + content: { + 'application/json': { + schema: responseSchema, + }, + 'application/problem+json': { + schema: responseSchema, + }, + }, + }, + }, + }); + }); + }); + describe.each(['query', 'header', 'cookie', 'path'] as const)('%s', (elem) => { + const exampleSchema = z.literal('example'); + const exampleSchema2 = z.literal('new'); + it('should return an empty object when no effect', () => { + const req = requires(); + expect(enhanceAll(req._effects, {} as ZodOpenApiOperationObject)).toEqual({}); + }); + it(`should return the correct schema for requires().${elem}()`, () => { + const value = requires()[elem]('test', exampleSchema); + const spec = enhanceAll(value._effects, {} as ZodOpenApiOperationObject); + expect(spec).toEqual({ + requestParams: { + [elem]: expect.any(ZodObject), + }, + }); + expect(spec?.requestParams?.[elem]?.shape).toEqual({ + test: exampleSchema, + }); + }); + it(`should preserve existing schemas for ${elem}`, () => { + const value = requires()[elem]('test', exampleSchema); + const spec = enhanceAll(value._effects, { + requestParams: { + [elem]: z.object({ + example: exampleSchema2, + }), + } as ZodOpenApiParameters, + } as ZodOpenApiOperationObject); + expect(spec).toEqual({ + requestParams: { + [elem]: expect.any(ZodObject), + }, + }); + expect(spec?.requestParams?.[elem]?.shape).toEqual({ + test: exampleSchema, + example: exampleSchema2, + }); + }); + it(`should return the correct schema for many requires().${elem}()`, () => { + const number = z.number(); + const boolean = z.boolean(); + const value = requires() + [elem]('test', exampleSchema) + [elem]('test2', number) + [elem]('another', boolean); + const spec = enhanceAll(value._effects, {} as ZodOpenApiOperationObject); + expect(spec).toEqual({ + requestParams: { + [elem]: expect.any(ZodObject), + }, + }); + expect(spec?.requestParams?.[elem]?.shape).toEqual({ + test: exampleSchema, + test2: number, + another: boolean, + }); + }); + }); +}); diff --git a/src/requires/enhancers.ts b/src/requires/enhancers.ts new file mode 100644 index 0000000..e751488 --- /dev/null +++ b/src/requires/enhancers.ts @@ -0,0 +1,130 @@ +import { HasteEffect, StatusCode } from '../types'; +import { AnyZodObject, z, ZodType } from 'zod'; +import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; +import { constant, pipe } from 'fp-ts/function'; +import { array, option } from 'fp-ts'; +import { ParameterLocation } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; +import { ZodOpenApiResponseObject } from 'zod-openapi'; +import { DEFAULT_CONTENT_TYPE } from '../constants'; +import { isZodType, mergeDeep } from '../utils'; + +const bodyEnhancer: Enhancer = (effects: HasteEffect) => + pipe( + effects.body, + option.fromNullable, + option.map((body) => ({ + requestBody: { + content: { + [body.config?.contentType || 'application/json']: { + schema: body.schema, + }, + }, + }, + })), + option.getOrElse(constant({})) + ); + +const parameterEnhancer = + (location: ParameterLocation): Enhancer => + (effects, operation) => + pipe( + ['query', 'path', 'header', 'cookie'] as ParameterLocation[], + array.filterMap((key) => + pipe( + effects[key], + option.fromPredicate((v) => key === location && !!v), + option.map((param) => ({ + [key]: mergeAndMap(param?.shape, operation.requestParams?.[key]), + })) + ) + ), + array.reduce({}, (a, b) => ({ ...a, ...b })), + (schema) => ({ requestParams: schema }) + ); + +const mergeAndMap = ( + newValue: Record | undefined, + oldValue: AnyZodObject | undefined +) => + pipe( + newValue, + option.fromNullable, + option.map((newSchema) => + pipe( + oldValue, + option.fromNullable, + option.getOrElse(() => z.object({})), + (oldSchema) => oldSchema.extend(newSchema) + ) + ), + option.getOrElseW(() => undefined) + ); + +const responseEnhancer: Enhancer = (effects, operation) => + pipe( + effects.response, + option.fromNullable, + option.map( + array.reduce(operation, (op, { status, config, schema }) => + pipe( + getResponseFromOperation(status, config, op), + option.fromNullable, + option.map((existingSchema) => existingSchema.or(schema)), + option.getOrElse(constant(schema)), + (newSchema) => ({ + responses: { + [status]: { + description: config?.description, + content: { + [config?.contentType || DEFAULT_CONTENT_TYPE]: { + schema: newSchema, + }, + }, + }, + }, + }), + (spec) => mergeDeep(op, spec) + ) + ) + ), + option.getOrElse(constant({})) + ); + +const getResponseFromOperation = ( + status: StatusCode, + config: Record | undefined, + operation: ZodOpenApiOperationObject +) => + pipe( + pipe( + operation.responses?.[status], + option.fromPredicate((v): v is ZodOpenApiResponseObject => !!v && 'content' in v), + option.chain((statusConfig) => + option.fromNullable(statusConfig.content?.[config?.contentType || DEFAULT_CONTENT_TYPE]) + ), + option.chain(({ schema }) => option.fromPredicate(isZodType)(schema)), + option.getOrElseW(constant(undefined)) + ) + ); + +type Enhancer = ( + effects: HasteEffect, + operation: ZodOpenApiOperationObject +) => Partial; +export const enhancerMapping: Record = { + body: bodyEnhancer, + response: responseEnhancer, + query: parameterEnhancer('query'), + path: parameterEnhancer('path'), + header: parameterEnhancer('header'), + cookie: parameterEnhancer('cookie'), +}; + +export const enhanceAll = ( + effect: HasteEffect, + operation: ZodOpenApiOperationObject +): Partial => + pipe( + Object.keys(effect) as (keyof HasteEffect)[], + array.reduce({}, (result, key) => mergeDeep(result, enhancerMapping[key](effect, operation))) + ); diff --git a/src/requires/index.ts b/src/requires/index.ts index 0c77ed6..7862069 100644 --- a/src/requires/index.ts +++ b/src/requires/index.ts @@ -1,14 +1 @@ -import { requiresParameter } from './requiresParameter'; -import { requiresBody } from './requiresBody'; - -export const body = requiresBody; - -export const path = requiresParameter('path'); -export const cookie = requiresParameter('cookie'); -export const query = requiresParameter('query'); -export const header = requiresParameter('header'); - -export { requiresResponse as response } from './requiresResponse'; -export { requiresMany as requires } from './requiresMany'; - -export * from './operation'; +export { requires } from './requires'; diff --git a/src/requires/operation.ts b/src/requires/operation.ts deleted file mode 100644 index c556a06..0000000 --- a/src/requires/operation.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { HasteEffect, HasteEnhancer, HasteOperation, HasteValidator } from '../types'; -import { Handler } from 'express'; -import { fold } from 'fp-ts/Either'; -import { pipe } from 'fp-ts/function'; -import { zodToRfcError } from '../utils'; - -export const createHasteOperation = ( - effects: E, - validator: HasteValidator, - enhancer: HasteEnhancer -): HasteOperation => { - const Operation: Omit, '()'> = { - _hastens: true, - _enhancer: enhancer, - _validator: validator, - _effects: effects, - }; - - return Object.assign( - (function (this: HasteOperation, req, res, next) { - pipe( - req, - this._validator, - fold( - (e) => { - res - .status(400) - .contentType('application/problem+validation+json') - .json(zodToRfcError(e)) - .send(); - }, - () => { - next(); - } - ) - ); - }).bind(Operation), - Operation - ); -}; diff --git a/src/requires/requires.spec.ts b/src/requires/requires.spec.ts new file mode 100644 index 0000000..6ab1958 --- /dev/null +++ b/src/requires/requires.spec.ts @@ -0,0 +1,217 @@ +import { requires } from './requires'; +import { validateAll } from './validators'; +import { either } from 'fp-ts'; +import express from 'express'; +import { z, ZodBoolean, ZodNumber, ZodObject, ZodString } from 'zod'; +import { enhanceAll } from './enhancers'; +import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; + +jest.mock('./validators'); +jest.mock('./enhancers'); +describe('requires', () => { + afterAll(() => jest.resetAllMocks()); + it('should be callable and have expected functions', () => { + const r = requires(); + expect(r).toEqual(expect.any(Function)); + expect(r._validator).toEqual(expect.any(Function)); + expect(r._enhancer).toEqual(expect.any(Function)); + expect(r._hastens).toBe(true); + }); + + const dummyZodError = { + issues: [ + { + code: 'invalid_literal', + expected: 'example', + message: 'Invalid literal value, expected "example"', + path: ['body', 'test1'], + }, + ], + }; + + describe('middleware function', () => { + it('should call validateAll when the class is used as a middleware', () => { + const requirements = requires().body(z.string()); + const validateAllReturn = either.right({}); + const request = {} as express.Request; + const response = {} as express.Response; + const next = jest.fn(); + + (validateAll as jest.Mock).mockReturnValue(validateAllReturn); + expect(requirements(request, response, next)).toEqual(undefined); + expect(validateAll).toHaveBeenCalledWith(requirements._effects, request); + expect(next).toHaveBeenCalled(); + }); + it('should return an error response when the class is used as a middleware and validation returns a left', () => { + const requirements = requires().body(z.string()); + const validateAllReturn = either.left(dummyZodError); + const request = {} as express.Request; + const response = { + status: jest.fn(), + contentType: jest.fn(), + json: jest.fn(), + send: jest.fn(), + } as unknown as express.Response; + Object.values(response).forEach((v: jest.Mock) => v.mockReturnValue(response)); + const next = jest.fn(); + + (validateAll as jest.Mock).mockReturnValue(validateAllReturn); + expect(requirements(request, response, next)).toEqual(undefined); + expect(validateAll).toHaveBeenCalledWith(requirements._effects, request); + expect(next).not.toHaveBeenCalled(); + expect(response.status).toHaveBeenCalledWith(400); + expect(response.contentType).toHaveBeenCalledWith('application/problem+validation+json'); + expect(response.json).toHaveBeenCalledWith({ + detail: 'Request failed to validate', + issues: [ + { + code: 'invalid_literal', + message: 'Invalid literal value, expected "example"', + path: ['body', 'test1'], + type: 'https://zod.dev/error_handling?id=zodissuecode', + }, + ], + title: 'Bad request', + type: 'about:blank', + }); + expect(response.send).toHaveBeenCalled(); + }); + it('should return a custom error response when a custom errorHandler is provided', () => { + const errorHandler = jest.fn(); + const requirements = requires({ errorHandler }).body(z.string()); + const validateAllReturn = either.left(dummyZodError); + const request = {} as express.Request; + const response = { + status: jest.fn(), + contentType: jest.fn(), + json: jest.fn(), + send: jest.fn(), + } as unknown as express.Response; + Object.values(response).forEach((v: jest.Mock) => v.mockReturnValue(response)); + const next = jest.fn(); + + (validateAll as jest.Mock).mockReturnValue(validateAllReturn); + expect(requirements(request, response, next)).toEqual(undefined); + expect(validateAll).toHaveBeenCalledWith(requirements._effects, request); + expect(next).not.toHaveBeenCalled(); + expect(errorHandler).toHaveBeenCalledWith(dummyZodError, response, next); + expect(response.status).not.toHaveBeenCalled(); + expect(response.contentType).not.toHaveBeenCalled(); + expect(response.json).not.toHaveBeenCalled(); + expect(response.send).not.toHaveBeenCalled(); + }); + }); + + describe('builders', () => { + const spec = requires(); + it('should correctly add a body effect', () => { + expect(spec.body(z.string())).toHaveProperty( + '_effects', + expect.objectContaining({ + body: { config: {}, schema: expect.any(ZodString) }, + }) + ); + }); + it('should override existing body effect', () => { + expect(spec.body(z.object({ test: z.boolean() }))).toHaveProperty( + '_effects', + expect.objectContaining({ + body: { config: {}, schema: expect.any(ZodObject) }, + }) + ); + }); + + it('should correctly add a response effect', () => { + const result = spec.response('500', z.string()); + expect(result).toHaveProperty( + '_effects', + expect.objectContaining({ + response: expect.arrayContaining([ + { status: '500', config: {}, schema: expect.any(ZodString) }, + ]), + }) + ); + }); + it('should add a second response effect', () => { + const result = spec.response('200', z.string(), { contentType: 'any/any' }); + expect(result).toHaveProperty( + '_effects', + expect.objectContaining({ + response: expect.arrayContaining([ + { status: '500', config: {}, schema: expect.any(ZodString) }, + { status: '200', config: { contentType: 'any/any' }, schema: expect.any(ZodString) }, + ]), + }) + ); + expect(result._effects).toHaveProperty('body'); + }); + it('should correctly add a query effect', () => { + const result = spec.query('test', z.number()); + expect(result).toHaveProperty( + '_effects', + expect.objectContaining({ + query: expect.objectContaining({ shape: { test: expect.any(ZodNumber) } }), + }) + ); + expect(result._effects).toHaveProperty('body', expect.any(Object)); + expect(result._effects).toHaveProperty('response', expect.any(Array)); + }); + it('should correctly add a path effect', () => { + const result = spec.path('example', z.boolean()); + expect(result).toHaveProperty( + '_effects', + expect.objectContaining({ + path: expect.objectContaining({ shape: { example: expect.any(ZodBoolean) } }), + }) + ); + expect(result._effects).toHaveProperty('body', expect.any(Object)); + expect(result._effects).toHaveProperty('response', expect.any(Array)); + expect(result._effects).toHaveProperty('query', expect.any(ZodObject)); + }); + it('should correctly add a header effect', () => { + const result = spec.header('x-example', z.string()); + expect(result).toHaveProperty( + '_effects', + expect.objectContaining({ + header: expect.objectContaining({ shape: { 'x-example': expect.any(ZodString) } }), + }) + ); + expect(result._effects).toHaveProperty('body', expect.any(Object)); + expect(result._effects).toHaveProperty('response', expect.any(Array)); + expect(result._effects).toHaveProperty('query', expect.any(ZodObject)); + expect(result._effects).toHaveProperty('path', expect.any(ZodObject)); + }); + it('should correctly add a cookie effect', () => { + const result = spec.cookie('some_cookie', z.string()); + expect(result).toHaveProperty( + '_effects', + expect.objectContaining({ + cookie: expect.objectContaining({ shape: { some_cookie: expect.any(ZodString) } }), + }) + ); + expect(result._effects).toHaveProperty('body', expect.any(Object)); + expect(result._effects).toHaveProperty('response', expect.any(Array)); + expect(result._effects).toHaveProperty('query', expect.any(ZodObject)); + expect(result._effects).toHaveProperty('path', expect.any(ZodObject)); + expect(result._effects).toHaveProperty('header', expect.any(ZodObject)); + }); + }); + + it('should call validateAll when _validator is called', () => { + const requirements = requires().body(z.string()); + const validateAllReturn = either.right(undefined); + const request = {} as express.Request; + (validateAll as jest.Mock).mockReturnValue(validateAllReturn); + expect(requirements._validator(request)).toEqual(validateAllReturn); + expect(validateAll).toHaveBeenCalledWith(requirements._effects, request); + }); + + it('should call enhanceAll when _enhancer is called', () => { + const requirements = requires().body(z.string()); + const returnValue = { some: 'spec object' }; + const operation = {} as ZodOpenApiOperationObject; + (enhanceAll as jest.Mock).mockReturnValue(returnValue); + expect(requirements._enhancer(operation)).toEqual(returnValue); + expect(enhanceAll).toHaveBeenCalledWith(requirements._effects, operation); + }); +}); diff --git a/src/requires/requires.ts b/src/requires/requires.ts new file mode 100644 index 0000000..651e376 --- /dev/null +++ b/src/requires/requires.ts @@ -0,0 +1,155 @@ +import { + BodyConfig, + ExtendEffect, + HasteEffect, + MergeTwoResponses, + RequirementConfig, + Requires, + ResponseConfig, + StatusCode, +} from '../types'; +import { constant, pipe } from 'fp-ts/function'; +import { option } from 'fp-ts'; +import { z, ZodError, ZodType } from 'zod'; +import { enhanceAll } from './enhancers'; +import { validateAll } from './validators'; +import { fold } from 'fp-ts/Either'; +import { zodToRfcError } from '../utils'; +import express, { NextFunction } from 'express'; + +export function requires< + Effect extends HasteEffect = Record, + Config extends RequirementConfig = RequirementConfig, +>(config?: Config, _effects?: Effect) { + const requires = { + _effects: _effects || ({} as Effect), + _config: config || {}, + _hastens: true, + _enhancer(operation) { + return enhanceAll(this._effects, operation); + }, + _validator(req) { + return validateAll(this._effects, req); + }, + + body(schema: Body, config?: Config) { + return enhanceRequirement(this, { body: { schema, config: config || ({} as Config) } }); + }, + + query(parameter: Key, schema: Value) { + return pipe( + this._effects.query, + option.fromNullable, + option.getOrElse(constant(z.object({}))), + (effect) => + ({ query: effect.extend({ [parameter]: schema }) }) as ExtendEffect< + Effect, + 'query', + { [P in Key]: Value } + >, + (effect) => enhanceRequirement(this, effect) + ); + }, + + path(key: Key, schema: Value) { + return pipe( + this._effects.path, + option.fromNullable, + option.getOrElse(constant(z.object({}))), + (effect) => + ({ path: effect.extend({ [key]: schema }) }) as ExtendEffect< + Effect, + 'path', + { [P in Key]: Value } + >, + (effect) => enhanceRequirement(this, effect) + ); + }, + header(key: Key, schema: Value) { + return pipe( + this._effects.header, + option.fromNullable, + option.getOrElse(constant(z.object({}))), + (effect) => + ({ header: effect.extend({ [key]: schema }) }) as ExtendEffect< + Effect, + 'header', + { [P in Key]: Value } + >, + (effect) => enhanceRequirement(this, effect) + ); + }, + cookie(key: Key, schema: Value) { + return pipe( + this._effects.cookie, + option.fromNullable, + option.getOrElse(constant(z.object({}))), + (effect) => + ({ cookie: effect.extend({ [key]: schema }) }) as ExtendEffect< + Effect, + 'cookie', + { [P in Key]: Value } + >, + (effect) => enhanceRequirement(this, effect) + ); + }, + + response( + status: Status, + schema: Response, + config?: RConfig + ) { + return pipe( + this._effects.response || [], + (oldResponse) => [...oldResponse, { status, schema, config: config || ({} as RConfig) }], + (newResponse) => ({ + response: newResponse, + }), + (newResponse) => + enhanceRequirement(this, newResponse) as Requires< + Omit & MergeTwoResponses + > + ); + }, + _handle(req: express.Request, res: express.Response, next: NextFunction) { + return pipe( + req, + (req) => this._validator(req), + fold( + (e) => + pipe( + config?.errorHandler, + option.fromNullable, + option.fold( + () => defaultErrorHandler(e, res), + (handler) => handler(e, res, next) + ) + ), + () => { + next(); + } + ) + ); + }, + // @ts-expect-error We need this to duct tape our typing system together. + '()': () => {}, + } satisfies Requires; + + return Object.assign(requires._handle.bind(requires), requires); +} + +const defaultErrorHandler = (e: ZodError, res: express.Response) => { + res.status(400).contentType('application/problem+validation+json').json(zodToRfcError(e)).send(); +}; + +const enhanceRequirement = , E extends HasteEffect>( + requires: R, + newEffect: E +) => { + Object.assign(requires._effects, { + ...requires._effects, + ...newEffect, + }); + // Because of a quirk in how this() is handled we cannot return a new/rebound object here. + return requires as unknown as Requires & E>; +}; diff --git a/src/requires/requiresBody.ts b/src/requires/requiresBody.ts deleted file mode 100644 index 1760520..0000000 --- a/src/requires/requiresBody.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { z, ZodSchema } from 'zod'; -import express from 'express'; -import { pipe } from 'fp-ts/function'; -import { parseSafe } from '../utils'; -import { createHasteOperation } from './operation'; - -type BodyOptions = { - contentType: string; -}; -export const requiresBody = (schema: S, options?: BodyOptions) => - createHasteOperation( - { - body: schema, - }, - bodyValidator(schema), - bodyEnhancer(schema, options) - ); -const bodyEnhancer = (schema: ZodSchema, options?: BodyOptions) => () => ({ - requestBody: { - content: { - [options?.contentType || 'application/json']: { - schema, - }, - }, - }, -}); -const bodyValidator = (schema: ZodSchema) => (req: express.Request) => - pipe({ body: req.body }, parseSafe(z.object({ body: schema }))); diff --git a/src/requires/requiresMany.ts b/src/requires/requiresMany.ts deleted file mode 100644 index e50b802..0000000 --- a/src/requires/requiresMany.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { concatAll, Monoid } from 'fp-ts/Monoid'; -import { HasteEffect, HasteOperation, MergeEvery } from '../types'; -import { createHasteOperation } from './operation'; -import { constant, flow, pipe } from 'fp-ts/function'; -import { Applicative, map, separate } from 'fp-ts/Array'; -import express from 'express'; -import { array, either } from 'fp-ts'; -import { ZodError } from 'zod'; -import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; -import { mergeDeep } from '../utils'; - -export function requiresMany, ...HasteOperation[]]>( - ...operations: H -): HasteOperation> { - return createHasteOperation( - pipe( - operations, - map(({ _effects }) => _effects as HasteEffect), - concatAll(HasteMergeMonoid), - (v) => v as MergeEvery - ), - validateMany(operations), - enhanceMany(operations) - ); -} - -const validateMany = (operations: HasteOperation[]) => (req: express.Request) => - pipe( - operations, - map((op) => op._validator(req)), - separate, - ({ left }) => - pipe( - left, - either.fromPredicate(array.isEmpty, constant(left)), - either.map(constant(true)), - either.mapLeft( - flow( - array.map(({ issues }) => issues), - array.sequence(Applicative), - array.flatten, - (issues) => ({ issues }) as ZodError - ) - ) - ) - ); -const enhanceMany = (operations: HasteOperation[]) => (schema: ZodOpenApiOperationObject) => - operations.reduce( - (result, operation) => mergeDeep({}, operation._enhancer(result), result), - schema - ); - -const HasteMergeMonoid: Monoid = { - concat: (a, b) => ({ ...a, ...b }), - empty: {}, -}; diff --git a/src/requires/requiresParameter.ts b/src/requires/requiresParameter.ts deleted file mode 100644 index dc19f12..0000000 --- a/src/requires/requiresParameter.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { AnyZodObject, z, ZodSchema, ZodType } from 'zod'; -import express, { Request } from 'express'; -import { identity, pipe } from 'fp-ts/function'; -import { parseSafe } from '../utils'; -import { flatten, fromNullable, getOrElseW, map } from 'fp-ts/Either'; -import { ParameterLocation } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; -import { createHasteOperation } from './operation'; -import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; -import { either, option, record } from 'fp-ts'; -import { HasteOperation } from '../types'; - -export const requiresParameter = - (where: L) => - (key: K, schema: S) => - createHasteOperation( - { - [where]: { - [key]: schema, - }, - } as { - [l in L]: { - [k in K]: S; - }; - }, - (req) => - pipe( - { [where]: { [key]: paramGetters[where](req, key) } }, - parseSafe(z.object({ [where]: z.object({ [key]: schema }) })), - either.map((s) => Object.assign(req[locationRequestMapping[where]] || {}, s[where])) - ), - parameterEnhancer - ); - -type ParamGetter = (req: Request, name: string) => unknown; - -const getHeaderParam: ParamGetter = (req, name) => - pipe(req.headers, (v) => v[name], fromNullable(undefined), getOrElseW(identity)); - -const getQueryParam: ParamGetter = (req, name) => - pipe(req.query, (v) => v[name], fromNullable(undefined), getOrElseW(identity)); -const getCookieParam: ParamGetter = (req, name) => - pipe( - req.cookies, - fromNullable(undefined), - map((v) => v[name]), - fromNullable(undefined), - flatten, - getOrElseW(identity) - ); - -const getPathParam: ParamGetter = (req, name) => - pipe(req.params, (v) => v[name], fromNullable(undefined), getOrElseW(identity)); - -const paramGetters: Record = { - path: getPathParam, - cookie: getCookieParam, - header: getHeaderParam, - query: getQueryParam, -}; - -const locationRequestMapping: Record = { - path: 'params', - cookie: 'cookies', - header: 'headers', - query: 'query', -}; - -function parameterEnhancer( - this: HasteOperation, - operation: ZodOpenApiOperationObject -): Partial { - return { - requestParams: pipe( - { - path: mergeAndMap(this._effects.path, operation.requestParams?.path), - cookie: mergeAndMap(this._effects.cookie, operation.requestParams?.cookie), - header: mergeAndMap(this._effects.header, operation.requestParams?.header), - query: mergeAndMap(this._effects.query, operation.requestParams?.query), - }, - record.filterMap(option.fromNullable) - ), - }; -} - -const mergeAndMap = ( - newValue: Record | undefined, - oldValue: AnyZodObject | undefined -) => - pipe( - newValue, - option.fromNullable, - option.map((newSchema) => - pipe( - oldValue, - option.fromNullable, - option.getOrElse(() => z.object({})), - (oldSchema) => oldSchema.extend(newSchema) - ) - ), - option.getOrElseW(() => undefined) - ); diff --git a/src/requires/requiresResponse.ts b/src/requires/requiresResponse.ts deleted file mode 100644 index 1046346..0000000 --- a/src/requires/requiresResponse.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ZodSchema, ZodType } from 'zod'; -import { constant, pipe } from 'fp-ts/function'; -import { createHasteOperation } from './operation'; -import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; -import { either, option } from 'fp-ts'; -import { ZodOpenApiResponseObject } from 'zod-openapi'; -import { HasteOperation, StatusCode } from '../types'; - -type ResponseOptions = { - description?: string; - contentType: string; -}; -export const requiresResponse = ( - status: Status, - schema: Schema, - options?: ResponseOptions -): HasteOperation<{ response: [{ status: Status; schema: Schema }] }> => - createHasteOperation( - { - response: [ - { - status, - schema, - }, - ] as [{ status: Status; schema: Schema }], - }, - constant(either.right(true)), - responseEnhancer(status, schema, options) - ); - -const responseEnhancer = ( - status: S, - schema: ZodSchema, - options?: ResponseOptions -) => - function responseEnhancer( - operation: ZodOpenApiOperationObject - ): Partial { - return pipe( - operation.responses, - option.fromNullable, - option.map((response) => - option.fromNullable( - (response[status] as ZodOpenApiResponseObject)?.content?.[ - options?.contentType || 'application/json' - ]?.schema as ZodType | undefined - ) - ), - option.flatten, - option.getOrElseW(() => undefined), - (oldSchema): Partial => ({ - responses: { - [status]: { - description: options?.description, - content: { - [options?.contentType || 'application/json']: { - schema: oldSchema ? oldSchema.or(schema) : schema, - }, - }, - }, - } as Record, - }) - ); - }; diff --git a/src/requires/validators.spec.ts b/src/requires/validators.spec.ts new file mode 100644 index 0000000..06bd422 --- /dev/null +++ b/src/requires/validators.spec.ts @@ -0,0 +1,278 @@ +import { requires } from './requires'; +import { z } from 'zod'; +import { validateAll } from './validators'; +import express from 'express'; +import { either } from 'fp-ts'; +import { identity } from 'fp-ts/function'; +import { ParameterLocation } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; +import { ParsedQs } from 'qs'; + +describe('validators', () => { + const exampleLiteral = z.literal('test'); + const exampleBody = z.object({ + mode: exampleLiteral, + }); + + it('should return right when there are no requirements', () => { + const result = validateAll(requires()._effects, {} as express.Request); + expect(either.isRight(result)).toBeTruthy(); + }); + + it('should always return right for response requirements', () => { + const validator = requires().response('200', z.literal('example')); + const result = validateAll(validator._effects, {} as express.Request); + expect(either.isRight(result)).toBeTruthy(); + }); + + describe('body', () => { + it('should return Right when body matches requirements', () => { + const validator = requires().body(exampleBody); + + const result = validateAll(validator._effects, { body: { mode: 'test' } } as express.Request); + + expect(either.isRight(result)).toBeTruthy(); + }); + it('should update body with any transforms', () => { + const validator = requires().body( + z.object({ + changesType: z.string().transform((v) => v === 'true'), + }) + ); + + const reqTrue = { body: { changesType: 'true' } } as express.Request; + const reqFalse = { body: { changesType: 'false' } } as express.Request; + + const resultTrue = validateAll(validator._effects, reqTrue); + + expect(either.isRight(resultTrue)).toBeTruthy(); + expect(reqTrue.body).toEqual({ changesType: true }); + + const resultFalse = validateAll(validator._effects, reqFalse); + + expect(either.isRight(resultFalse)).toBeTruthy(); + expect(reqFalse.body).toEqual({ changesType: false }); + }); + it('should Left with zodError when does not match requirements', () => { + const validator = requires().body(exampleBody); + + const result = validateAll(validator._effects, { + body: { mode: 'hello' }, + } as express.Request); + + expect(either.isLeft(result)).toBeTruthy(); + expect(either.getOrElseW(identity)(result)).toEqual({ + issues: [ + { + code: 'invalid_literal', + expected: 'test', + message: 'Invalid literal value, expected "test"', + path: ['body', 'mode'], + received: 'hello', + }, + ], + }); + }); + it('should Left with all zodError when does not match multiple requirements', () => { + const validator = requires().body( + z.object({ + test: z.boolean(), + mode: z.number(), + }) + ); + + const result = validateAll(validator._effects, { + body: { mode: 'hello' }, + } as express.Request); + + expect(either.isLeft(result)).toBeTruthy(); + expect(either.getOrElseW(identity)(result)).toEqual({ + issues: [ + { + code: 'invalid_type', + expected: 'boolean', + message: 'Required', + path: ['body', 'test'], + received: 'undefined', + }, + { + code: 'invalid_type', + expected: 'number', + message: 'Expected number, received string', + path: ['body', 'mode'], + received: 'string', + }, + ], + }); + }); + }); + + describe.each(['query', 'header', 'cookie', 'path'] as const)('%s', (elem) => { + const locationRequestMapping: Record = { + path: 'params', + cookie: 'cookies', + header: 'headers', + query: 'query', + }; + + const exampleSchema = z.literal('example'); + const validator = requires()[elem]('test', exampleSchema); + it(`should return Right when ${elem} matches requirements`, () => { + const result = validateAll(validator._effects, { + [locationRequestMapping[elem]]: { test: 'example' }, + } as unknown as express.Request); + + expect(either.isRight(result)).toBeTruthy(); + }); + it(`should return Left when ${elem} misses requirements`, () => { + const result = validateAll(validator._effects, { + [locationRequestMapping[elem]]: { test: 'not example' }, + } as unknown as express.Request); + + expect(either.isLeft(result)).toBeTruthy(); + expect(either.getOrElseW(identity)(result)).toEqual({ + issues: [ + { + code: 'invalid_literal', + expected: 'example', + message: 'Invalid literal value, expected "example"', + path: [locationRequestMapping[elem], 'test'], + received: 'not example', + }, + ], + }); + }); + it(`should return Right when multiple ${elem}'s are required and all are provided`, () => { + const validator = requires()[elem]('test1', exampleSchema)[elem]('test2', exampleSchema); + const result = validateAll(validator._effects, { + [locationRequestMapping[elem]]: { + test1: 'example', + test2: 'example', + }, + } as unknown as express.Request); + + expect(either.isRight(result)).toBeTruthy(); + }); + it(`should return Left with all errors when ${elem} misses more than one requirements`, () => { + const validator = requires()[elem]('test1', exampleSchema)[elem]('test2', exampleSchema); + + const result = validateAll(validator._effects, { + [locationRequestMapping[elem]]: { test: 'not example' }, + } as unknown as express.Request); + + expect(either.isLeft(result)).toBeTruthy(); + expect(either.getOrElseW(identity)(result)).toEqual({ + issues: [ + { + code: 'invalid_literal', + expected: 'example', + message: 'Invalid literal value, expected "example"', + path: [locationRequestMapping[elem], 'test1'], + }, + { + code: 'invalid_literal', + expected: 'example', + message: 'Invalid literal value, expected "example"', + path: [locationRequestMapping[elem], 'test2'], + }, + ], + }); + }); + if (['cookie', 'query'].includes(elem)) { + it(`should update req.${elem} with the validation result`, () => { + const validator = requires()[elem]( + 'test', + z.string().transform((v) => (v === 'example' ? 4 : 2)) + ); + + const req = { + [locationRequestMapping[elem]]: { test: 'example' }, + } as unknown as express.Request; + const result = validateAll(validator._effects, req); + + expect(either.isRight(result)).toBeTruthy(); + expect(req[locationRequestMapping[elem]]).toEqual({ + test: 4, + }); + }); + } else { + it(`should not update req.${elem} with the validation result`, () => { + const validator = requires()[elem]( + 'test', + z.string().transform((v) => (v === 'example' ? 4 : 2)) + ); + + const req = { + [locationRequestMapping[elem]]: { test: 'example' }, + } as unknown as express.Request; + const result = validateAll(validator._effects, req); + + expect(either.isRight(result)).toBeTruthy(); + expect(req[locationRequestMapping[elem]]).toEqual({ + test: 'example', + }); + }); + } + }); + + const validateMany = requires() + .body(z.object({ test: z.literal(true) })) + .query('example', z.string().array()) + .header('x-example', z.string()) + .cookie('cookie_example', z.string()) + .path('id', z.literal('v1')); + + it('should be able to validate all requirements and return right if matching', () => { + const result = validateAll(validateMany._effects, { + body: { test: true }, + query: { example: ['1', '2'] } as ParsedQs, + params: { id: 'v1' } as unknown, + cookies: { cookie_example: 'test' }, + headers: { 'x-example': 'some header value' } as unknown, + } as express.Request); + + expect(either.isRight(result)).toBe(true); + }); + it('should be able to validate all requirements and return left for all not matching', () => { + const result = validateAll(validateMany._effects, { + body: { test: 'true' }, + query: { example: '1' } as ParsedQs, + params: { id: true } as unknown, + cookies: { cookie_example: 1 }, + headers: { 'x-example': 'some header value' } as unknown, + } as express.Request); + + expect(either.isLeft(result)).toBe(true); + expect(either.getOrElseW(identity)(result)).toEqual({ + issues: [ + { + code: 'invalid_literal', + expected: true, + message: 'Invalid literal value, expected true', + path: ['body', 'test'], + received: 'true', + }, + { + code: 'invalid_type', + expected: 'array', + message: 'Expected array, received string', + path: ['query', 'example'], + received: 'string', + }, + { + code: 'invalid_type', + expected: 'string', + message: 'Expected string, received number', + path: ['cookies', 'cookie_example'], + received: 'number', + }, + { + code: 'invalid_literal', + expected: 'v1', + message: 'Invalid literal value, expected "v1"', + path: ['params', 'id'], + received: true, + }, + ], + }); + }); +}); diff --git a/src/requires/validators.ts b/src/requires/validators.ts new file mode 100644 index 0000000..0738d6b --- /dev/null +++ b/src/requires/validators.ts @@ -0,0 +1,88 @@ +import { HasteEffect } from '../types'; +import { z, ZodError } from 'zod'; +import { constant, flow, pipe } from 'fp-ts/function'; +import { array, either, option } from 'fp-ts'; +import { ParameterLocation } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; +import { parseSafe } from '../utils'; +import express from 'express'; +import { Either } from 'fp-ts/Either'; +import { Applicative, separate } from 'fp-ts/Array'; + +type Validator = (effects: HasteEffect, req: express.Request) => Either; + +const bodyValidator: Validator = (effects, req) => + pipe( + effects.body, + option.fromNullable, + option.map(({ schema }) => + pipe( + { body: req.body }, + parseSafe(z.object({ body: schema })), + either.map((result) => (req.body = result.body)) + ) + ), + option.getOrElseW(constant(either.right(undefined))) + ); +const parameterValidator = + (location: ParameterLocation): Validator => + (effects, req) => + pipe( + effects[location], + option.fromNullable, + option.map((schema) => + pipe( + { [locationRequestMapping[location]]: req[locationRequestMapping[location]] }, + parseSafe(z.object({ [locationRequestMapping[location]]: schema })), + either.tap((result) => + pipe( + location, + option.fromPredicate((l): l is 'query' | 'cookie' => ['query', 'cookie'].includes(l)), + option.map((key) => + Object.assign( + req[locationRequestMapping[key]], + result[locationRequestMapping[key] as keyof typeof result] + ) + ), + constant(either.right(undefined)) + ) + ) + ) + ), + option.getOrElseW(constant(either.right(undefined))) + ); + +const locationRequestMapping: Record = { + path: 'params', + cookie: 'cookies', + header: 'headers', + query: 'query', +}; +export const enhancerMapping: Record = { + body: bodyValidator, + response: constant(either.right({})), + query: parameterValidator('query'), + path: parameterValidator('path'), + header: parameterValidator('header'), + cookie: parameterValidator('cookie'), +}; + +export const validateAll = (effect: HasteEffect, req: express.Request) => + pipe( + Object.keys(effect) as (keyof HasteEffect)[], + array.map((key) => enhancerMapping[key](effect, req)), + separate, + ({ left }) => + pipe( + left, + either.fromPredicate(array.isEmpty, constant(left)), + either.map(constant(true)), + either.mapLeft( + flow( + array.map(({ issues }) => issues), + array.sequence(Applicative), + array.flatten, + (issues) => ({ issues }) as ZodError + ) + ) + ) + ); diff --git a/src/types/haste.ts b/src/types/haste.ts index 4312462..3dcdc92 100644 --- a/src/types/haste.ts +++ b/src/types/haste.ts @@ -1,7 +1,11 @@ import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; -import express, { Handler } from 'express'; +import express, { Handler, NextFunction } from 'express'; import { Either } from 'fp-ts/Either'; -import { ZodError } from 'zod'; +import { util, ZodError, ZodObject, ZodRawShape, ZodSchema, ZodType } from 'zod'; +import { BodyConfig, HasteEffect, ResponseConfig, SchemaWithConfig, StatusCode } from './index'; +import { ParameterLocation } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; +import { RequestHandler } from 'express-serve-static-core'; +import Omit = util.Omit; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface HasteOperation extends Handler { @@ -18,3 +22,72 @@ export type HasteValidator = ( this: H, req: express.Request ) => Either; + +export type MergeTwoResponses< + Effect extends HasteEffect, + Status extends StatusCode, + Response extends ZodType, +> = { + response: Effect['response'] extends [...infer Existing extends NonNullable] + ? [...Existing, { status: Status; schema: Response }] + : [ + { + status: Status; + schema: Response; + }, + ]; +}; + +export type ExtendEffect< + Effect extends HasteEffect, + Where extends ParameterLocation, + Value extends { [k in string]: ZodType }, +> = { + [W in Where]: Effect[Where] extends ZodObject + ? ZodObject + : ZodObject; +}; + +export type RequirementConfig = { + errorHandler?: (errors: ZodError, res: express.Response, next: NextFunction) => void; +}; + +export interface Requires extends express.RequestHandler { + _effects: Effect; + _handle: RequestHandler; + _config: RequirementConfig; + _hastens: true; + _enhancer: HasteEnhancer; + _validator: HasteValidator; + + body( + schema: Body, + config?: Config + ): Requires & { body: SchemaWithConfig }>; + + response( + status: Status, + schema: Response, + config?: RConfig + ): Requires & MergeTwoResponses>; + + query( + parameter: Param, + schema: Query + ): Requires & ExtendEffect>; + + header( + parameter: Key, + schema: Value + ): Requires & ExtendEffect>; + + path( + parameter: Key, + schema: Value + ): Requires & ExtendEffect>; + + cookie( + parameter: Key, + schema: Value + ): Requires & ExtendEffect>; +} diff --git a/src/types/index.ts b/src/types/index.ts index 63fb811..40c3f52 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,56 +1,58 @@ import { AnyZodObject, z, ZodSchema, ZodType } from 'zod'; import { HasteBadRequestSchema, HasteOptionSchema } from '../schemas'; -import { ParamsDictionary, RequestHandler } from 'express-serve-static-core'; +import * as core from 'express-serve-static-core'; +import { ParamsDictionary } from 'express-serve-static-core'; import { ParsedQs } from 'qs'; -import { HasteOperation } from './haste'; +import { Requires } from './haste'; +import express, { NextFunction } from 'express'; +import { ParseInt } from './utilities'; export type StatusCode = `${1 | 2 | 3 | 4 | 5}${string}`; -export type HasteResponseEffect = { status: StatusCode; schema: ZodSchema }; + +export type BodyConfig = { contentType?: string }; +export type ResponseConfig = { contentType?: string; description?: string }; + +export type SchemaWithConfig = { + schema: Schema; + config?: Config; +}; + +export type HasteResponseEffect = SchemaWithConfig & { + status: StatusCode; +}; export type HasteEffect = { - body?: ZodSchema; response?: HasteResponseEffect[]; + body?: SchemaWithConfig; path?: AnyZodObject; query?: AnyZodObject; header?: AnyZodObject; cookie?: AnyZodObject; }; -type RecurseInfer = T extends [ - infer I1 extends HasteResponseEffect, - ...infer I2 extends HasteResponseEffect[], -] - ? z.infer | RecurseInfer - : T extends [infer I3 extends HasteResponseEffect] - ? z.infer - : never; - -export type HasteResponseFor = E extends HasteEffect - ? E extends { response: infer R } - ? RecurseInfer - : unknown - : unknown; - -export type HasteParamsFor = E extends { [k in string]: ZodSchema } - ? { - [Key in keyof E]: z.infer; - } - : ParamsDictionary; - -export type HasteQueryFor = E extends { [k in string]: ZodSchema } - ? { - [Key in keyof E]: z.infer; - } & ParsedQs - : ParsedQs; - -export type HasteRequestHandler = O extends HasteOperation - ? RequestHandler< - E extends { path: infer P } ? HasteParamsFor

: ParamsDictionary, +export type HasteRequest = O extends Requires + ? express.Request< + E extends { path: infer P extends ZodType } ? z.infer

: ParamsDictionary, E extends { response: Array<{ schema: infer S extends ZodType }> } ? z.infer : unknown, - E extends { body: infer B extends ZodType } ? z.infer : unknown, - E extends { query: infer Q } ? HasteQueryFor : ParsedQs + E extends { body: { schema: infer B extends ZodType } } ? z.infer : unknown, + E extends { query: infer P extends ZodType } ? z.infer

& Omit : ParsedQs > - : RequestHandler; + : express.Request; + +export type HasteResponse = O extends Requires + ? core.Response< + E extends { response: Array<{ schema: infer S extends ZodType }> } ? z.infer : unknown, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + E extends { response: Array<{ status: infer S }> } ? ParseInt : number + > + : express.Response; + +export type HasteRequestHandler = ( + request: HasteRequest, + response: HasteResponse, + next: NextFunction +) => void; export type HasteOptionType = (typeof HasteOptionSchema)['_input']; diff --git a/src/types/utilities.ts b/src/types/utilities.ts index 7304c01..6103fb7 100644 --- a/src/types/utilities.ts +++ b/src/types/utilities.ts @@ -67,3 +67,5 @@ export type UnionToIntersection = (U extends any ? (x: U) => void : never) ex ) => void ? I : never; + +export type ParseInt = T extends `${infer N extends number}` ? N : never; From 5d312803a2a0f3891ecbcd268dfe5685d714c951 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Sun, 14 Jan 2024 21:25:10 +1100 Subject: [PATCH 02/13] Add jsdoc to validators. --- src/requires/requires.ts | 63 +++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/requires/requires.ts b/src/requires/requires.ts index 651e376..2db6bd7 100644 --- a/src/requires/requires.ts +++ b/src/requires/requires.ts @@ -22,20 +22,23 @@ export function requires< Config extends RequirementConfig = RequirementConfig, >(config?: Config, _effects?: Effect) { const requires = { - _effects: _effects || ({} as Effect), - _config: config || {}, - _hastens: true, - _enhancer(operation) { - return enhanceAll(this._effects, operation); - }, - _validator(req) { - return validateAll(this._effects, req); - }, - + /** + * Add a validation against the `request.body` field, this field must match the given schema or the errorHandler will + * be called. + * @param schema {ZodType} Any zod schema, json is the default assumed incoming value. + * @param config {BodyConfig} Customise the behaviour of the body validator including changing the contentType. + */ body(schema: Body, config?: Config) { return enhanceRequirement(this, { body: { schema, config: config || ({} as Config) } }); }, + /** + * Add a validation against the `request.query` field, this field must contain and match the expected value or the errorHandler will + * be called. + * @param parameter {string} The key of this query parameter. + * @param schema {ZodType} The schema this key should match, incoming query parameters should only ever be z.string() or z.string().array(), + * you can use transforms to extend these to another type ie transform a string to a number etc. + */ query(parameter: Key, schema: Value) { return pipe( this._effects.query, @@ -51,6 +54,13 @@ export function requires< ); }, + /** + * Add validation against the `request.params` field, this field must contain and match the expected value or the errorHandler will + * be called. + * @param key {string} The key of this path field as defined in the path given to express. + * @param schema {ZodType} The schema this key should match, incoming paths will only ever be z.string() although + * typing allows you to pass any schema. + */ path(key: Key, schema: Value) { return pipe( this._effects.path, @@ -65,6 +75,14 @@ export function requires< (effect) => enhanceRequirement(this, effect) ); }, + + /** + * Add validation against the `request.headers` `field, this field must contain and match the expected value or the errorHandler will + * be called. + * @param key {string} The header field name, non-standard header fields should be prefixed by x- as per convention. + * @param schema {ZodType} The schema the header value should match, incoming headers will only ever be z.string() although + * typing allows you to pass any schema. + */ header(key: Key, schema: Value) { return pipe( this._effects.header, @@ -79,6 +97,15 @@ export function requires< (effect) => enhanceRequirement(this, effect) ); }, + + /** + * Add validation against the `request.cookies` `field, this field must contain and match the expected value or the errorHandler will + * be called. + * @requires cookie-parser cookie-parser must set up to parse cookie fields. + * @param key {string} The cookie field name, non-standard header fields should be prefixed by x- as per convention. + * @param schema {ZodType} The schema the header value should match, incoming headers will only ever be z.string() although + * typing allows you to pass any schema. + */ cookie(key: Key, schema: Value) { return pipe( this._effects.cookie, @@ -94,6 +121,12 @@ export function requires< ); }, + /** + * This validator exists purely for request type enrichment and documentation purposes, no validation will occur for responses. + * @param status {StatusCode} The status code this response is for, used in documentation and type enhancement. + * @param schema {ZodType} The schema to validate, used for documentation and type enhancement. + * @param config { ResponseConfig } Specify an alternate contentType, used only for documentation. + */ response( status: Status, schema: Response, @@ -111,6 +144,16 @@ export function requires< > ); }, + + _effects: _effects || ({} as Effect), + _config: config || {}, + _hastens: true, + _enhancer(operation) { + return enhanceAll(this._effects, operation); + }, + _validator(req) { + return validateAll(this._effects, req); + }, _handle(req: express.Request, res: express.Response, next: NextFunction) { return pipe( req, From 0a1e68a3fa769105caff140138c6fc1742cf7f5f Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 18 Jan 2024 00:26:57 +1100 Subject: [PATCH 03/13] Add all the tests --- docs/examples/simple/index.ts | 22 +-- docs/examples/simple/package.json | 3 + docs/examples/simple/petHandler.ts | 3 +- package.json | 2 +- src/document.spec.ts | 1 - src/document.ts | 90 ------------ src/{ => document}/express-internal.d.ts | 0 src/document/extractors.spec.ts | 79 ++++++++++ src/document/extractors.ts | 74 ++++++++++ src/document/generate.spec.ts | 174 +++++++++++++++++++++++ src/document/generate.ts | 66 +++++++++ src/document/index.ts | 61 ++++++++ src/document/types.ts | 34 +++++ src/requires/enhancers.spec.ts | 106 +++++++++++++- src/requires/enhancers.ts | 37 ++++- src/requires/requires.spec.ts | 12 +- src/requires/requires.ts | 71 ++++----- src/requires/validators.spec.ts | 114 +++++++++++++++ src/requires/validators.ts | 60 ++++++++ src/types/haste.ts | 113 +++++++++++---- src/types/index.ts | 19 +++ src/types/utilities.ts | 1 + src/utils.ts | 2 +- tsconfig.json | 2 +- 24 files changed, 963 insertions(+), 183 deletions(-) delete mode 100644 src/document.spec.ts delete mode 100644 src/document.ts rename src/{ => document}/express-internal.d.ts (100%) create mode 100644 src/document/extractors.spec.ts create mode 100644 src/document/extractors.ts create mode 100644 src/document/generate.spec.ts create mode 100644 src/document/generate.ts create mode 100644 src/document/index.ts create mode 100644 src/document/types.ts diff --git a/docs/examples/simple/index.ts b/docs/examples/simple/index.ts index 05d4225..ae04194 100644 --- a/docs/examples/simple/index.ts +++ b/docs/examples/simple/index.ts @@ -9,7 +9,7 @@ import { import express, { Router, json } from 'express'; import { document } from 'express-haste'; import cookieParser from 'cookie-parser'; -import { getRedocHtml } from "./redoc"; +import { getRedocHtml } from './redoc'; import { UsernamePasswordAuth } from './schemas'; import { requires } from '../../../src'; @@ -19,24 +19,25 @@ app.use(json()); app.use(cookieParser()); // Define the document router before middleware asking for a username:password header. -const docRouter = Router() -app.use('/docs', docRouter) +const docRouter = Router(); +app.use('/docs', docRouter); // Get one pet is exempt from needing a header for demo reasons. app.get('/pet/:id', getOnePetRequirements, getOnePet); // Require an authorization header -app.use(requires().header('authorization', UsernamePasswordAuth)) +app.use(requires().header('authorization', UsernamePasswordAuth)); app.post('/pets/:id', createPetRequirements, createPetHandler); app.get('/pets', searchPetRequirements, searchPets); -const spec = document(app, { - info: { - title: 'MyPets', - version: '0.0.1', - }, -}); + +const spec = document(app).info({ + title: 'MyPets', + version: '0.0.1', +}).auth( + 'bearer', {type: 'apiKey', scheme: 'Bearer'} +).spec(); docRouter.get(`/openapi.json`, (req, res) => res.status(200).json(spec)); docRouter.get('/', (req, res) => { @@ -50,5 +51,4 @@ docRouter.get('/', (req, res) => { ); }); - export default app; diff --git a/docs/examples/simple/package.json b/docs/examples/simple/package.json index 8bb8017..9409062 100644 --- a/docs/examples/simple/package.json +++ b/docs/examples/simple/package.json @@ -17,5 +17,8 @@ "devDependencies": { "jest": "^29.7.0", "ts-node": "^10.9.2" + }, + "overrides": { + "@hookform/resolvers": "^3.3.1" } } diff --git a/docs/examples/simple/petHandler.ts b/docs/examples/simple/petHandler.ts index 34b5f55..6434112 100644 --- a/docs/examples/simple/petHandler.ts +++ b/docs/examples/simple/petHandler.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import { z, ZodType } from 'zod'; import { HasteRequestHandler, requires } from 'express-haste'; import { AsyncCreationRequest, @@ -14,7 +14,6 @@ export const searchPetRequirements = requires() .query('async', AsyncCreationRequest) .response('200', PetWithIdSchema) .response('202', JobAcceptedSchema); - export const searchPets: HasteRequestHandler = (req, res) => { if (req.query.async === true) { return res.status(200).json({ diff --git a/package.json b/package.json index 1da992e..b65b8d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-haste", - "version": "0.0.8", + "version": "1.0.0", "description": "", "homepage": "https://github.com/avra-m3/express-haste#readme", "bugs": { diff --git a/src/document.spec.ts b/src/document.spec.ts deleted file mode 100644 index 04c901a..0000000 --- a/src/document.spec.ts +++ /dev/null @@ -1 +0,0 @@ -describe('document', () => {}); diff --git a/src/document.ts b/src/document.ts deleted file mode 100644 index 5bca6c2..0000000 --- a/src/document.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Express } from 'express-serve-static-core'; -import { createDocument, ZodOpenApiResponseObject } from 'zod-openapi'; -import { constant, pipe } from 'fp-ts/function'; -import { - ZodOpenApiOperationObject, - ZodOpenApiPathsObject, -} from 'zod-openapi/lib-types/create/document'; -import express, { Layer, Router } from 'express'; -import { match } from 'fp-ts/boolean'; -import * as O from 'fp-ts/Option'; -import { HasteBadRequestSchema, HasteOptionSchema } from './schemas'; -import { HasteOperation, HasteOptionType } from './types'; -import { mergeDeep } from './utils'; -import { record } from 'fp-ts'; - -const AllPathsKey = '!all'; - -export const document = (app: Express, options: HasteOptionType) => { - const router: Router = app._router; - const { info, openApiVersion } = HasteOptionSchema.parse(options); - const specification = { - openapi: openApiVersion, - info, - paths: {} as ZodOpenApiPathsObject, - }; - specification.paths = router.stack.reduce( - (newPaths, layer) => addRouteToDocument(newPaths, layer), - specification.paths - ); - delete specification.paths[AllPathsKey]; - return createDocument(specification); -}; - -export const addRouteToDocument = (paths: ZodOpenApiPathsObject, layer: Layer) => { - const { path, methods } = layer.route || { - path: AllPathsKey, - methods: { use: true } as Record, - }; - - if (!path || !methods) { - return paths; - } - - return Object.assign({}, paths, { - [path]: pipe( - methods, - record.filterMap((value) => - pipe( - value, - match(constant(O.none), () => - O.some({ - responses: { - 400: BadRequest, - }, - }) - ), - O.map((op) => mergeDeep(op, paths?.[AllPathsKey]?.['use' as 'get'] || {})), - O.map((op) => mergeDeep(op, paths?.[path]?.['use' as 'get'] || {})), - O.map((op) => improveOperationFromLayer(layer, op)) - ) - ), - (result) => mergeDeep({}, paths[path] || {}, result) - ), - }); -}; - -const improveOperationFromLayer = (layer: Layer, operation: ZodOpenApiOperationObject) => { - if (layer.route) { - layer.route.stack.forEach( - (subOperation: Layer) => (operation = improveOperationFromLayer(subOperation, operation)) - ); - } - if (isHasteOperation(layer.handle)) { - return mergeDeep({}, operation, layer.handle._enhancer(operation)); - } - return Object.assign({}, operation); -}; - -export const isHasteOperation = (value: express.Handler): value is HasteOperation => - !!value && '_hastens' in value && value._hastens === true; - -export const BadRequest: ZodOpenApiResponseObject = Object.freeze({ - description: '400 BAD REQUEST', - content: { - 'application/problem+validation+json': { - schema: HasteBadRequestSchema, - }, - }, - ref: '400-bad-request', -}); diff --git a/src/express-internal.d.ts b/src/document/express-internal.d.ts similarity index 100% rename from src/express-internal.d.ts rename to src/document/express-internal.d.ts diff --git a/src/document/extractors.spec.ts b/src/document/extractors.spec.ts new file mode 100644 index 0000000..babc1e1 --- /dev/null +++ b/src/document/extractors.spec.ts @@ -0,0 +1,79 @@ +import { extractLayerPaths } from './extractors'; +import { Layer } from 'express'; + +describe('extractLayerPaths', () => { + const makeLayer = (handler: unknown, path: string, method: string) => + ({ + route: { path, methods: { [method]: true } }, + handle: handler, + }) as unknown as Layer; + + it('should return an empty object when no routes', () => { + expect(extractLayerPaths({} as Layer)).toEqual({}); + }); + it('should return an empty object when route is missing methods', () => { + expect( + extractLayerPaths({ route: { path: '/test', methods: undefined } } as unknown as Layer) + ).toEqual({}); + }); + it('should return an empty object when route is missing methods', () => { + expect( + extractLayerPaths({ route: { path: undefined, methods: { get: true } } } as unknown as Layer) + ).toEqual({}); + }); + + it('should return the operations attached to the route handler', () => { + const op = { _enhancer: jest.fn(), _hastens: true }; + op._enhancer.mockReturnValue({ test: 'value' }); + expect( + extractLayerPaths({ + route: { path: '/test', methods: { get: true } }, + handle: op, + } as unknown as Layer) + ).toEqual({ + '/test': { get: [op] }, + }); + }); + it('should return operations for all methods attached to the route handler', () => { + const op = { _enhancer: jest.fn(), _hastens: true }; + op._enhancer.mockReturnValue({ test: 'value' }); + expect( + extractLayerPaths({ + route: { path: '/test', methods: { get: true, post: true, put: false } }, + handle: op, + } as unknown as Layer) + ).toEqual({ + '/test': { get: [op], post: [op] }, + }); + }); + it('should return operations attached to the route.stack[]', () => { + const op1 = { _enhancer: jest.fn(), _hastens: true }; + const op2 = { _enhancer: jest.fn(), _hastens: true }; + const root = { + route: { + path: '/test', + methods: { get: true }, + stack: [makeLayer(op1, '/test1', 'get'), makeLayer(op2, '/test2', 'get')], + }, + }; + expect(extractLayerPaths(root as unknown as Layer)).toEqual({ + '/test': { get: [op1, op2] }, + }); + }); + it('should merge operations attached to the handler route.stack[]', () => { + const op1 = { _enhancer: jest.fn(), _hastens: true }; + const op2 = { _enhancer: jest.fn(), _hastens: true }; + const op3 = { _enhancer: jest.fn(), _hastens: true }; + const root = { + handle: op3, + route: { + path: '/test', + methods: { get: true }, + stack: [makeLayer(op1, '/test1', 'get'), makeLayer(op2, '/test2', 'get')], + }, + }; + expect(extractLayerPaths(root as unknown as Layer)).toEqual({ + '/test': { get: [op3, op1, op2] }, + }); + }); +}); diff --git a/src/document/extractors.ts b/src/document/extractors.ts new file mode 100644 index 0000000..e75f04b --- /dev/null +++ b/src/document/extractors.ts @@ -0,0 +1,74 @@ +import { constant, identity, pipe } from 'fp-ts/function'; +import { array, option, record } from 'fp-ts'; +import { HasteEffect, Requires, RequiresProp } from '../types'; +import { mergeDeep } from '../utils'; +import { AllPathsKey, HasteRequirementMap } from './types'; +import express, { Layer } from 'express'; +import { Option } from 'fp-ts/Option'; +import { not } from 'fp-ts/Predicate'; + +const isHasteOperation = (value: express.Handler): value is Requires => + !!value && '_hastens' in value && value._hastens === true; + +export const extractLayerPaths = (layer: Layer): HasteRequirementMap => + pipe( + layer.route, + getRouteOrNone, + option.map(({ path, methods }) => ({ + [path]: pipe(interrogateLayerMethods(layer, path, methods), (layerResult) => + pipe( + interrogateLayerChildren(layer, path, methods), + option.fromPredicate(not(record.isEmpty)), + option.map((v) => mergeDeep(option.getOrElse(constant({}))(layerResult), v)), + option.orElse(() => layerResult) + ) + ), + })), + option.map(record.filterMap(identity)), + option.getOrElse(constant({})) + ); + +const getRouteOrNone = (route: Layer['route']) => + pipe( + route, + option.fromPredicate((r): r is NonNullable => !!r && (!!r.methods || !!r.path)), + option.orElse( + constant( + option.some({ + path: AllPathsKey, + methods: { use: true } as Record, + }) + ) + ), + option.chain(option.fromPredicate(routeIsValid)) + ); + +const routeIsValid = ( + route: Pick, 'path' | 'methods'> +): route is RequiresProp => !!route.path && !!route.methods; + +const interrogateLayerMethods = ( + layer: Layer, + _path: string, + methods: Record +): Option[]>> => + pipe( + layer.handle, + option.fromPredicate(isHasteOperation), + option.map((operation) => + pipe(methods, record.filter(identity), record.map(constant([operation]))) + ) + ); +const interrogateLayerChildren = ( + layer: Layer, + path: string, + methods: Record +): HasteRequirementMap[string] => + pipe( + layer, + option.fromPredicate((layer): layer is RequiresProp => !!layer.route), + option.map((layer) => layer.route.stack), + option.chain(option.fromNullable), + option.map(array.filterMap((subLayer) => interrogateLayerMethods(subLayer, path, methods))), + option.fold(constant({}), array.reduce({}, mergeDeep)) + ); diff --git a/src/document/generate.spec.ts b/src/document/generate.spec.ts new file mode 100644 index 0000000..93ce60b --- /dev/null +++ b/src/document/generate.spec.ts @@ -0,0 +1,174 @@ +import { HasteEffect, Requires } from '../types'; +import { generateComponentsFromOperations, generatePathsFromOperations } from './generate'; +import { AllPathsKey } from './types'; + +describe('generate', () => { + const fakeRequirement = (r: unknown) => + ({ + _components: jest.fn().mockReturnValue(r), + _enhancer: jest.fn().mockReturnValue(r), + }) as unknown as Requires; + describe('generateComponentsFromOperations', () => { + it('should return the result of _components', () => { + const components = {}; + const map = { '/path': { post: fakeRequirement({ test: 'value' }) } }; + expect(generateComponentsFromOperations(components, map)).toEqual({ test: 'value' }); + expect(map['/path'].post._components).toBeCalledWith(components); + }); + it('should return a merged result for many effects', () => { + const components = {}; + const map = { + '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value2'] } }), + }, + }; + expect(generateComponentsFromOperations(components, map)).toEqual({ + test: { deep: ['value1', 'value2'] }, + }); + expect(map['/patha'].post._components).toBeCalledWith(components); + expect(map['/pathb'].get._components).toBeCalledWith(components); + }); + it('should treat !all same as any other path', () => { + const components = {}; + const map = { + [AllPathsKey]: { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value2'] } }), + }, + }; + expect(generateComponentsFromOperations(components, map)).toEqual({ + test: { deep: ['value1', 'value2'] }, + }); + expect(map[AllPathsKey].post._components).toBeCalledWith(components); + expect(map['/pathb'].get._components).toBeCalledWith(components); + }); + it('should treat use same as any other method', () => { + const components = {}; + const map = { + '/patha': { use: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value2'] } }), + }, + }; + expect(generateComponentsFromOperations(components, map)).toEqual({ + test: { deep: ['value1', 'value2'] }, + }); + expect(map['/patha'].use._components).toBeCalledWith(components); + expect(map['/pathb'].get._components).toBeCalledWith(components); + }); + it('should correctly handle partial paths', () => { + const components = {}; + const map = { + '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value2'] } }), + }, + pathc: {}, + }; + expect(generateComponentsFromOperations(components, map)).toEqual({ + test: { deep: ['value1', 'value2'] }, + }); + expect(map['/patha'].post._components).toBeCalledWith(components); + expect(map['/pathb'].get._components).toBeCalledWith(components); + }); + it('should preserve original compoennt', () => { + const components = { test: { deep: ['value0'] } }; + const map = { + '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value2'] } }), + }, + pathc: {}, + }; + expect(generateComponentsFromOperations(components, map)).toEqual({ + test: { deep: ['value0', 'value1', 'value2'] }, + }); + expect(map['/patha'].post._components).toBeCalledWith(components); + expect(map['/pathb'].get._components).toBeCalledWith(components); + }); + }); + describe('generatePathsFromOperations', () => { + it('should return the result of _enhancer', () => { + const paths = {}; + const map = { '/path': { post: fakeRequirement({ test: 'value' }) } }; + expect(generatePathsFromOperations(paths, map)).toEqual({ + '/path': { post: { test: 'value' } }, + }); + expect(map['/path'].post._enhancer).toBeCalledWith(paths); + }); + it('should return a merged result for many effects', () => { + const pathObject = {}; + const map = { + '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value2'] } }), + }, + }; + expect(generatePathsFromOperations(pathObject, map)).toEqual({ + '/patha': { post: { test: { deep: ['value1'] } } }, + '/pathb': { get: { test: { deep: ['value2'] } } }, + }); + expect(map['/patha'].post._enhancer).toBeCalledWith(pathObject); + expect(map['/pathb'].get._enhancer).toBeCalledWith(pathObject); + }); + it('should apply !all to all paths', () => { + const pathObject = {}; + const map = { + [AllPathsKey]: { get: fakeRequirement({ test: { deep: ['value2'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value1'] } }), + }, + }; + expect(generatePathsFromOperations(pathObject, map)).toEqual({ + '/pathb': { get: { test: { deep: ['value1', 'value2'] } } }, + }); + expect(map[AllPathsKey].get._enhancer).toBeCalledWith(pathObject); + expect(map['/pathb'].get._enhancer).toBeCalledWith(pathObject); + }); + it('should apply use to all methods on the same path', () => { + const pathConfig = {}; + const map = { + '/patha': { + use: fakeRequirement({ test: { deep: ['value1'] } }), + get: fakeRequirement({ test: { deep: ['value2'] } }), + post: fakeRequirement({ another: 'key' }), + }, + }; + expect(generatePathsFromOperations(pathConfig, map)).toEqual({ + '/patha': { + get: { + test: { + deep: ['value2', 'value1'], + }, + }, + post: { + another: 'key', + test: { + deep: ['value1'], + }, + }, + }, + }); + expect(map['/patha'].use._enhancer).toBeCalledWith(pathConfig); + expect(map['/patha'].get._enhancer).toBeCalledWith(pathConfig); + expect(map['/patha'].post._enhancer).toBeCalledWith(pathConfig); + }); + it('should correctly handle partial paths', () => { + const components = {}; + const map = { + '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/pathb': { + get: fakeRequirement({ test: { deep: ['value2'] } }), + }, + pathc: {}, + }; + expect(generatePathsFromOperations(components, map)).toEqual({ + '/patha': { post: { test: { deep: ['value1'] } } }, + '/pathb': { get: { test: { deep: ['value2'] } } }, + }); + expect(map['/patha'].post._enhancer).toBeCalledWith(components); + expect(map['/pathb'].get._enhancer).toBeCalledWith(components); + }); + }); +}); diff --git a/src/document/generate.ts b/src/document/generate.ts new file mode 100644 index 0000000..c57eedf --- /dev/null +++ b/src/document/generate.ts @@ -0,0 +1,66 @@ +import { + ZodOpenApiPathItemObject, + ZodOpenApiPathsObject, +} from 'zod-openapi/lib-types/create/document'; +import { constant, flow, pipe } from 'fp-ts/function'; +import { array, option, record, tuple } from 'fp-ts'; +import { mergeDeep } from '../utils'; +import { AllPathsKey, HasteRequirementMap } from './types'; +import { ZodOpenApiComponentsObject } from 'zod-openapi'; +import { not } from 'fp-ts/Predicate'; + +export const generatePathsFromOperations = ( + spec: ZodOpenApiPathsObject, + operations: HasteRequirementMap +) => + pipe( + operations, + record.filterMapWithIndex((path, v) => + pipe( + v, + record.mapWithIndex((method, requirements) => + pipe( + requirements._enhancer(spec?.[path]?.[method as keyof ZodOpenApiPathItemObject] || {}), + (enhancement) => mergeDeep({}, spec?.[path]?.[method as 'get'] || {}, enhancement) + ) + ), + option.fromPredicate(not(record.isEmpty)) + ) + ), + (paths) => + pipe( + paths, + (p) => popAndMergeOnOtherKeys(AllPathsKey)(p), + record.map((methods) => popAndMergeOnOtherKeys('use')(methods)) + ) + ); + +const popAndMergeOnOtherKeys = + (key: string) => + (value: Record): Record => + pipe( + value, + record.pop(key), + option.fold(constant(value), (result) => + pipe( + result, + tuple.snd, + record.map((v) => mergeDeep(v, tuple.fst(result)) as V) + ) + ) + ); + +export const generateComponentsFromOperations = ( + components: ZodOpenApiComponentsObject, + operations: HasteRequirementMap +) => + pipe( + operations, + getRecordValues, + array.map(getRecordValues), + array.flatten, + array.map((operation) => operation._components(components)), + array.reduce(components, mergeDeep) + ); + +const getRecordValues = flow(record.toArray, array.map(tuple.snd)); diff --git a/src/document/index.ts b/src/document/index.ts new file mode 100644 index 0000000..13cb602 --- /dev/null +++ b/src/document/index.ts @@ -0,0 +1,61 @@ +import { Express } from 'express-serve-static-core'; +import { mergeDeep } from '../utils'; +import { createDocument, ZodOpenApiComponentsObject } from 'zod-openapi'; +import { HasteDocument, HasteRequirementMap } from './types'; +import { pipe } from 'fp-ts/function'; +import express, { Router } from 'express'; +import { array } from 'fp-ts'; +import { ZodOpenApiPathsObject } from 'zod-openapi/lib-types/create/document'; +import { extractLayerPaths } from './extractors'; +import { generateComponentsFromOperations, generatePathsFromOperations } from './generate'; + +export { HasteDocument } from './types'; +export const document = ( + app: Express, + openApiVersion: '3.1.0' | '3.0.0' | '3.0.1' | '3.0.2' | '3.0.3' = '3.1.0' +) => { + const requirements = getStackRequirements(app); + console.log(requirements); + return { + info(info) { + Object.assign(this._spec.info, info); + return this; + }, + auth(name, scheme) { + this.component({ + securitySchemes: { + [name]: scheme, + }, + }); + if (this._spec.security) { + this._spec.security.push({ [name]: [] }); + } + return this; + }, + + component(component) { + mergeDeep(this._spec.components, component); + return this; + }, + spec() { + return createDocument(this._spec); + }, + _spec: { + openapi: openApiVersion, + info: { + title: 'Example Title', + version: '0.0.0', + }, + security: [], + paths: generatePathsFromOperations({} as ZodOpenApiPathsObject, requirements), + components: generateComponentsFromOperations({} as ZodOpenApiComponentsObject, requirements), + }, + } satisfies HasteDocument; +}; + +const getStackRequirements = (app: express.Application) => + pipe( + (app._router as Router).stack, + array.map(extractLayerPaths), + array.reduce({} as HasteRequirementMap, mergeDeep) + ); diff --git a/src/document/types.ts b/src/document/types.ts new file mode 100644 index 0000000..25220e8 --- /dev/null +++ b/src/document/types.ts @@ -0,0 +1,34 @@ +import { InfoObject } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; +import { + ComponentsObject, + SecuritySchemeObject, +} from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi30'; +import { createDocument, ZodOpenApiObject } from 'zod-openapi'; +import { HasteEffect, Requires } from '../types'; + +export interface HasteDocument { + /** + * Add information to the open api spec including title, version and more. + * @param info {InfoObject} The information to include. + */ + info: (info: InfoObject) => HasteDocument; + /** + * Apply a security scheme to all requests in this document, to apply to a single request, see requires().auth() + * @param scheme + */ + auth: (name: string, scheme: SecuritySchemeObject) => HasteDocument; + /** + * A catch-all allowing you to manually define components, this is an escape hatch and is not recommended. + * @param component {ComponentsObject} + */ + component: (component: ComponentsObject) => HasteDocument; + /** + * Create and return the final json OpenApi specification object. + */ + spec: () => ReturnType; + + _spec: ZodOpenApiObject; +} + +export const AllPathsKey = '!all'; +export type HasteRequirementMap = { [p: string]: Record> }; diff --git a/src/requires/enhancers.spec.ts b/src/requires/enhancers.spec.ts index 4274995..a1da57f 100644 --- a/src/requires/enhancers.spec.ts +++ b/src/requires/enhancers.spec.ts @@ -1,6 +1,6 @@ import { requires } from './requires'; import { z, ZodObject } from 'zod'; -import { enhanceAll } from './enhancers'; +import { enhanceAll, enhanceAllComponents } from './enhancers'; import { ZodOpenApiOperationObject, ZodOpenApiParameters, @@ -241,4 +241,108 @@ describe('enhancers', () => { }); }); }); + + describe('auth', () => { + it('should return no security scheme', () => { + const spec = requires().body(z.number()); + expect(enhanceAllComponents(spec._effects)).toEqual({}); + }); + it('should return the security scheme', () => { + const auth = requires().auth('ApiBearerToken', { + type: 'apiKey', + scheme: 'Bearer', + }); + expect(enhanceAll(auth._effects, {} as ZodOpenApiOperationObject)).toEqual({ + security: [ + { + ApiBearerToken: [], + }, + ], + }); + }); + it('should return many security scheme', () => { + const spec = requires() + .auth('ApiBearerToken', { + type: 'apiKey', + scheme: 'Bearer', + }) + .auth( + 'Oauth2', + { + type: 'oauth2', + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: ' https://example.com/oauth/token', + scopes: ['read', 'write'], + }, + }, + }, + { requireScopes: ['write'] } + ); + expect(enhanceAll(spec._effects, {} as ZodOpenApiOperationObject)).toEqual({ + security: [ + { + ApiBearerToken: [], + }, + { + Oauth2: ['write'], + }, + ], + }); + }); + it('should provide security components', () => { + const spec = requires().auth('ApiBearerToken', { + type: 'apiKey', + scheme: 'Bearer', + }); + expect(enhanceAllComponents(spec._effects)).toEqual({ + securitySchemes: { + ApiBearerToken: { + scheme: 'Bearer', + type: 'apiKey', + }, + }, + }); + }); + it('should provide security components when more than one auth option is provided', () => { + const spec = requires() + .auth('ApiBearerToken', { + type: 'apiKey', + scheme: 'Bearer', + }) + .auth( + 'Oauth2', + { + type: 'oauth2', + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: ' https://example.com/oauth/token', + scopes: ['read', 'write'], + }, + }, + }, + { requireScopes: ['write'] } + ); + expect(enhanceAllComponents(spec._effects)).toEqual({ + securitySchemes: { + ApiBearerToken: { + scheme: 'Bearer', + type: 'apiKey', + }, + Oauth2: { + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: ['read', 'write'], + tokenUrl: ' https://example.com/oauth/token', + }, + }, + type: 'oauth2', + }, + }, + }); + }); + }); }); diff --git a/src/requires/enhancers.ts b/src/requires/enhancers.ts index e751488..848d85d 100644 --- a/src/requires/enhancers.ts +++ b/src/requires/enhancers.ts @@ -1,10 +1,10 @@ import { HasteEffect, StatusCode } from '../types'; import { AnyZodObject, z, ZodType } from 'zod'; import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; -import { constant, pipe } from 'fp-ts/function'; -import { array, option } from 'fp-ts'; +import { constant, identity, pipe } from 'fp-ts/function'; +import { array, option, record } from 'fp-ts'; import { ParameterLocation } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; -import { ZodOpenApiResponseObject } from 'zod-openapi'; +import { ZodOpenApiComponentsObject, ZodOpenApiResponseObject } from 'zod-openapi'; import { DEFAULT_CONTENT_TYPE } from '../constants'; import { isZodType, mergeDeep } from '../utils'; @@ -60,6 +60,16 @@ const mergeAndMap = ( option.getOrElseW(() => undefined) ); +const authEnhancer: Enhancer = (effects, operation) => { + return pipe( + effects.auth, + option.fromNullable, + option.map((auth) => Object.entries(auth)), + option.map(array.map(([key, value]) => ({ [key]: value.config.requireScopes || [] }))), + option.map((newAuth) => ({ security: [...(operation.security || []), ...newAuth] })), + option.getOrElseW(constant({})) + ); +}; const responseEnhancer: Enhancer = (effects, operation) => pipe( effects.response, @@ -113,6 +123,7 @@ type Enhancer = ( ) => Partial; export const enhancerMapping: Record = { body: bodyEnhancer, + auth: authEnhancer, response: responseEnhancer, query: parameterEnhancer('query'), path: parameterEnhancer('path'), @@ -128,3 +139,23 @@ export const enhanceAll = ( Object.keys(effect) as (keyof HasteEffect)[], array.reduce({}, (result, key) => mergeDeep(result, enhancerMapping[key](effect, operation))) ); + +/** + * At the moment this only handles auth, but we can add more component-level manipulation in the future if need be. + * @param effects The haste effects + */ +export const enhanceAllComponents = (effects: HasteEffect): ZodOpenApiComponentsObject => { + return pipe( + { + securitySchemes: pipe( + effects.auth, + option.fromNullable, + option.map((auth) => Object.entries(auth)), + option.map( + array.reduce({}, (result, [key, value]) => mergeDeep(result, { [key]: value.scheme })) + ) + ), + }, + record.filterMap(identity) + ); +}; diff --git a/src/requires/requires.spec.ts b/src/requires/requires.spec.ts index 6ab1958..701b024 100644 --- a/src/requires/requires.spec.ts +++ b/src/requires/requires.spec.ts @@ -3,8 +3,9 @@ import { validateAll } from './validators'; import { either } from 'fp-ts'; import express from 'express'; import { z, ZodBoolean, ZodNumber, ZodObject, ZodString } from 'zod'; -import { enhanceAll } from './enhancers'; +import { enhanceAll, enhanceAllComponents } from './enhancers'; import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document'; +import { ZodOpenApiComponentsObject } from 'zod-openapi'; jest.mock('./validators'); jest.mock('./enhancers'); @@ -214,4 +215,13 @@ describe('requires', () => { expect(requirements._enhancer(operation)).toEqual(returnValue); expect(enhanceAll).toHaveBeenCalledWith(requirements._effects, operation); }); + + it('should call enhanceAllComponents when _components is called', () => { + const requirements = requires().body(z.string()); + const returnValue = { some: 'spec object' }; + const operation = {} as ZodOpenApiComponentsObject; + (enhanceAllComponents as jest.Mock).mockReturnValue(returnValue); + expect(requirements._components(operation)).toEqual(returnValue); + expect(enhanceAllComponents).toHaveBeenCalledWith(requirements._effects); + }); }); diff --git a/src/requires/requires.ts b/src/requires/requires.ts index 2db6bd7..59cfb41 100644 --- a/src/requires/requires.ts +++ b/src/requires/requires.ts @@ -1,4 +1,5 @@ import { + AuthConfig, BodyConfig, ExtendEffect, HasteEffect, @@ -11,34 +12,22 @@ import { import { constant, pipe } from 'fp-ts/function'; import { option } from 'fp-ts'; import { z, ZodError, ZodType } from 'zod'; -import { enhanceAll } from './enhancers'; +import { enhanceAll, enhanceAllComponents } from './enhancers'; import { validateAll } from './validators'; import { fold } from 'fp-ts/Either'; import { zodToRfcError } from '../utils'; import express, { NextFunction } from 'express'; +import { SecuritySchemeObject } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi30'; -export function requires< - Effect extends HasteEffect = Record, - Config extends RequirementConfig = RequirementConfig, ->(config?: Config, _effects?: Effect) { +export function requires( + config?: Config, + _effects?: Effect +) { const requires = { - /** - * Add a validation against the `request.body` field, this field must match the given schema or the errorHandler will - * be called. - * @param schema {ZodType} Any zod schema, json is the default assumed incoming value. - * @param config {BodyConfig} Customise the behaviour of the body validator including changing the contentType. - */ body(schema: Body, config?: Config) { return enhanceRequirement(this, { body: { schema, config: config || ({} as Config) } }); }, - /** - * Add a validation against the `request.query` field, this field must contain and match the expected value or the errorHandler will - * be called. - * @param parameter {string} The key of this query parameter. - * @param schema {ZodType} The schema this key should match, incoming query parameters should only ever be z.string() or z.string().array(), - * you can use transforms to extend these to another type ie transform a string to a number etc. - */ query(parameter: Key, schema: Value) { return pipe( this._effects.query, @@ -54,13 +43,6 @@ export function requires< ); }, - /** - * Add validation against the `request.params` field, this field must contain and match the expected value or the errorHandler will - * be called. - * @param key {string} The key of this path field as defined in the path given to express. - * @param schema {ZodType} The schema this key should match, incoming paths will only ever be z.string() although - * typing allows you to pass any schema. - */ path(key: Key, schema: Value) { return pipe( this._effects.path, @@ -76,13 +58,6 @@ export function requires< ); }, - /** - * Add validation against the `request.headers` `field, this field must contain and match the expected value or the errorHandler will - * be called. - * @param key {string} The header field name, non-standard header fields should be prefixed by x- as per convention. - * @param schema {ZodType} The schema the header value should match, incoming headers will only ever be z.string() although - * typing allows you to pass any schema. - */ header(key: Key, schema: Value) { return pipe( this._effects.header, @@ -98,14 +73,6 @@ export function requires< ); }, - /** - * Add validation against the `request.cookies` `field, this field must contain and match the expected value or the errorHandler will - * be called. - * @requires cookie-parser cookie-parser must set up to parse cookie fields. - * @param key {string} The cookie field name, non-standard header fields should be prefixed by x- as per convention. - * @param schema {ZodType} The schema the header value should match, incoming headers will only ever be z.string() although - * typing allows you to pass any schema. - */ cookie(key: Key, schema: Value) { return pipe( this._effects.cookie, @@ -121,12 +88,6 @@ export function requires< ); }, - /** - * This validator exists purely for request type enrichment and documentation purposes, no validation will occur for responses. - * @param status {StatusCode} The status code this response is for, used in documentation and type enhancement. - * @param schema {ZodType} The schema to validate, used for documentation and type enhancement. - * @param config { ResponseConfig } Specify an alternate contentType, used only for documentation. - */ response( status: Status, schema: Response, @@ -144,6 +105,21 @@ export function requires< > ); }, + auth( + name: Name, + scheme: Scheme, + config?: Config + ) { + return enhanceRequirement(this, { + auth: { + ...this._effects.auth, + [name]: { + scheme, + config: config || ({} as AuthConfig), + }, + } as { [N in Name]: { scheme: Scheme; config: Config } }, + }); + }, _effects: _effects || ({} as Effect), _config: config || {}, @@ -151,6 +127,9 @@ export function requires< _enhancer(operation) { return enhanceAll(this._effects, operation); }, + _components() { + return enhanceAllComponents(this._effects); + }, _validator(req) { return validateAll(this._effects, req); }, diff --git a/src/requires/validators.spec.ts b/src/requires/validators.spec.ts index 06bd422..729deae 100644 --- a/src/requires/validators.spec.ts +++ b/src/requires/validators.spec.ts @@ -275,4 +275,118 @@ describe('validators', () => { ], }); }); + + describe('auth', () => { + it('should return right by default', () => { + const req = requires().auth('oauth', { type: 'http' }); + const result = validateAll(req._effects, {} as express.Request); + expect(either.isRight(result)).toEqual(true); + }); + it('should return a right when validation function returns true', () => { + const validator = jest.fn().mockReturnValue(true); + const secScheme = { type: 'http' } as const; + const requirements = requires().auth('oauth', secScheme, { validator }); + const req = {} as express.Request; + const result = validateAll(requirements._effects, req); + expect(either.isRight(result)).toEqual(true); + expect(validator).toBeCalledWith(req, secScheme); + }); + it('should return a left when validation function returns false', () => { + const validator = jest.fn().mockReturnValue(false); + const secScheme = { type: 'http' } as const; + const requirements = requires().auth('oauth', secScheme, { validator }); + const req = {} as express.Request; + const result = validateAll(requirements._effects, req); + expect(either.isLeft(result)).toEqual(true); + expect(result).toEqual({ _tag: 'Left', left: { issues: [] } }); + expect(validator).toBeCalledWith(req, secScheme); + }); + it('should return a left when validation function returns a string error', () => { + const validator = jest.fn().mockReturnValue('this is what went wrong'); + const secScheme = { type: 'http' } as const; + const requirements = requires().auth('oauth', secScheme, { validator }); + const req = {} as express.Request; + const result = validateAll(requirements._effects, req); + expect(either.isLeft(result)).toEqual(true); + expect(result).toEqual({ + _tag: 'Left', + left: { + issues: [ + { + code: 'custom', + message: 'this is what went wrong', + path: ['authentication'], + }, + ], + }, + }); + expect(validator).toBeCalledWith(req, secScheme); + }); + it('should return a left when validation function throws an error', () => { + const validator = jest.fn().mockImplementation(() => { + throw new Error(); + }); + const secScheme = { type: 'http' } as const; + const requirements = requires().auth('oauth', secScheme, { validator }); + const req = {} as express.Request; + const result = validateAll(requirements._effects, req); + expect(either.isLeft(result)).toEqual(true); + expect(result).toEqual({ + _tag: 'Left', + left: { + issues: [], + }, + }); + expect(validator).toBeCalledWith(req, secScheme); + }); + it('should return a right when all validation functions return true', () => { + const validator1 = jest.fn().mockReturnValue(true); + const validator2 = jest.fn().mockReturnValue(true); + const secScheme = { type: 'http' } as const; + const requirements = requires() + .auth('oauth', secScheme, { validator: validator1 }) + .auth('another', secScheme, { validator: validator2 }); + const req = {} as express.Request; + const result = validateAll(requirements._effects, req); + expect(either.isRight(result)).toEqual(true); + expect(validator1).toBeCalledWith(req, secScheme); + expect(validator2).toBeCalledWith(req, secScheme); + }); + it('should return a left when any validation functions return false', () => { + const validator1 = jest.fn().mockReturnValue(true); + const validator2 = jest.fn().mockReturnValue(false); + const secScheme = { type: 'http' } as const; + const requirements = requires() + .auth('oauth', secScheme, { validator: validator1 }) + .auth('another', secScheme, { validator: validator2 }); + const req = {} as express.Request; + const result = validateAll(requirements._effects, req); + expect(either.isLeft(result)).toEqual(true); + expect(result).toEqual({ + _tag: 'Left', + left: { + issues: [], + }, + }); + expect(validator1).toBeCalledWith(req, secScheme); + expect(validator2).toBeCalledWith(req, secScheme); + }); + it('should return a left when any validation functions return false and other auth validator doesnt exist', () => { + const validator1 = jest.fn().mockReturnValue(false); + const secScheme = { type: 'http' } as const; + const requirements = requires() + .auth('oauth', secScheme, { validator: validator1 }) + .auth('another', secScheme); + const req = {} as express.Request; + const result = validateAll(requirements._effects, req); + expect(either.isLeft(result)).toEqual(true); + expect(result).toEqual({ + _tag: 'Left', + left: { + issues: [], + }, + }); + expect(validator1).toBeCalledWith(req, secScheme); + }); + }); }); diff --git a/src/requires/validators.ts b/src/requires/validators.ts index 0738d6b..ced808c 100644 --- a/src/requires/validators.ts +++ b/src/requires/validators.ts @@ -51,6 +51,65 @@ const parameterValidator = option.getOrElseW(constant(either.right(undefined))) ); +const authValidator: Validator = (effect, req) => + pipe( + effect.auth, + option.fromNullable, + option.map((auth) => Object.values(auth)), + option.map( + array.map((method) => + pipe( + method.config?.validator, + option.fromNullable, + option.fold(constant(either.right(true)), (fn) => + pipe( + either.tryCatch( + () => fn(req, method.scheme), + () => new ZodError([]) + ), + either.chain( + either.fromPredicate( + (v): v is boolean => v === true, + (v) => + new ZodError( + typeof v === 'string' + ? [ + { + code: 'custom', + message: v, + path: ['authentication'], + }, + ] + : [] + ) + ) + ) + ) + ) + ) + ) + ), + option.fold( + constant(either.right(true)), + flow( + array.separate, + either.fromPredicate( + ({ left }) => !left.length, + ({ left }) => left + ), + either.mapLeft( + flow( + array.map(({ issues }) => issues), + array.sequence(Applicative), + array.flatten, + (issues) => new ZodError(issues) + ) + ), + either.map(constant(true)) + ) + ) + ); + const locationRequestMapping: Record = { path: 'params', cookie: 'cookies', @@ -59,6 +118,7 @@ const locationRequestMapping: Record = }; export const enhancerMapping: Record = { body: bodyValidator, + auth: authValidator, response: constant(either.right({})), query: parameterValidator('query'), path: parameterValidator('path'), diff --git a/src/types/haste.ts b/src/types/haste.ts index 3dcdc92..4e4f80b 100644 --- a/src/types/haste.ts +++ b/src/types/haste.ts @@ -2,22 +2,31 @@ import { ZodOpenApiOperationObject } from 'zod-openapi/lib-types/create/document import express, { Handler, NextFunction } from 'express'; import { Either } from 'fp-ts/Either'; import { util, ZodError, ZodObject, ZodRawShape, ZodSchema, ZodType } from 'zod'; -import { BodyConfig, HasteEffect, ResponseConfig, SchemaWithConfig, StatusCode } from './index'; +import { + AuthConfig, + BodyConfig, + HasteEffect, + ResponseConfig, + SchemaWithConfig, + StatusCode, +} from './index'; import { ParameterLocation } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi31'; import { RequestHandler } from 'express-serve-static-core'; +import { SecuritySchemeObject } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi30'; +import { ZodOpenApiComponentsObject } from 'zod-openapi'; import Omit = util.Omit; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface HasteOperation extends Handler { _hastens: boolean; - _enhancer: HasteEnhancer; + _enhancer: HastePathEnhancer; _effects: Effects; _validator: HasteValidator; } -export type HasteEnhancer = ( - operation: ZodOpenApiOperationObject -) => Partial; +export type Enhancer = (operation: T) => Partial; +export type HastePathEnhancer = Enhancer; +export type HasteComponentEnhancer = Enhancer; export type HasteValidator = ( this: H, req: express.Request @@ -53,41 +62,95 @@ export type RequirementConfig = { }; export interface Requires extends express.RequestHandler { - _effects: Effect; - _handle: RequestHandler; - _config: RequirementConfig; - _hastens: true; - _enhancer: HasteEnhancer; - _validator: HasteValidator; - + /** + * Add a validation against the `request.body` field, this field must match the given schema or the errorHandler will + * be called. + * @param schema {ZodType} Any zod schema, json is the default assumed incoming value. + * @param config {BodyConfig} Customise the behaviour of the body validator including changing the contentType. + */ body( schema: Body, config?: Config ): Requires & { body: SchemaWithConfig }>; - response( - status: Status, - schema: Response, - config?: RConfig - ): Requires & MergeTwoResponses>; - + /** + * Add a validation against the `request.query` field, this field must contain and match the expected value or the errorHandler will + * be called. + * @param parameter {string} The key of this query parameter. + * @param schema {ZodType} The schema this key should match, incoming query parameters should only ever be z.string() or z.string().array(), + * you can use transforms to extend these to another type ie transform a string to a number etc. + */ query( parameter: Param, schema: Query ): Requires & ExtendEffect>; - header( - parameter: Key, - schema: Value - ): Requires & ExtendEffect>; - + /** + * Add validation against the `request.params` field, this field must contain and match the expected value or the errorHandler will + * be called. + * @param key {string} The key of this path field as defined in the path given to express. + * @param schema {ZodType} The schema this key should match, incoming paths will only ever be z.string() although + * typing allows you to pass any schema. + */ path( - parameter: Key, + key: Key, schema: Value ): Requires & ExtendEffect>; + /** + * Add validation against the `request.headers` `field, this field must contain and match the expected value or the errorHandler will + * be called. + * @param key {string} The header field name, non-standard header fields should be prefixed by x- as per convention. + * @param schema {ZodType} The schema the header value should match, incoming headers will only ever be z.string() although + * typing allows you to pass any schema. + */ + header( + key: Key, + schema: Value + ): Requires & ExtendEffect>; + + /** + * Add validation against the `request.cookies` `field, this field must contain and match the expected value or the errorHandler will + * be called. + * @requires cookie-parser cookie-parser must set up to parse cookie fields. + * @param key {string} The cookie field name, non-standard header fields should be prefixed by x- as per convention. + * @param schema {ZodType} The schema the header value should match, incoming headers will only ever be z.string() although + * typing allows you to pass any schema. + */ cookie( - parameter: Key, + key: Key, schema: Value ): Requires & ExtendEffect>; + + /** + * This validator exists purely for request type enrichment and documentation purposes, no validation will occur for responses. + * @param status {StatusCode} The status code this response is for, used in documentation and type enhancement. + * @param schema {ZodType} The schema to validate, used for documentation and type enhancement. + * @param config { ResponseConfig } Specify an alternate contentType, used only for documentation. + */ + response( + status: Status, + schema: Response, + config?: RConfig + ): Requires & MergeTwoResponses>; + + /** + * + * @param name {string} A name for the authentication scheme in the api spec. + * @param scheme {SecuritySchemeObject} The OAIS security scheme to apply to this request. + * @param config {AuthConfig} If no handler is defined, no validation will occur for this auth pattern. + */ + auth( + name: Name, + scheme: Scheme, + config?: Config + ): Requires & { auth: { [N in Name]: { scheme: Scheme; config: Config } } }>; + + readonly _hastens: true; + _effects: Effect; + _handle: RequestHandler; + _config: RequirementConfig; + _enhancer: HastePathEnhancer; + _components: HasteComponentEnhancer; + _validator: HasteValidator; } diff --git a/src/types/index.ts b/src/types/index.ts index 40c3f52..49bb00d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,11 +6,22 @@ import { ParsedQs } from 'qs'; import { Requires } from './haste'; import express, { NextFunction } from 'express'; import { ParseInt } from './utilities'; +import { SecuritySchemeObject } from 'zod-openapi/lib-types/openapi3-ts/dist/model/openapi30'; export type StatusCode = `${1 | 2 | 3 | 4 | 5}${string}`; export type BodyConfig = { contentType?: string }; export type ResponseConfig = { contentType?: string; description?: string }; +export type AuthConfig = { + /** + * A validator function that handles auth validation. + * @param req The request to validate against. + * @param method The security scheme to validate. + * @returns {boolean | () => string} A literal true value when validation is successful, false or a string error when not successful + */ + validator?: (req: express.Request, method: SecuritySchemeObject) => boolean | string; + requireScopes?: string[]; +}; export type SchemaWithConfig = { schema: Schema; @@ -21,8 +32,16 @@ export type HasteResponseEffect = SchemaWithConfig & status: StatusCode; }; +export type HasteAuthEffect = { + [name in string]: { + scheme: SecuritySchemeObject; + config: AuthConfig; + }; +}; + export type HasteEffect = { response?: HasteResponseEffect[]; + auth?: HasteAuthEffect; body?: SchemaWithConfig; path?: AnyZodObject; query?: AnyZodObject; diff --git a/src/types/utilities.ts b/src/types/utilities.ts index 6103fb7..0b8a04a 100644 --- a/src/types/utilities.ts +++ b/src/types/utilities.ts @@ -69,3 +69,4 @@ export type UnionToIntersection = (U extends any ? (x: U) => void : never) ex : never; export type ParseInt = T extends `${infer N extends number}` ? N : never; +export type RequiresProp = Omit & Required>; diff --git a/src/utils.ts b/src/utils.ts index 246e0e0..0166a84 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -70,7 +70,7 @@ export function isObject(item: unknown): item is Record { } /** - * Deep merge two objects. + * Deep merge two objects but very specific to haste. * @param target * @param sources */ diff --git a/tsconfig.json b/tsconfig.json index e604f2c..3099bd9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "esModuleInterop": true }, "include": [ - "./src/express-internal.d.ts" + "./src/document/express-internal.d.ts" ], "exclude": [ "node_modules" From f4e49f28f304df89908ef6c1974e2c637b0f8462 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 18 Jan 2024 00:42:45 +1100 Subject: [PATCH 04/13] Fix type being incorrect --- src/document/generate.spec.ts | 84 +++++++++++++++++------------------ src/document/generate.ts | 17 +++++-- src/document/index.ts | 1 - src/document/types.ts | 2 +- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/src/document/generate.spec.ts b/src/document/generate.spec.ts index 93ce60b..8c10f16 100644 --- a/src/document/generate.spec.ts +++ b/src/document/generate.spec.ts @@ -11,128 +11,128 @@ describe('generate', () => { describe('generateComponentsFromOperations', () => { it('should return the result of _components', () => { const components = {}; - const map = { '/path': { post: fakeRequirement({ test: 'value' }) } }; + const map = { '/path': { post: [fakeRequirement({ test: 'value' })] } }; expect(generateComponentsFromOperations(components, map)).toEqual({ test: 'value' }); - expect(map['/path'].post._components).toBeCalledWith(components); + expect(map['/path'].post[0]._components).toBeCalledWith(components); }); it('should return a merged result for many effects', () => { const components = {}; const map = { - '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/patha': { post: [fakeRequirement({ test: { deep: ['value1'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value2'] } }), + get: [fakeRequirement({ test: { deep: ['value2'] } })], }, }; expect(generateComponentsFromOperations(components, map)).toEqual({ test: { deep: ['value1', 'value2'] }, }); - expect(map['/patha'].post._components).toBeCalledWith(components); - expect(map['/pathb'].get._components).toBeCalledWith(components); + expect(map['/patha'].post[0]._components).toBeCalledWith(components); + expect(map['/pathb'].get[0]._components).toBeCalledWith(components); }); it('should treat !all same as any other path', () => { const components = {}; const map = { - [AllPathsKey]: { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + [AllPathsKey]: { post: [fakeRequirement({ test: { deep: ['value1'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value2'] } }), + get: [fakeRequirement({ test: { deep: ['value2'] } })], }, }; expect(generateComponentsFromOperations(components, map)).toEqual({ test: { deep: ['value1', 'value2'] }, }); - expect(map[AllPathsKey].post._components).toBeCalledWith(components); - expect(map['/pathb'].get._components).toBeCalledWith(components); + expect(map[AllPathsKey].post[0]._components).toBeCalledWith(components); + expect(map['/pathb'].get[0]._components).toBeCalledWith(components); }); it('should treat use same as any other method', () => { const components = {}; const map = { - '/patha': { use: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/patha': { use: [fakeRequirement({ test: { deep: ['value1'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value2'] } }), + get: [fakeRequirement({ test: { deep: ['value2'] } })], }, }; expect(generateComponentsFromOperations(components, map)).toEqual({ test: { deep: ['value1', 'value2'] }, }); - expect(map['/patha'].use._components).toBeCalledWith(components); - expect(map['/pathb'].get._components).toBeCalledWith(components); + expect(map['/patha'].use[0]._components).toBeCalledWith(components); + expect(map['/pathb'].get[0]._components).toBeCalledWith(components); }); it('should correctly handle partial paths', () => { const components = {}; const map = { - '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/patha': { post: [fakeRequirement({ test: { deep: ['value1'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value2'] } }), + get: [fakeRequirement({ test: { deep: ['value2'] } })], }, pathc: {}, }; expect(generateComponentsFromOperations(components, map)).toEqual({ test: { deep: ['value1', 'value2'] }, }); - expect(map['/patha'].post._components).toBeCalledWith(components); - expect(map['/pathb'].get._components).toBeCalledWith(components); + expect(map['/patha'].post[0]._components).toBeCalledWith(components); + expect(map['/pathb'].get[0]._components).toBeCalledWith(components); }); it('should preserve original compoennt', () => { const components = { test: { deep: ['value0'] } }; const map = { - '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/patha': { post: [fakeRequirement({ test: { deep: ['value1'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value2'] } }), + get: [fakeRequirement({ test: { deep: ['value2'] } })], }, pathc: {}, }; expect(generateComponentsFromOperations(components, map)).toEqual({ test: { deep: ['value0', 'value1', 'value2'] }, }); - expect(map['/patha'].post._components).toBeCalledWith(components); - expect(map['/pathb'].get._components).toBeCalledWith(components); + expect(map['/patha'].post[0]._components).toBeCalledWith(components); + expect(map['/pathb'].get[0]._components).toBeCalledWith(components); }); }); describe('generatePathsFromOperations', () => { it('should return the result of _enhancer', () => { const paths = {}; - const map = { '/path': { post: fakeRequirement({ test: 'value' }) } }; + const map = { '/path': { post: [fakeRequirement({ test: 'value' })] } }; expect(generatePathsFromOperations(paths, map)).toEqual({ '/path': { post: { test: 'value' } }, }); - expect(map['/path'].post._enhancer).toBeCalledWith(paths); + expect(map['/path'].post[0]._enhancer).toBeCalledWith(paths); }); it('should return a merged result for many effects', () => { const pathObject = {}; const map = { - '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/patha': { post: [fakeRequirement({ test: { deep: ['value1'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value2'] } }), + get: [fakeRequirement({ test: { deep: ['value2'] } })], }, }; expect(generatePathsFromOperations(pathObject, map)).toEqual({ '/patha': { post: { test: { deep: ['value1'] } } }, '/pathb': { get: { test: { deep: ['value2'] } } }, }); - expect(map['/patha'].post._enhancer).toBeCalledWith(pathObject); - expect(map['/pathb'].get._enhancer).toBeCalledWith(pathObject); + expect(map['/patha'].post[0]._enhancer).toBeCalledWith(pathObject); + expect(map['/pathb'].get[0]._enhancer).toBeCalledWith(pathObject); }); it('should apply !all to all paths', () => { const pathObject = {}; const map = { - [AllPathsKey]: { get: fakeRequirement({ test: { deep: ['value2'] } }) }, + [AllPathsKey]: { get: [fakeRequirement({ test: { deep: ['value2'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value1'] } }), + get: [fakeRequirement({ test: { deep: ['value1'] } })], }, }; expect(generatePathsFromOperations(pathObject, map)).toEqual({ '/pathb': { get: { test: { deep: ['value1', 'value2'] } } }, }); - expect(map[AllPathsKey].get._enhancer).toBeCalledWith(pathObject); - expect(map['/pathb'].get._enhancer).toBeCalledWith(pathObject); + expect(map[AllPathsKey].get[0]._enhancer).toBeCalledWith(pathObject); + expect(map['/pathb'].get[0]._enhancer).toBeCalledWith(pathObject); }); it('should apply use to all methods on the same path', () => { const pathConfig = {}; const map = { '/patha': { - use: fakeRequirement({ test: { deep: ['value1'] } }), - get: fakeRequirement({ test: { deep: ['value2'] } }), - post: fakeRequirement({ another: 'key' }), + use: [fakeRequirement({ test: { deep: ['value1'] } })], + get: [fakeRequirement({ test: { deep: ['value2'] } })], + post: [fakeRequirement({ another: 'key' })], }, }; expect(generatePathsFromOperations(pathConfig, map)).toEqual({ @@ -150,16 +150,16 @@ describe('generate', () => { }, }, }); - expect(map['/patha'].use._enhancer).toBeCalledWith(pathConfig); - expect(map['/patha'].get._enhancer).toBeCalledWith(pathConfig); - expect(map['/patha'].post._enhancer).toBeCalledWith(pathConfig); + expect(map['/patha'].use[0]._enhancer).toBeCalledWith(pathConfig); + expect(map['/patha'].get[0]._enhancer).toBeCalledWith(pathConfig); + expect(map['/patha'].post[0]._enhancer).toBeCalledWith(pathConfig); }); it('should correctly handle partial paths', () => { const components = {}; const map = { - '/patha': { post: fakeRequirement({ test: { deep: ['value1'] } }) }, + '/patha': { post: [fakeRequirement({ test: { deep: ['value1'] } })] }, '/pathb': { - get: fakeRequirement({ test: { deep: ['value2'] } }), + get: [fakeRequirement({ test: { deep: ['value2'] } })], }, pathc: {}, }; @@ -167,8 +167,8 @@ describe('generate', () => { '/patha': { post: { test: { deep: ['value1'] } } }, '/pathb': { get: { test: { deep: ['value2'] } } }, }); - expect(map['/patha'].post._enhancer).toBeCalledWith(components); - expect(map['/pathb'].get._enhancer).toBeCalledWith(components); + expect(map['/patha'].post[0]._enhancer).toBeCalledWith(components); + expect(map['/pathb'].get[0]._enhancer).toBeCalledWith(components); }); }); }); diff --git a/src/document/generate.ts b/src/document/generate.ts index c57eedf..ba33d4a 100644 --- a/src/document/generate.ts +++ b/src/document/generate.ts @@ -18,10 +18,18 @@ export const generatePathsFromOperations = ( record.filterMapWithIndex((path, v) => pipe( v, - record.mapWithIndex((method, requirements) => + record.mapWithIndex((method, values) => pipe( - requirements._enhancer(spec?.[path]?.[method as keyof ZodOpenApiPathItemObject] || {}), - (enhancement) => mergeDeep({}, spec?.[path]?.[method as 'get'] || {}, enhancement) + values, + array.reduce({}, (result, requirements) => + pipe( + requirements._enhancer( + spec?.[path]?.[method as keyof ZodOpenApiPathItemObject] || {} + ), + (enhancement) => + mergeDeep(result, spec?.[path]?.[method as 'get'] || {}, enhancement) + ) + ) ) ), option.fromPredicate(not(record.isEmpty)) @@ -59,7 +67,8 @@ export const generateComponentsFromOperations = ( getRecordValues, array.map(getRecordValues), array.flatten, - array.map((operation) => operation._components(components)), + array.map(array.map((operation) => operation._components(components))), + array.flatten, array.reduce(components, mergeDeep) ); diff --git a/src/document/index.ts b/src/document/index.ts index 13cb602..a150a01 100644 --- a/src/document/index.ts +++ b/src/document/index.ts @@ -15,7 +15,6 @@ export const document = ( openApiVersion: '3.1.0' | '3.0.0' | '3.0.1' | '3.0.2' | '3.0.3' = '3.1.0' ) => { const requirements = getStackRequirements(app); - console.log(requirements); return { info(info) { Object.assign(this._spec.info, info); diff --git a/src/document/types.ts b/src/document/types.ts index 25220e8..1390cca 100644 --- a/src/document/types.ts +++ b/src/document/types.ts @@ -31,4 +31,4 @@ export interface HasteDocument { } export const AllPathsKey = '!all'; -export type HasteRequirementMap = { [p: string]: Record> }; +export type HasteRequirementMap = { [p: string]: Record[]> }; From 0b8d0e36d084e3e1d68a6ea9ad2b9047dfa87a64 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 18 Jan 2024 00:45:13 +1100 Subject: [PATCH 05/13] Remove body schema from get request. --- docs/examples/simple/petHandler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/examples/simple/petHandler.ts b/docs/examples/simple/petHandler.ts index 6434112..34b3a2a 100644 --- a/docs/examples/simple/petHandler.ts +++ b/docs/examples/simple/petHandler.ts @@ -1,4 +1,4 @@ -import { z, ZodType } from 'zod'; +import { z } from 'zod'; import { HasteRequestHandler, requires } from 'express-haste'; import { AsyncCreationRequest, @@ -9,7 +9,6 @@ import { } from './schemas'; export const searchPetRequirements = requires() - .body(z.object({})) .query('id', PetId) .query('async', AsyncCreationRequest) .response('200', PetWithIdSchema) From 98c0054c987e7d63c02c22d99aac2bccab0da98c Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 18 Jan 2024 00:53:29 +1100 Subject: [PATCH 06/13] Update Readme --- README.md | 77 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 416d9ba..62129e7 100644 --- a/README.md +++ b/README.md @@ -46,25 +46,28 @@ A simple validated request in an app, looks as follows; import express, { json } from 'express'; import { document } from 'express-haste'; import cookieParser from 'cookie-parser'; -import { requiresMany } from "./requiresMany"; -import { requires, body, query, header } from "./index"; +import { requiresMany } from './requiresMany'; +import { requires, body, query, header } from './index'; import { z } from 'zod'; const app = express(); -app.post('/test', requires( - body(z.object({})), - query('someParam', z.string().default('somevalue')), - query('manyparam', z.string().array()), - header('x-my-header', z.string().uuid()) -), handler); +app.post('/test', requires() + .body(z.object({})) + .query('someParam', z.string().default('somevalue')) + .query('manyparam', z.string().array()) + .header('x-my-header', z.string().uuid()) + , handler); ``` #### `body(schema)` +`requires().body(z.string())` + Given a zod schema, validates the req.body matches that schema and will make it the required body for that request -in the documentation. You may only provide one body, more than one will result in undefined behaviour for now. +in the documentation. You may only provide one body, when providing many .body(), the last to be defined will be taken. + #### `header(key, schema)` @@ -73,8 +76,12 @@ Given a key and a ZodSchema, validate the header passes schema validation. *You should always start with z.string() here but you can use [.transform](https://zod.dev/?id=transform)* *and [.refine](https://zod.dev/?id=refine) to abstract validation logic.* +`requires().header('x-id', z.string())` + #### `cookie(key, schema)` +`requires().cookie('cookie', z.string())` + Given a key and a ZodSchema, validate the cookie passes schema validation, you will need the [cookie-parser middleware](https://expressjs.com/en/resources/middleware/cookie-parser.html) or similar for this to work. @@ -82,8 +89,11 @@ or similar for this to work. *You should always start with z.string() here but you can use [.transform](https://zod.dev/?id=transform)* *and [.refine](https://zod.dev/?id=refine) to abstract validation logic.* + #### `query(key, schema)` +`requires().query('page', z.string().array())` + Given a key and a Schema, validate a search/query parameter meets that validation, valid starting types are - `z.string()` For single values @@ -91,10 +101,14 @@ Given a key and a Schema, validate a search/query parameter meets that validatio #### `path(key, schema)` +`requires().path('id', z.string())` + Given a key and a Schema, validate a path parameter listed in your path, key should take the exact same name as the `:name` given to the parameter -#### `response(status, schema, {description})` +#### `response(status, schema, {description, contentType})` + +`requires().response('200', z.object({message: z.string()}))` Given a string status code, zod schema, and optionally a description, add this response to the documentation. This validator will **NOT** validate the response, but will provide type checking if using `HasteRequestHandler`. @@ -146,25 +160,25 @@ Who doesn't love having a typed application, you can add typing to your request here's an example; ```typescript -import express, { json } from "express"; -import { document } from "express-haste"; -import cookieParser from "cookie-parser"; -import { requiresMany } from "./requiresMany"; -import { requires, body, query, header, HasteRequestHandler } from "./index"; -import { z } from "zod"; +import express, { json } from 'express'; +import { document } from 'express-haste'; +import cookieParser from 'cookie-parser'; +import { requiresMany } from './requiresMany'; +import { requires, body, query, header, HasteRequestHandler } from './index'; +import { z } from 'zod'; const app = express(); -const testRequirements = requires( - body(z.object({ param: z.string().optional() })), - query("someParam", z.string().default("somevalue")), - query("manyparam", z.string().array()), - path("pathid", z.string().transform(z.number().parse)), - response(200, z.object({ - returnValue: z.number() - })) -) +const testRequirements = requires() + .body(z.object({ param: z.string().optional() })) + .query('someParam', z.string().default('somevalue')) + .query('manyparam', z.string().array()) + .path('pathid', z.string().transform(z.number().parse)) + .response(200, z.object({ + returnValue: z.number(), + }), + ); const handler: HasteRequestHandler = (req, res) => { req.body; // Will be {param?: string} @@ -176,7 +190,7 @@ const handler: HasteRequestHandler = (req, res) => { }); }; -app.post("/test/:pathid", testRequirements, handler); +app.post('/test/:pathid', testRequirements, handler); ``` ### Documenting @@ -187,7 +201,7 @@ You can then feed this into the openapi provider of your choice to generate docu import { document } from 'express-haste'; //... All your routing, it's important these have been finalised before you call document. -const spec = document(app, { +const spec = document(app).info({ appTitle: 'My First App', appVersion: '1.0.0' }) @@ -207,12 +221,13 @@ full end-to-end examples of how express-haste works. ### Roadmap * [X] Request Handler typing. -* [X] Improve test coverage. +* [X] Improve test coverage (97% coverage). * [X] Lint and Test checking in github actions. * [ ] Tests for typing (it's very fragile and hard to catch all edge cases manually). * [X] Explore whether typing can be made less complicated. * [ ] Ability to pass many parameters into one query, header, etc function call. ie; `query({q1: z.string(), q2: z.string()})`. -* [ ] Ability to customize error response when the request fails. -* [ ] Define behaviour for when many of the same body validation schemas are provided. -* [ ] Response validation and/or warning. \ No newline at end of file + * This is now unblocked +* [X] Ability to customize error response when the request fails. +* [X] Define behaviour for when many of the same body validation schemas are provided. +~~* [ ] Response validation and/or warning.~~ \ No newline at end of file From b03a970b616c8c17e97a7a3526ea048414d4352f Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Sat, 20 Jan 2024 16:05:58 +1100 Subject: [PATCH 07/13] Update example tests to reflect the change to use request keys. --- docs/examples/simple/index.spec.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/examples/simple/index.spec.ts b/docs/examples/simple/index.spec.ts index ee8fa5e..0f59f2b 100644 --- a/docs/examples/simple/index.spec.ts +++ b/docs/examples/simple/index.spec.ts @@ -2,19 +2,20 @@ import request from 'supertest'; import * as H from '../../../src'; import app from './index'; import 'express-haste'; +import { response } from 'express'; jest.mock('express-haste', () => H); describe('requires', () => { it('Should validate the request body', async () => { - await request(app) + const response = await request(app) .post('/pets/123') .send({ type: 'fish', breed: 'carp', vaccinated: true, }) - .expect({ + expect(response.body).toEqual({ type: 'about:blank', title: 'Bad request', detail: 'Request failed to validate', @@ -22,12 +23,12 @@ describe('requires', () => { { type: 'https://zod.dev/error_handling?id=zodissuecode', code: 'invalid_type', - path: ['header', 'authorization'], + path: ['headers', 'authorization'], message: 'Required', }, ], }) - .expect(400); + expect(response.status).toEqual(400); await request(app) .post('/pets/123') .auth('admin', 'password') @@ -66,7 +67,7 @@ describe('requires', () => { title: 'created', details: 'cat/breeds/tomcat', }) - .expect(200); + .expect(201); }); it('Should return a validation error when cookie is missing', async () => { @@ -119,7 +120,7 @@ describe('requires', () => { { type: 'https://zod.dev/error_handling?id=zodissuecode', code: 'invalid_string', - path: ['path', 'id'], + path: ['params', 'id'], message: 'Must be a valid pet identifier.', }, ], From 4a0fd759355a1573e131e7ee802b2b6d3dff7040 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 1 Feb 2024 22:36:05 +1100 Subject: [PATCH 08/13] Remove validator as you can just use a normal handler. --- src/requires/validators.spec.ts | 108 +------------------------------- src/requires/validators.ts | 59 +---------------- src/types/haste.ts | 7 ++- src/types/index.ts | 1 - 4 files changed, 8 insertions(+), 167 deletions(-) diff --git a/src/requires/validators.spec.ts b/src/requires/validators.spec.ts index 729deae..81eda67 100644 --- a/src/requires/validators.spec.ts +++ b/src/requires/validators.spec.ts @@ -277,116 +277,10 @@ describe('validators', () => { }); describe('auth', () => { - it('should return right by default', () => { + it('should return right', () => { const req = requires().auth('oauth', { type: 'http' }); const result = validateAll(req._effects, {} as express.Request); expect(either.isRight(result)).toEqual(true); }); - it('should return a right when validation function returns true', () => { - const validator = jest.fn().mockReturnValue(true); - const secScheme = { type: 'http' } as const; - const requirements = requires().auth('oauth', secScheme, { validator }); - const req = {} as express.Request; - const result = validateAll(requirements._effects, req); - expect(either.isRight(result)).toEqual(true); - expect(validator).toBeCalledWith(req, secScheme); - }); - it('should return a left when validation function returns false', () => { - const validator = jest.fn().mockReturnValue(false); - const secScheme = { type: 'http' } as const; - const requirements = requires().auth('oauth', secScheme, { validator }); - const req = {} as express.Request; - const result = validateAll(requirements._effects, req); - expect(either.isLeft(result)).toEqual(true); - expect(result).toEqual({ _tag: 'Left', left: { issues: [] } }); - expect(validator).toBeCalledWith(req, secScheme); - }); - it('should return a left when validation function returns a string error', () => { - const validator = jest.fn().mockReturnValue('this is what went wrong'); - const secScheme = { type: 'http' } as const; - const requirements = requires().auth('oauth', secScheme, { validator }); - const req = {} as express.Request; - const result = validateAll(requirements._effects, req); - expect(either.isLeft(result)).toEqual(true); - expect(result).toEqual({ - _tag: 'Left', - left: { - issues: [ - { - code: 'custom', - message: 'this is what went wrong', - path: ['authentication'], - }, - ], - }, - }); - expect(validator).toBeCalledWith(req, secScheme); - }); - it('should return a left when validation function throws an error', () => { - const validator = jest.fn().mockImplementation(() => { - throw new Error(); - }); - const secScheme = { type: 'http' } as const; - const requirements = requires().auth('oauth', secScheme, { validator }); - const req = {} as express.Request; - const result = validateAll(requirements._effects, req); - expect(either.isLeft(result)).toEqual(true); - expect(result).toEqual({ - _tag: 'Left', - left: { - issues: [], - }, - }); - expect(validator).toBeCalledWith(req, secScheme); - }); - it('should return a right when all validation functions return true', () => { - const validator1 = jest.fn().mockReturnValue(true); - const validator2 = jest.fn().mockReturnValue(true); - const secScheme = { type: 'http' } as const; - const requirements = requires() - .auth('oauth', secScheme, { validator: validator1 }) - .auth('another', secScheme, { validator: validator2 }); - const req = {} as express.Request; - const result = validateAll(requirements._effects, req); - expect(either.isRight(result)).toEqual(true); - expect(validator1).toBeCalledWith(req, secScheme); - expect(validator2).toBeCalledWith(req, secScheme); - }); - it('should return a left when any validation functions return false', () => { - const validator1 = jest.fn().mockReturnValue(true); - const validator2 = jest.fn().mockReturnValue(false); - const secScheme = { type: 'http' } as const; - const requirements = requires() - .auth('oauth', secScheme, { validator: validator1 }) - .auth('another', secScheme, { validator: validator2 }); - const req = {} as express.Request; - const result = validateAll(requirements._effects, req); - expect(either.isLeft(result)).toEqual(true); - expect(result).toEqual({ - _tag: 'Left', - left: { - issues: [], - }, - }); - expect(validator1).toBeCalledWith(req, secScheme); - expect(validator2).toBeCalledWith(req, secScheme); - }); - it('should return a left when any validation functions return false and other auth validator doesnt exist', () => { - const validator1 = jest.fn().mockReturnValue(false); - const secScheme = { type: 'http' } as const; - const requirements = requires() - .auth('oauth', secScheme, { validator: validator1 }) - .auth('another', secScheme); - const req = {} as express.Request; - const result = validateAll(requirements._effects, req); - expect(either.isLeft(result)).toEqual(true); - expect(result).toEqual({ - _tag: 'Left', - left: { - issues: [], - }, - }); - expect(validator1).toBeCalledWith(req, secScheme); - }); }); }); diff --git a/src/requires/validators.ts b/src/requires/validators.ts index ced808c..1be3584 100644 --- a/src/requires/validators.ts +++ b/src/requires/validators.ts @@ -51,64 +51,7 @@ const parameterValidator = option.getOrElseW(constant(either.right(undefined))) ); -const authValidator: Validator = (effect, req) => - pipe( - effect.auth, - option.fromNullable, - option.map((auth) => Object.values(auth)), - option.map( - array.map((method) => - pipe( - method.config?.validator, - option.fromNullable, - option.fold(constant(either.right(true)), (fn) => - pipe( - either.tryCatch( - () => fn(req, method.scheme), - () => new ZodError([]) - ), - either.chain( - either.fromPredicate( - (v): v is boolean => v === true, - (v) => - new ZodError( - typeof v === 'string' - ? [ - { - code: 'custom', - message: v, - path: ['authentication'], - }, - ] - : [] - ) - ) - ) - ) - ) - ) - ) - ), - option.fold( - constant(either.right(true)), - flow( - array.separate, - either.fromPredicate( - ({ left }) => !left.length, - ({ left }) => left - ), - either.mapLeft( - flow( - array.map(({ issues }) => issues), - array.sequence(Applicative), - array.flatten, - (issues) => new ZodError(issues) - ) - ), - either.map(constant(true)) - ) - ) - ); +const authValidator: Validator = constant(either.right(true)); const locationRequestMapping: Record = { path: 'params', diff --git a/src/types/haste.ts b/src/types/haste.ts index 4e4f80b..2013ef7 100644 --- a/src/types/haste.ts +++ b/src/types/haste.ts @@ -57,8 +57,13 @@ export type ExtendEffect< : ZodObject; }; +export type HasteCustomErrorHandler = ( + errors: ZodError, + res: express.Response, + next: NextFunction +) => void; export type RequirementConfig = { - errorHandler?: (errors: ZodError, res: express.Response, next: NextFunction) => void; + errorHandler?: HasteCustomErrorHandler; }; export interface Requires extends express.RequestHandler { diff --git a/src/types/index.ts b/src/types/index.ts index 49bb00d..47daa9a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -19,7 +19,6 @@ export type AuthConfig = { * @param method The security scheme to validate. * @returns {boolean | () => string} A literal true value when validation is successful, false or a string error when not successful */ - validator?: (req: express.Request, method: SecuritySchemeObject) => boolean | string; requireScopes?: string[]; }; From 215448292cf59b774d2ee8826aab6dea7ed67973 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 1 Feb 2024 22:52:30 +1100 Subject: [PATCH 09/13] Add example for error handling --- .../customErrorHandlingAndAuth/auth.ts | 37 + .../customErrorHandlingAndAuth/index.spec.ts | 11 + .../customErrorHandlingAndAuth/index.ts | 47 + .../customErrorHandlingAndAuth/jest.config.js | 6 + .../customErrorHandlingAndAuth/package.json | 26 + .../customErrorHandlingAndAuth/redoc.ts | 28 + .../customErrorHandlingAndAuth/serve.ts | 5 + .../customErrorHandlingAndAuth/yarn.lock | 2832 +++++++++++++++++ 8 files changed, 2992 insertions(+) create mode 100644 docs/examples/customErrorHandlingAndAuth/auth.ts create mode 100644 docs/examples/customErrorHandlingAndAuth/index.spec.ts create mode 100644 docs/examples/customErrorHandlingAndAuth/index.ts create mode 100644 docs/examples/customErrorHandlingAndAuth/jest.config.js create mode 100644 docs/examples/customErrorHandlingAndAuth/package.json create mode 100644 docs/examples/customErrorHandlingAndAuth/redoc.ts create mode 100644 docs/examples/customErrorHandlingAndAuth/serve.ts create mode 100644 docs/examples/customErrorHandlingAndAuth/yarn.lock diff --git a/docs/examples/customErrorHandlingAndAuth/auth.ts b/docs/examples/customErrorHandlingAndAuth/auth.ts new file mode 100644 index 0000000..bed8575 --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/auth.ts @@ -0,0 +1,37 @@ +import { requires } from '../../../src'; +import * as jwt from 'jsonwebtoken'; +import { z } from 'zod'; +import { HasteRequestHandler } from 'express-haste'; + +export const requireAuth = requires() + .auth('jwt', { + type: 'apiKey', + scheme: 'Bearer', + }) + .response('401', z.object({ message: z.literal('Unauthorized') })); + +const splitBearer = z.tuple([z.literal('Bearer'), z.string()]); +const tokenHeaderSchema = z + .string() + .transform((value) => splitBearer.parse(value.split(' ', 1))[1]); +/** + * This is NOT intended to be a reference for implementing secure jwt validation. + * This example is vastly oversimplified and inherently insecure, + * for details on a proper jwt implementation see https://www.npmjs.com/package/jsonwebtoken + */ +export const authValidator: HasteRequestHandler = (req, res, next) => { + try { + const rawToken = req.headers['authorization']; + const probablyToken = tokenHeaderSchema.parse(rawToken); + // IMPORTANT: this is being verified with a symmetric key, do not use in a real application + const token = jwt.verify(probablyToken, 'totally very secret'); + if (token) { + req.app.set('user', token) + next(); + } + } catch (e) { + res.status(401).json({ + message: 'Unauthorized', + }); + } +}; diff --git a/docs/examples/customErrorHandlingAndAuth/index.spec.ts b/docs/examples/customErrorHandlingAndAuth/index.spec.ts new file mode 100644 index 0000000..8692761 --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/index.spec.ts @@ -0,0 +1,11 @@ +import * as H from '../../../src'; +import 'express-haste'; + +jest.mock('express-haste'); + + +describe('customErrorHandlingAndAuth', () => { + it('should pass', () => { + /** End-to-End Tests coming soon TM **/ + }) +}); diff --git a/docs/examples/customErrorHandlingAndAuth/index.ts b/docs/examples/customErrorHandlingAndAuth/index.ts new file mode 100644 index 0000000..6076d8d --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/index.ts @@ -0,0 +1,47 @@ +import express, { json, Router } from 'express'; +import { HasteRequestHandler } from 'express-haste'; +import cookieParser from 'cookie-parser'; +import { HasteCustomErrorHandler, requires } from '../../../src'; +import { z } from 'zod'; +import { authValidator, requireAuth } from './auth'; + +const app: express.Express = express(); + +app.use(json()); +app.use(cookieParser()); +/** + * In this example we have 2 routes + * /public -> returns {message: "hello world"} + * /user -> accepts a jwt and returns the user object + * /docs -> view the documentation (no authentication) + */ + +const docRouter = Router(); +app.use('/docs', docRouter); + +const customErrorFunction: HasteCustomErrorHandler = (e, res) => + res.send({ + message: e.issues.map((i) => i.message).join(' and '), + }); + +const r = () => requires({ errorHandler: customErrorFunction }); + +// Get one pet is exempt from needing a header for demo reasons. +app.get('/public', r().response('200', z.object({ message: z.string() })), (_req, res) => + res.json({ + message: 'hello world', + }) +); + +// Require an authorization header for requests not public +app.use(requireAuth, authValidator); + +app.get('/user', r().response('200', z.object({}).passthrough().describe('Anything in the JWT'))); +app.post('/user'); + +const updateRequirements = requires({ errorHandler: customErrorFunction }) + .body(z.object({ email: z.string() }).describe('Update the email of this user')) + .response('202', z.object({ message: z.literal('Accepted') })); +const updateUser: HasteRequestHandler = (_req, res) => res.status(202); + +export default app; diff --git a/docs/examples/customErrorHandlingAndAuth/jest.config.js b/docs/examples/customErrorHandlingAndAuth/jest.config.js new file mode 100644 index 0000000..e677fd3 --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], + modulePathIgnorePatterns: ['/node_modules/', '/dist/'], +}; diff --git a/docs/examples/customErrorHandlingAndAuth/package.json b/docs/examples/customErrorHandlingAndAuth/package.json new file mode 100644 index 0000000..aa78457 --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/package.json @@ -0,0 +1,26 @@ +{ + "name": "simple-haste-example", + "version": "1.0.0", + "main": "index.ts", + "license": "MIT", + "scripts": { + "start": "ts-node ./serve.ts", + "test:ci": "jest" + }, + "dependencies": { + "@types/jsonwebtoken": "^9.0.5", + "body-parser": "^1.20.2", + "cookie-parser": "^1.4.6", + "express": "^4.18.2", + "express-haste": "0.0.6", + "jsonwebtoken": "^9.0.2", + "zod": "^3.22.4" + }, + "devDependencies": { + "jest": "^29.7.0", + "ts-node": "^10.9.2" + }, + "overrides": { + "@hookform/resolvers": "^3.3.1" + } +} diff --git a/docs/examples/customErrorHandlingAndAuth/redoc.ts b/docs/examples/customErrorHandlingAndAuth/redoc.ts new file mode 100644 index 0000000..b468921 --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/redoc.ts @@ -0,0 +1,28 @@ +export const getRedocHtml = ({ apiPath }: { apiPath: string }) => + ` + + + + Example Pet App docs + + + + + + + + + + + + + +` + .replace('{{apiPath}}', apiPath) diff --git a/docs/examples/customErrorHandlingAndAuth/serve.ts b/docs/examples/customErrorHandlingAndAuth/serve.ts new file mode 100644 index 0000000..3159569 --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/serve.ts @@ -0,0 +1,5 @@ +import app from "./index"; + +app.listen(3000, () => { + console.log('Check out http://localhost:3000/docs') +}) \ No newline at end of file diff --git a/docs/examples/customErrorHandlingAndAuth/yarn.lock b/docs/examples/customErrorHandlingAndAuth/yarn.lock new file mode 100644 index 0000000..122c92f --- /dev/null +++ b/docs/examples/customErrorHandlingAndAuth/yarn.lock @@ -0,0 +1,2832 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + +"@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" + integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.23.9" + "@babel/parser" "^7.23.9" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.23.6", "@babel/generator@^7.7.2": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + +"@babel/helpers@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== + dependencies: + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" + integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz#24f460c85dbbc983cd2b9c4994178bcc01df958f" + integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.3.3": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9", "@babel/types@^7.3.3": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@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.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + 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.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@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.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.22" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" + integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/babel__core@^7.1.14": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd" + integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ== + dependencies: + "@babel/types" "^7.20.7" + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jsonwebtoken@^9.0.5": + version "9.0.5" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz#0bd9b841c9e6c5a937c17656e2368f65da025588" + integrity sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.11.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.15.tgz#b853a86cfedbc768360c552b4653302b4e7417bf" + integrity sha512-gscmuADZfvNULx1eyirVbr3kVOVZtpQtzKMCZpeSZcN6MfbkRXAR4s9/gsQ4CzxLHw6EStDtKLNtSDL3vbq05A== + dependencies: + undici-types "~5.26.4" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.32" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" + integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== + dependencies: + "@types/yargs-parser" "*" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-walk@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^8.4.1: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.22.2: + version "4.22.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" + integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== + dependencies: + caniuse-lite "^1.0.30001580" + electron-to-chromium "^1.4.648" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001580: + version "1.0.30001582" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001582.tgz#db3070547ce0b48d9f44a509b86c4a02ba5d9055" + integrity sha512-vsJG3V5vgfduaQGVxL53uSX/HUzxyr2eA8xCo36OLal7sRcSZbibJtLeh0qja4sFOr/QQGt4opB4tOy+eOgAxg== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cjs-module-lexer@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-parser@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +dedent@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" + integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.648: + version "1.4.653" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.653.tgz#832ab25e80ad698ac09c1ca547bd9ee6cce7df10" + integrity sha512-wA2A2LQCqnEwQAvwADQq3KpMpNwgAUBnRmrFgRzHnPhbQUFArTR32Ab46f4p0MovDLcg4uqd4nCsN2hTltslpA== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +express-haste@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/express-haste/-/express-haste-0.0.6.tgz#a76899286111450f9525260822288eae056f83ca" + integrity sha512-J13rry4GbsC7kBWNhBjbC0gYZL/fthXkm71v6W1nKUChng1It/WA0e0F3mtuGm5/PEBaafzoLUqJEYZ3BEkpqA== + dependencies: + fp-ts "^2.16.1" + zod "^3.22.4" + zod-openapi "^2.11.0" + optionalDependencies: + swagger-ui-express "^5.0.0" + +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fp-ts@^2.16.1: + version "2.16.2" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.2.tgz#7faa90f6fc2e8cf84c711d2c4e606afe2be9e342" + integrity sha512-CkqAjnIKFqvo3sCyoBTqgJvF+bHrSik584S9nhTjtBESLx26cbtVMR/T9a6ApChOcSDAaM3JydDmWDUn4EEXng== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" + integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a" + integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + 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.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@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.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +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.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@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.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-inspect@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + 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" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pure-rand@^6.0.0: + version "6.0.4" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" + integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +safe-buffer@5.2.1, safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1" + integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== + dependencies: + define-data-property "^1.1.1" + function-bind "^1.1.2" + get-intrinsic "^1.2.2" + gopd "^1.0.1" + has-property-descriptors "^1.0.1" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-ui-dist@>=5.0.0: + version "5.11.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz#b423e820928df703586ff58f80b09ffcf2434e08" + integrity sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A== + +swagger-ui-express@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49" + integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA== + dependencies: + swagger-ui-dist ">=5.0.0" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.2.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" + integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +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== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod-openapi@^2.11.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/zod-openapi/-/zod-openapi-2.12.0.tgz#28b181ebd06283104f2ad7faab57d9f3d7684877" + integrity sha512-vTjUNJN7COtV36w5kVaKMpX6Anwn8fDWOBWkpJMnfbNJMjjM5IO8I//2N4cQbvB3EKOdX4ym/4Laa7lrtrWYLg== + +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== From 35a0b8054e06183172ff81a8d6ffcae0b8570ad5 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 1 Feb 2024 22:54:41 +1100 Subject: [PATCH 10/13] Update CI to include new example --- .github/workflows/test.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87b4b9c..9dcf894 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,10 +50,18 @@ jobs: - name: "Install project dependencies ⚙️" run: yarn install --frozen-lockfile - - name: "Install example dependencies ⚙️" + - name: "Install simple example dependencies ⚙️" working-directory: docs/examples/simple run: yarn install --frozen-lockfile - - name: "Test 🧪" + - name: "Install auth example dependencies ⚙️" + working-directory: docs/examples/customErrorHandlingAndAuth + run: yarn install --frozen-lockfile + + - name: "Test auth example 🧪" + working-directory: docs/examples/customErrorHandlingAndAuth + run: yarn test:ci + + - name: "Test simple example 🧪" working-directory: docs/examples/simple run: yarn test:ci \ No newline at end of file From 1a73533e9956af1483677e1b55e0b7c1cd5f7a09 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 1 Feb 2024 23:08:12 +1100 Subject: [PATCH 11/13] Just make the tests work... do better later --- .github/workflows/test.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9dcf894..b5d56c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,18 +50,10 @@ jobs: - name: "Install project dependencies ⚙️" run: yarn install --frozen-lockfile - - name: "Install simple example dependencies ⚙️" + - name: "Install example dependencies ⚙️" working-directory: docs/examples/simple run: yarn install --frozen-lockfile - - name: "Install auth example dependencies ⚙️" - working-directory: docs/examples/customErrorHandlingAndAuth - run: yarn install --frozen-lockfile - - - name: "Test auth example 🧪" - working-directory: docs/examples/customErrorHandlingAndAuth - run: yarn test:ci - - name: "Test simple example 🧪" working-directory: docs/examples/simple run: yarn test:ci \ No newline at end of file From 7092c25ba0b0fa47107d8f84cb6b1c88f2753d54 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 1 Feb 2024 23:13:25 +1100 Subject: [PATCH 12/13] v1.0.0-canary.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b65b8d8..1f53feb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-haste", - "version": "1.0.0", + "version": "1.0.0-canary.0", "description": "", "homepage": "https://github.com/avra-m3/express-haste#readme", "bugs": { From 3594b513a0918b67e7352f8493db3d221f9ba7f2 Mon Sep 17 00:00:00 2001 From: avra-m3 Date: Thu, 1 Feb 2024 23:14:43 +1100 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=99=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../customErrorHandlingAndAuth/package.json | 2 +- .../customErrorHandlingAndAuth/yarn.lock | 22 ++++--------------- docs/examples/simple/package.json | 2 +- docs/examples/simple/yarn.lock | 22 ++++--------------- 4 files changed, 10 insertions(+), 38 deletions(-) diff --git a/docs/examples/customErrorHandlingAndAuth/package.json b/docs/examples/customErrorHandlingAndAuth/package.json index aa78457..01b6c77 100644 --- a/docs/examples/customErrorHandlingAndAuth/package.json +++ b/docs/examples/customErrorHandlingAndAuth/package.json @@ -12,7 +12,7 @@ "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "express": "^4.18.2", - "express-haste": "0.0.6", + "express-haste": "1.0.0-canary.0", "jsonwebtoken": "^9.0.2", "zod": "^3.22.4" }, diff --git a/docs/examples/customErrorHandlingAndAuth/yarn.lock b/docs/examples/customErrorHandlingAndAuth/yarn.lock index 122c92f..beb1ce9 100644 --- a/docs/examples/customErrorHandlingAndAuth/yarn.lock +++ b/docs/examples/customErrorHandlingAndAuth/yarn.lock @@ -1237,16 +1237,14 @@ expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" -express-haste@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/express-haste/-/express-haste-0.0.6.tgz#a76899286111450f9525260822288eae056f83ca" - integrity sha512-J13rry4GbsC7kBWNhBjbC0gYZL/fthXkm71v6W1nKUChng1It/WA0e0F3mtuGm5/PEBaafzoLUqJEYZ3BEkpqA== +express-haste@1.0.0-canary.0: + version "1.0.0-canary.0" + resolved "https://registry.yarnpkg.com/express-haste/-/express-haste-1.0.0-canary.0.tgz#9ae7e4812885e9857294a8fe156dbf1dcb4b5a6e" + integrity sha512-kDrIDv9fIi70L8CtR0gYXKey/UWzVw69WPsflVBsUM+63asV+BELkfa823Jq1TTUxgesX9outV1fGSoD3mZ4MQ== dependencies: fp-ts "^2.16.1" zod "^3.22.4" zod-openapi "^2.11.0" - optionalDependencies: - swagger-ui-express "^5.0.0" express@^4.18.2: version "4.18.2" @@ -2620,18 +2618,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -swagger-ui-dist@>=5.0.0: - version "5.11.2" - resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz#b423e820928df703586ff58f80b09ffcf2434e08" - integrity sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A== - -swagger-ui-express@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49" - integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA== - dependencies: - swagger-ui-dist ">=5.0.0" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" diff --git a/docs/examples/simple/package.json b/docs/examples/simple/package.json index 9409062..4543dd2 100644 --- a/docs/examples/simple/package.json +++ b/docs/examples/simple/package.json @@ -11,7 +11,7 @@ "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "express": "^4.18.2", - "express-haste": "0.0.6", + "express-haste": "1.0.0-canary.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/docs/examples/simple/yarn.lock b/docs/examples/simple/yarn.lock index a686731..cd7700f 100644 --- a/docs/examples/simple/yarn.lock +++ b/docs/examples/simple/yarn.lock @@ -1218,16 +1218,14 @@ expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" -express-haste@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/express-haste/-/express-haste-0.0.6.tgz#a76899286111450f9525260822288eae056f83ca" - integrity sha512-J13rry4GbsC7kBWNhBjbC0gYZL/fthXkm71v6W1nKUChng1It/WA0e0F3mtuGm5/PEBaafzoLUqJEYZ3BEkpqA== +express-haste@1.0.0-canary.0: + version "1.0.0-canary.0" + resolved "https://registry.yarnpkg.com/express-haste/-/express-haste-1.0.0-canary.0.tgz#9ae7e4812885e9857294a8fe156dbf1dcb4b5a6e" + integrity sha512-kDrIDv9fIi70L8CtR0gYXKey/UWzVw69WPsflVBsUM+63asV+BELkfa823Jq1TTUxgesX9outV1fGSoD3mZ4MQ== dependencies: fp-ts "^2.16.1" zod "^3.22.4" zod-openapi "^2.11.0" - optionalDependencies: - swagger-ui-express "^5.0.0" express@^4.18.2: version "4.18.2" @@ -2532,18 +2530,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -swagger-ui-dist@>=5.0.0: - version "5.10.5" - resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.10.5.tgz" - integrity sha512-Uv8E7hV/nXALQKgW86X1i58gl1O6DFg+Uq54sDwhYqucBBxj/47dLNw872TNILNlOTuPA6dRvUMGQdmlpaX8qQ== - -swagger-ui-express@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz" - integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA== - dependencies: - swagger-ui-dist ">=5.0.0" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"