From 0475204dca8c0428a808028b1d0cf5754216472a Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:19:34 +0200 Subject: [PATCH 01/13] Fix admins not being allowed to access authenticators (#47) --- src/gewis/gewis.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gewis/gewis.ts b/src/gewis/gewis.ts index 2742c291f..2451f812a 100644 --- a/src/gewis/gewis.ts +++ b/src/gewis/gewis.ts @@ -394,6 +394,9 @@ export default class Gewis { this.roleManager.registerRole({ name: 'SudoSOS - BAC PM', permissions: { + Authenticator: { + ...admin, + }, Container: { ...admin, }, From 44e910852f1db4b5422231c949c064807a86f0a1 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Mon, 25 Sep 2023 07:42:59 +0200 Subject: [PATCH 02/13] Parallelize test suite runner (#64) * Parallelize test suite runner * Remove unnecessary mocha options * Add max number of jobs to prevent timeouts * Add separate coverage command to limit workers for ci runner --- .github/workflows/build.yml | 2 +- mocha.json | 7 ++----- package.json | 6 ++++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 747ff79f1..16439e01e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -95,7 +95,7 @@ jobs: if: steps.cache-node.outputs.cache-hit != 'true' - run: openssl genrsa -out ./config/jwt.key 2048 && chmod 0777 ./config/jwt.key - run: npm run swagger - - run: npm run coverage + - run: npm run coverage-ci # Separate command to limit the number of workers to prevent timeouts - run: git config --global --add safe.directory $GITHUB_WORKSPACE # To avoid dubious ownership - name: "Comment code coverage on PR" if: github.event_name == 'pull_request' diff --git a/mocha.json b/mocha.json index a6a567fc2..85a8fbc9e 100644 --- a/mocha.json +++ b/mocha.json @@ -1,6 +1,3 @@ { - "reporterEnabled": "spec, mocha-junit-reporter", - "mochaJunitReporterReporterOptions": { - "mochaFile": "reports/junit.xml" - } -} \ No newline at end of file + "reporterEnabled": "spec" +} diff --git a/package.json b/package.json index 581baa046..4533e75dd 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "schema": "node out/src/database/schema.js", "seed": "ts-node-dev --poll src/database/seed.ts", "swagger": "ts-node-dev --poll src/start/swagger.ts", - "test": "mocha -r ts-node/register --timeout 50000 --file ./test/setup.ts --reporter mocha-multi-reporters --reporter-options configFile=mocha.json 'test/**/*.ts' --exit", - "test-file": "mocha -r ts-node/register --timeout 10000 --file ./test/setup.ts", + "test": "mocha -r ts-node/register --parallel --timeout 50000 --require ./test/setup.ts 'test/**/*.ts' --exit", + "test-ci": "mocha -r ts-node/register --parallel --jobs 8 --timeout 50000 --require ./test/setup.ts 'test/**/*.ts' --exit", + "test-file": "mocha -r ts-node/register --timeout 10000 --require ./test/setup.ts", "coverage": "nyc npm run test", + "coverage-ci": "nyc npm run test-ci", "lint": "eslint", "lint-fix": "eslint src test --ext .js --ext .ts --fix", "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js" From ccaf01a8f4e6122a7b45df48eac30b30ac761df0 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:56:54 +0200 Subject: [PATCH 03/13] Feature/Events & planning (#48) * Created initial BorrelSchema entities + seeder * Created borrel-schema controller, service, response and request, update borrel-schema entities * Created basic GET in controller * Rename borrel schema and its related entities to events * Add tests for getEvents function * Fix and test event update and create functions * Add getShifts and (soft) delete shift functions * Add test for create and update shift endpoints * Add event shift answer update function and tests * Add first endpoints for events to new controller * Add PATCH event endpoint * Add event shift endpoints & fix swagger docs * Add endpoints to change shift availability and assignment & add type * Add endpoint to sync event answers and function to send reminder emails * Register controllers and timed events in production * Revert package-lock.json * Add explicit reference to shifts in event entity * Fix test suite * Increase EventService & EventController test coverage * Add endpoint to get number of times users were selected for shifts * @Yoronex fix ur lint * Reduce number of workers to prevent timeouts on CI runner --------- Co-authored-by: Sjoerd Co-authored-by: Job van de Ven Co-authored-by: Samuel Oosterholt --- package.json | 2 +- src/controller/event-controller.ts | 441 +++++++ src/controller/event-shift-controller.ts | 280 +++++ src/controller/request/event-request.ts | 77 ++ src/controller/response/event-response.ts | 109 ++ src/database/database.ts | 6 + src/entity/event/event-shift-answer.ts | 72 ++ src/entity/event/event-shift.ts | 45 + src/entity/event/event.ts | 70 ++ src/entity/user/user.ts | 6 +- src/helpers/query-filter.ts | 17 +- src/helpers/validators.ts | 42 + src/index.ts | 52 +- src/mailer/templates/forgot-event-planning.ts | 69 ++ src/service/event-service.ts | 507 ++++++++ test/seed.ts | 126 +- test/unit/controller/event-controller.ts | 1062 +++++++++++++++++ .../unit/controller/event-shift-controller.ts | 521 ++++++++ test/unit/service/event-service.ts | 918 ++++++++++++++ 19 files changed, 4416 insertions(+), 6 deletions(-) create mode 100644 src/controller/event-controller.ts create mode 100644 src/controller/event-shift-controller.ts create mode 100644 src/controller/request/event-request.ts create mode 100644 src/controller/response/event-response.ts create mode 100644 src/entity/event/event-shift-answer.ts create mode 100644 src/entity/event/event-shift.ts create mode 100644 src/entity/event/event.ts create mode 100644 src/mailer/templates/forgot-event-planning.ts create mode 100644 src/service/event-service.ts create mode 100644 test/unit/controller/event-controller.ts create mode 100644 test/unit/controller/event-shift-controller.ts create mode 100644 test/unit/service/event-service.ts diff --git a/package.json b/package.json index 4533e75dd..ae90d4f44 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "seed": "ts-node-dev --poll src/database/seed.ts", "swagger": "ts-node-dev --poll src/start/swagger.ts", "test": "mocha -r ts-node/register --parallel --timeout 50000 --require ./test/setup.ts 'test/**/*.ts' --exit", - "test-ci": "mocha -r ts-node/register --parallel --jobs 8 --timeout 50000 --require ./test/setup.ts 'test/**/*.ts' --exit", + "test-ci": "mocha -r ts-node/register --parallel --jobs 4 --timeout 50000 --require ./test/setup.ts 'test/**/*.ts' --exit", "test-file": "mocha -r ts-node/register --timeout 10000 --require ./test/setup.ts", "coverage": "nyc npm run test", "coverage-ci": "nyc npm run test-ci", diff --git a/src/controller/event-controller.ts b/src/controller/event-controller.ts new file mode 100644 index 000000000..b41f8d8db --- /dev/null +++ b/src/controller/event-controller.ts @@ -0,0 +1,441 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import log4js, { Logger } from 'log4js'; +import { Response } from 'express'; +import BaseController, { BaseControllerOptions } from './base-controller'; +import Policy from './policy'; +import { RequestWithToken } from '../middleware/token-middleware'; +import EventService, { + CreateEventParams, + EventFilterParameters, + parseEventFilterParameters, parseUpdateEventRequestParameters, UpdateEventAnswerParams, UpdateEventParams, +} from '../service/event-service'; +import { parseRequestPagination } from '../helpers/pagination'; +import { EventAnswerAssignmentRequest, EventAnswerAvailabilityRequest, EventRequest } from './request/event-request'; +import Event from '../entity/event/event'; +import EventShiftAnswer from '../entity/event/event-shift-answer'; +import { asShiftAvailability } from '../helpers/validators'; + +export default class EventController extends BaseController { + private logger: Logger = log4js.getLogger('EventLogger'); + + /** + * Create a new user controller instance. + * @param options - The options passed to the base controller. + */ + public constructor( + options: BaseControllerOptions, + ) { + super(options); + this.logger.level = process.env.LOG_LEVEL; + } + + public getPolicy(): Policy { + return { + '/': { + GET: { + policy: async (req) => this.roleManager.can( + req.token.roles, 'get', 'all', 'Event', ['*'], + ), + handler: this.getAllEvents.bind(this), + }, + POST: { + policy: async (req) => this.roleManager.can( + req.token.roles, 'create', 'all', 'Event', ['*'], + ), + body: { modelName: 'CreateEventRequest' }, + handler: this.createEvent.bind(this), + }, + }, + '/:id(\\d+)': { + GET: { + policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Event', ['*']), + handler: this.getSingleEvent.bind(this), + }, + PATCH: { + policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Event', ['*']), + handler: this.updateEvent.bind(this), + body: { modelName: 'UpdateEventRequest' }, + }, + DELETE: { + policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Event', ['*']), + handler: this.deleteEvent.bind(this), + }, + }, + '/:id(\\d+)/sync': { + POST: { + policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Event', ['*']), + handler: this.syncEventShiftAnswers.bind(this), + }, + }, + '/:eventId(\\d+)/shift/:shiftId(\\d+)/user/:userId(\\d+)/assign': { + PUT: { + policy: async (req) => this.roleManager.can(req.token.roles, 'assign', 'all', 'EventAnswer', ['*']), + handler: this.assignEventShift.bind(this), + body: { modelName: 'EventAnswerAssignmentRequest' }, + }, + }, + '/:eventId(\\d+)/shift/:shiftId(\\d+)/user/:userId(\\d+)/availability': { + PUT: { + policy: async (req) => this.roleManager.can(req.token.roles, 'assign', EventController.getRelation(req), 'EventAnswer', ['*']), + handler: this.updateShiftAvailability.bind(this), + body: { modelName: 'EventAnswerAvailabilityRequest' }, + }, + }, + }; + } + + private static getRelation(req: RequestWithToken): string { + return req.params.userId === req.token.user.id.toString() ? 'own' : 'all'; + } + + /** + * Get all events + * @route GET /events + * @group events - Operations of the event controller + * @operationId getAllEvents + * @security JWT + * @param {string} name.query - Name of the event + * @param {integer} createdById.query - ID of user that created the event + * @param {string} beforeDate.query - Get only events that start after this date + * @param {string} afterDate.query - Get only events that start before this date + * @param {string} type.query - Get only events that are this type + * @param {integer} take.query - How many entries the endpoint should return + * @param {integer} skip.query - How many entries should be skipped (for pagination) + * @returns {PaginatedBaseEventResponse.model} 200 - All existing events + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async getAllEvents(req: RequestWithToken, res: Response): Promise { + this.logger.trace('Get all events by user', req.token.user); + + let take; + let skip; + try { + const pagination = parseRequestPagination(req); + take = pagination.take; + skip = pagination.skip; + } catch (e) { + res.status(400).json(e.message); + return; + } + + let filters: EventFilterParameters; + try { + filters = parseEventFilterParameters(req); + } catch (e) { + res.status(400).json(e.message); + return; + } + + // Handle request + try { + const events = await EventService.getEvents(filters, { take, skip }); + res.json(events); + } catch (e) { + this.logger.error('Could not return all events:', e); + res.status(500).json('Internal server error.'); + } + } + + /** + * Get a single event with its answers and shifts + * @route GET /events/{id} + * @group events - Operations of the event controller + * @operationId getSingleEvent + * @security JWT + * @param {integer} id.path.required - The id of the event which should be returned + * @returns {EventResponse.model} 200 - All existing events + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async getSingleEvent(req: RequestWithToken, res: Response) { + const { id } = req.params; + this.logger.trace('Get single event with ID', id, 'by', req.token.user); + + try { + const parsedId = Number.parseInt(id, 10); + const event = await EventService.getSingleEvent(parsedId); + if (event == null) { + res.status(404).send(); + return; + } + res.json(event); + } catch (error) { + this.logger.error('Could not return single event:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Create an event with its corresponding answers objects + * @route POST /events + * @group events - Operations of the event controller + * @operationId createEvent + * @security JWT + * @param {CreateEventRequest.model} body.body.required + * @returns {EventResponse.model} 200 - Created event + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async createEvent(req: RequestWithToken, res: Response) { + const body = req.body as EventRequest; + this.logger.trace('Create event', body, 'by user', req.token.user); + + let params: CreateEventParams; + try { + params = { + ...await parseUpdateEventRequestParameters(req), + createdById: req.token.user.id, + }; + } catch (e) { + res.status(400).json(e.message); + return; + } + + // handle request + try { + res.json(await EventService.createEvent(params)); + } catch (error) { + this.logger.error('Could not create event:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Update an event with its corresponding answers objects + * @route PATCH /events/{id} + * @group events - Operations of the event controller + * @operationId updateEvent + * @security JWT + * @param {integer} id.path.required - The id of the event which should be returned + * @param {UpdateEventRequest.model} body.body.required + * @returns {EventResponse.model} 200 - Created event + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async updateEvent(req: RequestWithToken, res: Response) { + const { id } = req.params; + const body = req.body as EventRequest; + this.logger.trace('Update event', id, 'with body', body, 'by user', req.token.user); + + let parsedId = Number.parseInt(id, 10); + try { + const event = await EventService.getSingleEvent(parsedId); + if (event == null) { + res.status(404).send(); + return; + } + } catch (error) { + this.logger.error('Could not update event:', error); + res.status(500).json('Internal server error.'); + } + + let params: Partial; + try { + params = { + ...await parseUpdateEventRequestParameters(req, true, parsedId), + }; + } catch (e) { + res.status(400).json(e.message); + return; + } + + // handle request + try { + res.json(await EventService.updateEvent(parsedId, params)); + } catch (error) { + this.logger.error('Could not update event:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Delete an event with its answers + * @route DELETE /events/{id} + * @group events - Operations of the event controller + * @operationId deleteEvent + * @security JWT + * @param {integer} id.path.required - The id of the event which should be deleted + * @returns {string} 204 - Success + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async deleteEvent(req: RequestWithToken, res: Response) { + const { id } = req.params; + this.logger.trace('Get single event with ID', id, 'by', req.token.user); + + try { + const parsedId = Number.parseInt(id, 10); + const event = await EventService.getSingleEvent(parsedId); + if (event == null) { + res.status(404).send(); + return; + } + + await EventService.deleteEvent(parsedId); + res.status(204).send(); + } catch (error) { + this.logger.error('Could not delete event:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Synchronize an event, so that EventShiftAnswers are created/deleted + * for users that are (no longer) part of a shift + * @route GET /events/{id} + * @group events - Operations of the event controller + * @operationId syncEventShiftAnswers + * @security JWT + * @param {integer} id.path.required - The id of the event which should be returned + * @returns {EventResponse.model} 200 - All existing events + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async syncEventShiftAnswers(req: RequestWithToken, res: Response) { + const { id } = req.params; + this.logger.trace('Synchronise single event with ID', id, 'by', req.token.user); + + try { + const parsedId = Number.parseInt(id, 10); + const event = await Event.findOne({ where: { id: parsedId }, relations: ['answers', 'shifts'] }); + if (event == null) { + res.status(404).send(); + return; + } + + await EventService.syncEventShiftAnswers(event); + res.status(200).json(await EventService.getSingleEvent(parsedId)); + } catch (error) { + this.logger.error('Could not synchronize event answers:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Change the assignment of users to shifts on an event + * @route PUT /events/{eventId}/shift/{shiftId}/user/{userId}/assign + * @group events - Operations of the event controller + * @operationId assignEventShift + * @security JWT + * @param {integer} eventId.path.required - The id of the event + * @param {integer} shiftId.path.required - The id of the shift + * @param {integer} userId.path.required - The id of the user + * @param {EventAnswerAssignmentRequest.model} body.body.required + * @returns {BaseEventAnswerResponse.model} 200 - Created event + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async assignEventShift(req: RequestWithToken, res: Response) { + const { eventId: rawEventId, shiftId: rawShiftId, userId: rawUserId } = req.params; + const body = req.body as EventAnswerAssignmentRequest; + this.logger.trace('Update event shift selection for event', rawEventId, 'for shift', rawShiftId, 'for user', rawUserId, 'by', req.token.user); + + let eventId = Number.parseInt(rawEventId, 10); + let shiftId = Number.parseInt(rawShiftId, 10); + let userId = Number.parseInt(rawUserId, 10); + try { + const answer = await EventShiftAnswer.findOne({ where: { eventId, shiftId, userId }, relations: ['event'] }); + if (answer == null) { + res.status(404).send(); + return; + } + if (answer.event.startDate.getTime() < new Date().getTime()) { + res.status(400).json('Event has already started or is already over.'); + return; + } + } catch (error) { + this.logger.error('Could not update event:', error); + res.status(500).json('Internal server error.'); + return; + } + + let params: Partial = { + selected: body.selected, + }; + + // handle request + try { + const answer = await EventService.updateEventShiftAnswer(eventId, shiftId, userId, params); + res.json(answer); + } catch (error) { + this.logger.error('Could not update event:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Update the availability of a user for a shift in an event + * @route POST /events/{eventId}/shift/{shiftId}/user/{userId}/availability + * @group events - Operations of the event controller + * @operationId updateEventShiftAvailability + * @security JWT + * @param {integer} eventId.path.required - The id of the event + * @param {integer} shiftId.path.required - The id of the shift + * @param {integer} userId.path.required - The id of the user + * @param {EventAnswerAvailabilityRequest.model} body.body.required + * @returns {BaseEventAnswerResponse.model} 200 - Created event + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async updateShiftAvailability(req: RequestWithToken, res: Response) { + const { userId: rawUserId, shiftId: rawShiftId, eventId: rawEventId } = req.params; + const body = req.body as EventAnswerAvailabilityRequest; + this.logger.trace('Update event shift availability for user', rawUserId, 'for shift', rawShiftId, 'for event', rawEventId, 'by', req.token.user); + + let userId = Number.parseInt(rawUserId, 10); + let shiftId = Number.parseInt(rawShiftId, 10); + let eventId = Number.parseInt(rawEventId, 10); + try { + const answer = await EventShiftAnswer.findOne({ where: { eventId, shiftId, userId }, relations: ['event'] }); + if (answer == null) { + res.status(404).send(); + return; + } + if (answer.event.startDate.getTime() < new Date().getTime()) { + res.status(400).json('Event has already started or is already over.'); + return; + } + } catch (error) { + this.logger.error('Could not update event:', error); + res.status(500).json('Internal server error.'); + return; + } + + let params: Partial; + try { + params = { + availability: asShiftAvailability(body.availability), + }; + } catch (e) { + res.status(400).json('Invalid event availability.'); + return; + } + + // handle request + try { + const answer = await EventService.updateEventShiftAnswer(eventId, shiftId, userId, params); + res.json(answer); + } catch (error) { + this.logger.error('Could not update event:', error); + res.status(500).json('Internal server error.'); + } + } +} diff --git a/src/controller/event-shift-controller.ts b/src/controller/event-shift-controller.ts new file mode 100644 index 000000000..5449e7f6e --- /dev/null +++ b/src/controller/event-shift-controller.ts @@ -0,0 +1,280 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import BaseController, { BaseControllerOptions } from './base-controller'; +import { Response } from 'express'; +import log4js, { Logger } from 'log4js'; +import Policy from './policy'; +import { RequestWithToken } from '../middleware/token-middleware'; +import EventService, { ShiftSelectedCountParams } from '../service/event-service'; +import { EventShiftRequest } from './request/event-request'; +import EventShift from '../entity/event/event-shift'; +import { parseRequestPagination } from '../helpers/pagination'; +import { asDate, asEventType } from '../helpers/validators'; + +export default class EventShiftController extends BaseController { + private logger: Logger = log4js.getLogger('EventShiftLogger'); + + /** + * Create a new user controller instance. + * @param options - The options passed to the base controller. + */ + public constructor( + options: BaseControllerOptions, + ) { + super(options); + this.logger.level = process.env.LOG_LEVEL; + } + + public getPolicy(): Policy { + return { + '/': { + GET: { + policy: async (req) => this.roleManager.can( + req.token.roles, 'get', 'all', 'Event', ['*'], + ), + handler: this.getAllShifts.bind(this), + }, + POST: { + policy: async (req) => this.roleManager.can( + req.token.roles, 'create', 'all', 'Event', ['*'], + ), + handler: this.createShift.bind(this), + }, + }, + '/:id(\\d+)': { + PATCH: { + policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Event', ['*']), + handler: this.updateShift.bind(this), + }, + DELETE: { + policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Event', ['*']), + handler: this.deleteShift.bind(this), + }, + }, + '/:id(\\d+)/counts': { + GET: { + policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Event', ['*']), + handler: this.getShiftSelectedCount.bind(this), + }, + }, + }; + } + + /** + * Get all event shifts + * @route GET /eventshifts + * @group events - Operations of the event controller + * @operationId getAllEventShifts + * @security JWT + * @param {integer} take.query - How many entries the endpoint should return + * @param {integer} skip.query - How many entries should be skipped (for pagination) + * @returns {PaginatedEventShiftResponse.model} 200 - All existing event shifts + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async getAllShifts(req: RequestWithToken, res: Response) { + this.logger.trace('Get all shifts by user', req.token.user); + + let take; + let skip; + try { + const pagination = parseRequestPagination(req); + take = pagination.take; + skip = pagination.skip; + } catch (e) { + res.status(400).send(e.message); + return; + } + + try { + const shifts = await EventService.getEventShifts({ take, skip }); + res.json(shifts); + } catch (e) { + this.logger.error('Could not return all shifts:', e); + res.status(500).json('Internal server error.'); + } + } + + /** + * Create an event shift + * @route POST /eventshifts + * @group events - Operations of the event controller + * @operationId createEventShift + * @security JWT + * @param {CreateEventShiftRequest.model} body.body.required + * @returns {EventShiftResponse.model} 200 - Created event shift + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async createShift(req: RequestWithToken, res: Response) { + const body = req.body as EventShiftRequest; + this.logger.trace('Create shift', body, 'by user', req.token.user); + + let params: EventShiftRequest; + try { + params = { + name: req.body.name.toString(), + roles: req.body.roles, + }; + if (params.name === '' || !Array.isArray(params.roles)) { + res.status(400).json('Invalid shift.'); + return; + } + } catch (e) { + res.status(400).json('Invalid shift.'); + return; + } + + // handle request + try { + res.json(await EventService.createEventShift(params)); + } catch (error) { + this.logger.error('Could not create event shift:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Update an event shift + * @route PATCH /eventshifts/{id} + * @group events - Operations of the event controller + * @operationId updateEventShift + * @security JWT + * @param {integer} id.path.required - The id of the event which should be returned + * @param {UpdateEventShiftRequest.model} body.body.required + * @returns {EventShiftResponse.model} 200 - Created event shift + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async updateShift(req: RequestWithToken, res: Response) { + const { id: rawId } = req.params; + const body = req.body as EventShiftRequest; + this.logger.trace('Update shift', rawId, 'with body', body, 'by user', req.token.user); + + let id = Number.parseInt(rawId, 10); + try { + const shift = await EventShift.findOne({ where: { id } }); + if (shift == null) { + res.status(404).send(); + return; + } + } catch (error) { + this.logger.error('Could not update event:', error); + res.status(500).json('Internal server error.'); + } + + let param: Partial; + try { + param = { + name: req.body.name?.toString(), + roles: req.body.roles, + }; + if (param.name === '' || (param.roles !== undefined && !Array.isArray(param.roles))) { + res.status(400).json('Invalid shift.'); + return; + } + } catch (e) { + res.status(400).json('Invalid shift.'); + return; + } + + // handle request + try { + res.json(await EventService.updateEventShift(id, param)); + } catch (error) { + this.logger.error('Could not update event shift:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Delete an event shift with its answers + * @route DELETE /eventshifts/{id} + * @group events - Operations of the event controller + * @operationId deleteEventShift + * @security JWT + * @param {integer} id.path.required - The id of the event which should be deleted + * @returns {string} 204 - Success + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async deleteShift(req: RequestWithToken, res: Response) { + const { id: rawId } = req.params; + this.logger.trace('Delete shift with ID', rawId, 'by user', req.token.user); + + try { + const id = Number.parseInt(rawId, 10); + const shift = await EventShift.findOne({ where: { id }, withDeleted: true }); + if (shift == null) { + res.status(404).send(); + return; + } + + await EventService.deleteEventShift(id); + res.status(204).send(); + } catch (error) { + this.logger.error('Could not delete event shift:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Get the number of times a user has been selected for the given shift + * @route GET /eventshifts/{id}/counts + * @group events - Operations of the event controller + * @operationId getAllEventShifts + * @security JWT + * @param {string} eventType.query - Only include events of this type + * @param {string} afterDate.query - Only include events after this date + * @param {string} beforeDate.query - Only include events before this date + * @returns {PaginatedEventShiftResponse.model} 200 - All existing event shifts + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async getShiftSelectedCount(req: RequestWithToken, res: Response) { + const { id: rawId } = req.params; + this.logger.trace('Delete shift with ID', rawId, 'by user', req.token.user); + + try { + const id = Number.parseInt(rawId, 10); + const shift = await EventShift.findOne({ where: { id } }); + if (shift == null) { + res.status(404).send(); + return; + } + + let params: ShiftSelectedCountParams; + try { + params = { + eventType: asEventType(req.query.eventType), + afterDate: asDate(req.query.afterDate), + beforeDate: asDate(req.query.beforeDate), + }; + } catch (e) { + res.status(400).send(e.message); + return; + } + + const counts = await EventService.getShiftSelectedCount(id, params); + res.json(counts); + } catch (error) { + this.logger.error('Could not get event shift counts:', error); + res.status(500).json('Internal server error.'); + } + } +} diff --git a/src/controller/request/event-request.ts b/src/controller/request/event-request.ts new file mode 100644 index 000000000..7dc861292 --- /dev/null +++ b/src/controller/request/event-request.ts @@ -0,0 +1,77 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Availability } from '../../entity/event/event-shift-answer'; + +/** + * @typedef CreateEventRequest + * @property {string} name.required - Name of the event. + * @property {string} startDate.required - The starting date of the event. + * @property {string} endDate.required - The end date of the event. + * @property {string} type - The type of the event. + * @property {Array} shiftIds.required - IDs of shifts that are in this event + * per participant per borrel. + */ + +/** + * @typedef UpdateEventRequest + * @property {string} name - Name of the event. + * @property {string} startDate - The starting date of the event. + * @property {string} endDate - The end date of the event. + * @property {string} type - The type of the event. + * @property {Array} shiftIds - IDs of shifts that are in this event + * per participant per borrel. + */ +export interface EventRequest { + name: string, + startDate: string, + endDate: string, + type: string, + shiftIds: number[], +} + +/** + * @typedef CreateShiftRequest + * @property {string} name.required - Name of the event + * @property {Array} roles.required - Roles that (can) have this shift + */ + +/** + * @typedef UpdateShiftRequest + * @property {string} name - Name of the event + * @property {Array} roles - Roles that (can) have this shift + */ +export interface EventShiftRequest { + name: string, + roles: string[], +} + +/** + * @typedef EventAnswerAssignmentRequest + * @property {boolean} selected.required - Whether this user is selected for the given shift at the given event + */ +export interface EventAnswerAssignmentRequest { + selected: boolean, +} + +/** + * @typedef EventAnswerAvailabilityRequest + * @property {string} availability.required - New availability of the given user for the given event (YES, NO, LATER, NA) + */ +export interface EventAnswerAvailabilityRequest { + availability: Availability, +} diff --git a/src/controller/response/event-response.ts b/src/controller/response/event-response.ts new file mode 100644 index 000000000..4f346781e --- /dev/null +++ b/src/controller/response/event-response.ts @@ -0,0 +1,109 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { PaginationResult } from '../../helpers/pagination'; +import BaseResponse from './base-response'; +import { BaseUserResponse } from './user-response'; +import { EventType } from '../../entity/event/event'; + +/** + * @typedef {BaseResponse} BaseEventResponse + * @property {string} name.required - Name of the borrel. + * @property {BaseUserResponse.model} createdBy.required - Creator of the event. + * @property {string} startDate.required - The starting date of the event. + * @property {string} endDate.required - The end date of the event. + * @property {string} type.required - The tpye of event. + */ +export interface BaseEventResponse extends BaseResponse { + createdBy: BaseUserResponse, + name: string, + startDate: string, + endDate: string, + type: EventType, +} + +/** + * @typedef {BaseResponse} BaseEventShiftResponse + * @property {string} name.required - Name of the shift. + */ +export interface BaseEventShiftResponse extends BaseResponse { + name: string, +} + +/** + * @typedef {BaseEventShiftResponse} EventShiftResponse + * @property {Array} roles.required - Which roles can fill in this shift. + */ +export interface EventShiftResponse extends BaseEventShiftResponse { + roles: string[], +} + +/** + * @typedef {EventShiftResponse} EventInShiftResponse + * @property {Array} answers - Answers for this shift. + */ +export interface EventInShiftResponse extends EventShiftResponse { + answers: BaseEventAnswerResponse[]; +} + +/** + * @typedef PaginatedEventShiftResponse + * @property {PaginationResult.model} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned event shifts + */ +export interface PaginatedEventShiftResponse { + _pagination: PaginationResult, + records: EventShiftResponse[], +} + +/** + * @typedef {BaseEventResponse} EventResponse + * @property {Array} shifts.required - Shifts for this event + */ +export interface EventResponse extends BaseEventResponse { + shifts: EventInShiftResponse[], +} + +/** + * @typedef BaseEventAnswerResponse + * @property {BaseUserResponse.model} user.required - Participant that filled in their availability + * @property {string} availability - Filled in availability per slot. + * @property {boolean} selected.required - Whether this user is selected for the shift in the event + */ +export interface BaseEventAnswerResponse { + user: BaseUserResponse, + availability: string, + selected: boolean, +} + +/** + * @typedef PaginatedBaseEventResponse + * @property {PaginationResult.model} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned borrel Schemas + */ +export interface PaginatedBaseEventResponse { + _pagination: PaginationResult, + records: BaseEventResponse[], +} + +/** + * @typedef {BaseUserResponse} EventPlanningSelectedCount + * @property {integer} count.required - Number of times this user was selected for this shift + */ +export interface EventPlanningSelectedCount extends BaseUserResponse { + count: number; +} diff --git a/src/database/database.ts b/src/database/database.ts index f689d599d..c390e290e 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -63,6 +63,9 @@ import KeyAuthenticator from '../entity/authenticator/key-authenticator'; import Fine from '../entity/fine/fine'; import FineHandoutEvent from '../entity/fine/fineHandoutEvent'; import UserFineGroup from '../entity/fine/userFineGroup'; +import Event from '../entity/event/event'; +import EventShiftAnswer from '../entity/event/event-shift-answer'; +import EventShift from '../entity/event/event-shift'; export default class Database { public static async initialize(): Promise { @@ -126,6 +129,9 @@ export default class Database { BannerImage, AssignedRole, ResetToken, + Event, + EventShift, + EventShiftAnswer, ], }; return createConnection(options); diff --git a/src/entity/event/event-shift-answer.ts b/src/entity/event/event-shift-answer.ts new file mode 100644 index 000000000..2d13ab570 --- /dev/null +++ b/src/entity/event/event-shift-answer.ts @@ -0,0 +1,72 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { + Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, +} from 'typeorm'; +import User from '../user/user'; +import Event from './event'; +import EventShift from './event-shift'; +import BaseEntityWithoutId from '../base-entity-without-id'; + +export enum Availability { + YES = 'YES', + MAYBE = 'MAYBE', + LATER = 'LATER', + NO = 'NO', + NA = 'NA', +} + +/** + * @typedef {BaseEntity} EventShiftAnswer + * @property {User.model} user - Participant that filled in their availability + * @property {enum} availability - Filled in availability per slot. + * @property {boolean} selected - Indicator whether the person has the related shift + * during the related borrel. + * @property {EventShift.model} shift - Shift that answers are related to. + * @property {Event.model} event - Event that answers are related to + */ + +@Entity() +export default class EventShiftAnswer extends BaseEntityWithoutId { + @PrimaryColumn({ nullable: false }) + public userId: number; + + @ManyToOne(() => User, { nullable: false, eager: true }) + @JoinColumn({ name: 'userId' }) + public user: User; + + @Column({ nullable: true }) + public availability: Availability | null; + + @Column({ default: false }) + public selected: boolean; + + @PrimaryColumn({ nullable: false }) + public shiftId: number; + + @ManyToOne(() => EventShift, { onDelete: 'RESTRICT' }) + @JoinColumn({ name: 'shiftId' }) + public shift: EventShift; + + @PrimaryColumn({ nullable: false }) + public eventId: number; + + @ManyToOne(() => Event, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'eventId' }) + public event: Event; +} diff --git a/src/entity/event/event-shift.ts b/src/entity/event/event-shift.ts new file mode 100644 index 000000000..14c434eba --- /dev/null +++ b/src/entity/event/event-shift.ts @@ -0,0 +1,45 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { + Column, DeleteDateColumn, Entity, +} from 'typeorm'; +import BaseEntity from '../base-entity'; + +/** + * @typedef {BaseEntity} EventShift + * @property {string} name - Name of the shift. + * @property {boolean} default - Indicator whether the shift is a regular shift. + */ + +@Entity() +export default class EventShift extends BaseEntity { + @DeleteDateColumn() + public deletedAt?: Date | null; + + @Column() + public name: string; + + @Column({ + type: 'varchar', + transformer: { + to: (val: string[]) => JSON.stringify(val), + from: (val: string) => JSON.parse(val), + }, + }) + public roles: string[]; +} diff --git a/src/entity/event/event.ts b/src/entity/event/event.ts new file mode 100644 index 000000000..d3df0b81a --- /dev/null +++ b/src/entity/event/event.ts @@ -0,0 +1,70 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { + Column, Entity, ManyToOne, JoinColumn, OneToMany, ManyToMany, JoinTable, +} from 'typeorm'; +import BaseEntity from '../base-entity'; +import User from '../user/user'; +import EventShiftAnswer from './event-shift-answer'; +import EventShift from './event-shift'; + +export enum EventType { + BORREL = 'BORREL', // Weekly GEWIS borrel, both normal borrels and extended borrels + EXTERNAL_BORREL = 'EXTERNAL_BORREL', // Borrel with/for external party + OTHER = 'OTHER', // All other activities +} + +/** + * @typedef {BaseEntity} Event + * @property {string} name - Name of the event. + * @property {User.model} createdBy - Creator of the event. + * @property {string} startDate - The starting date from which the banner should be shown. + * @property {string} endDate - The end date from which the banner should no longer be shown. + */ + +@Entity() +export default class Event extends BaseEntity { + @Column() + public name: string; + + @JoinColumn() + @ManyToOne(() => User, { nullable: false, eager: true }) + public createdBy: User; + + @Column({ + type: 'datetime', + }) + public startDate: Date; + + @Column({ + type: 'datetime', + }) + public endDate: Date; + + @Column({ + nullable: false, + }) + public type: EventType; + + @ManyToMany(() => EventShift) + @JoinTable() + public shifts: EventShift[]; + + @OneToMany(() => EventShiftAnswer, (a) => a.event) + public answers: EventShiftAnswer[]; +} diff --git a/src/entity/user/user.ts b/src/entity/user/user.ts index 3eedf6bf1..d2e93a5a4 100644 --- a/src/entity/user/user.ts +++ b/src/entity/user/user.ts @@ -16,10 +16,11 @@ * along with this program. If not, see . */ import { - Column, Entity, JoinColumn, OneToOne, + Column, Entity, JoinColumn, OneToMany, OneToOne, } from 'typeorm'; import BaseEntity from '../base-entity'; import UserFineGroup from '../fine/userFineGroup'; +import AssignedRole from '../roles/assigned-role'; export enum TermsOfServiceStatus { ACCEPTED = 'ACCEPTED', @@ -123,4 +124,7 @@ export default class User extends BaseEntity { }) @JoinColumn() public currentFines?: UserFineGroup | null; + + @OneToMany(() => AssignedRole, (role) => role.user) + public roles: AssignedRole[]; } diff --git a/src/helpers/query-filter.ts b/src/helpers/query-filter.ts index 02e248323..ba1be0e18 100644 --- a/src/helpers/query-filter.ts +++ b/src/helpers/query-filter.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ import { - FindOptionsWhere, + FindOptionsWhere, Like, SelectQueryBuilder, } from 'typeorm'; import { asNumber } from './validators'; @@ -82,7 +82,20 @@ export default class QueryFilter { const value = params[param]; if (value !== undefined) { const property: string = mapping[param]; - where[property] = value; + const split = property.split('.'); + if (split.length === 1 && property.substring(0, 1) === '%') { + // No dot, so no nested where clause. However, search starts with a "%" + where[property.substring(1)] = Like(`%${value}%`); + } else if (split.length === 1) { + // No dot, so no nested where clause and no LIKE-search + where[property] = value; + // No + } else { + // Where clause is nested, so where clause should be an object + const newMapping: any = {}; + newMapping[param] = split.slice(1).join('.'); + where[split[0]] = this.createFilterWhereClause(newMapping, params); + } } }); diff --git a/src/helpers/validators.ts b/src/helpers/validators.ts index 75fee5d34..1f5ea46cd 100644 --- a/src/helpers/validators.ts +++ b/src/helpers/validators.ts @@ -21,6 +21,8 @@ import { VatDeclarationPeriod } from '../entity/vat-group'; import { UserType } from '../entity/user/user'; import { Dinero } from 'dinero.js'; import DineroTransformer from '../entity/transformer/dinero-transformer'; +import { Availability } from '../entity/event/event-shift-answer'; +import { EventType } from '../entity/event/event'; /** * Returns whether the given object is a number @@ -142,6 +144,36 @@ export function asUserType(input: any): UserType | undefined { return state; } +/** + * Converts the input to a shift availability + * @param input - The input which should be converted. + * @returns The parsed shift Availability. + * @throws TypeError - If the input is not a valid Availability + */ +export function asShiftAvailability(input: any): Availability | undefined { + if (!input) return undefined; + const state: Availability = Availability[input as keyof typeof Availability]; + if (state === undefined) { + throw new TypeError(`Input '${input}' is not a valid shift Availability.`); + } + return state; +} + +/** + * Converts the input to an EventType + * @param input - The input which should be converted. + * @returns The parsed EventType. + * @throws TypeError - If the input is not a valid EventType + */ +export function asEventType(input: any): EventType | undefined { + if (!input) return undefined; + const state: EventType = EventType[input as keyof typeof EventType]; + if (state === undefined) { + throw new TypeError(`Input '${input}' is not a valid EventType.`); + } + return state; +} + /** * Converts the input to a list of UserTypes * @param input @@ -152,3 +184,13 @@ export function asArrayOfUserTypes(input: any): UserType[] | undefined { if (!Array.isArray(input)) return undefined; return input.map((i) => asUserType(i)); } + +/** + * Converts the input to a list of numbers + * @param input + */ +export function asArrayOfNumbers(input: any): number[] | undefined { + if (!input) return undefined; + if (!Array.isArray(input)) return undefined; + return input.map((i) => asNumber(i)); +} diff --git a/src/index.ts b/src/index.ts index db9497657..9c64999a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ import { config } from 'dotenv'; import express from 'express'; import log4js, { Logger } from 'log4js'; import { Connection } from 'typeorm'; +import cron from 'node-cron'; import fileUpload from 'express-fileupload'; import Database from './database/database'; import Swagger from './start/swagger'; @@ -42,6 +43,7 @@ import ProductController from './controller/product-controller'; import ProductCategoryController from './controller/product-category-controller'; import TransactionController from './controller/transaction-controller'; import BorrelkaartGroupController from './controller/borrelkaart-group-controller'; +import BalanceService from './service/balance-service'; import BalanceController from './controller/balance-controller'; import RbacController from './controller/rbac-controller'; import GewisAuthenticationController from './gewis/controller/gewis-authentication-controller'; @@ -56,10 +58,14 @@ import { extractRawBody } from './helpers/raw-body'; import InvoiceController from './controller/invoice-controller'; import PayoutRequestController from './controller/payout-request-controller'; import RootController from './controller/root-controller'; +import ADService from './service/ad-service'; import VatGroupController from './controller/vat-group-controller'; import TestController from './controller/test-controller'; import AuthenticationSecureController from './controller/authentication-secure-controller'; import DebtorController from './controller/debtor-controller'; +import EventController from './controller/event-controller'; +import EventShiftController from './controller/event-shift-controller'; +import EventService from './service/event-service'; export class Application { app: express.Express; @@ -74,9 +80,12 @@ export class Application { logger: Logger; + tasks: cron.ScheduledTask[]; + public async stop(): Promise { this.logger.info('Stopping application instance...'); await util.promisify(this.server.close).bind(this.server)(); + this.tasks.forEach((task) => task.stop()); await this.connection.close(); this.logger.info('Application stopped.'); } @@ -171,7 +180,7 @@ export default async function createApp(): Promise { application.connection = await Database.initialize(); // Silent in-dependency logs unless really wanted by the environment. - const logger = log4js.getLogger('Console (index)'); + const logger = log4js.getLogger('Console'); logger.level = process.env.LOG_LEVEL; console.log = (message: any, ...additional: any[]) => logger.debug(message, ...additional); @@ -218,6 +227,45 @@ export default async function createApp(): Promise { // Setup token handler and authentication controller. await setupAuthentication(tokenHandler, application); + await BalanceService.updateBalances({}); + const syncBalances = cron.schedule('41 1 * * *', () => { + logger.debug('Syncing balances.'); + BalanceService.updateBalances({}).then(() => { + logger.debug('Synced balances.'); + }).catch((error => { + logger.error('Could not sync balances.', error); + })); + }); + const syncEventShiftAnswers = cron.schedule('39 2 * * *', () => { + logger.debug('Syncing event shift answers.'); + EventService.syncAllEventShiftAnswers() + .then(() => logger.debug('Synced event shift answers.')) + .catch((error) => logger.error('Could not sync event shift answers.', error)); + }); + const sendEventPlanningReminders = cron.schedule('39 13 * * *', () => { + logger.debug('Send event planning reminder emails.'); + EventService.sendEventPlanningReminders() + .then(() => logger.debug('Sent event planning reminder emails.')) + .catch((error) => logger.error('Could not send event planning reminder emails.', error)); + }); + + application.tasks = [syncBalances, syncEventShiftAnswers, sendEventPlanningReminders]; + + if (process.env.ENABLE_LDAP === 'true') { + await ADService.syncUsers(); + await ADService.syncSharedAccounts().then( + () => ADService.syncUserRoles(application.roleManager), + ); + const syncADGroups = cron.schedule('*/10 * * * *', async () => { + logger.debug('Syncing AD.'); + await ADService.syncSharedAccounts().then( + () => ADService.syncUserRoles(application.roleManager), + ); + logger.debug('Synced AD'); + }); + application.tasks.push(syncADGroups); + } + // REMOVE LATER const options: BaseControllerOptions = { specification: application.specification, @@ -227,6 +275,8 @@ export default async function createApp(): Promise { application.app.use('/v1/balances', new BalanceController(options).getRouter()); application.app.use('/v1/banners', new BannerController(options).getRouter()); application.app.use('/v1/users', new UserController(options, tokenHandler).getRouter()); + application.app.use('/v1/events', new EventController(options).getRouter()); + application.app.use('/v1/eventshifts', new EventShiftController(options).getRouter()); application.app.use('/v1/vatgroups', new VatGroupController(options).getRouter()); application.app.use('/v1/products', new ProductController(options).getRouter()); application.app.use('/v1/productcategories', new ProductCategoryController(options).getRouter()); diff --git a/src/mailer/templates/forgot-event-planning.ts b/src/mailer/templates/forgot-event-planning.ts new file mode 100644 index 000000000..dad008e10 --- /dev/null +++ b/src/mailer/templates/forgot-event-planning.ts @@ -0,0 +1,69 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import MailTemplate, { Language, MailLanguageMap } from './mail-template'; +import MailContent from './mail-content'; + +export interface ForgotEventPlanningOptions { + name: string; + eventName: string; +} + +const forgotEventPlanningEnglish = new MailContent({ + getHTML: (context) => `

Dear ${context.name},

+

What is this? Have you not yet filled in the borrel planning for ${context.eventName}? Shame on you!
+Go quickly to SudoSOS to fix your mistakes!

+ +

Hugs,
+The SudoSOS borrel planning robot

`, + getText: (context) => `Dear ${context.name}, + +What is this? Have you not yet filled in the borrel planning for ${context.eventName}? Shame on you! +Go quickly to SudoSOS to fix your mistakes! + +Hugs, +The SudoSOS borrel planning robot`, + getSubject: ({ eventName }) => `Borrel planning ${eventName}`, +}); + +const forgotEventPlanningDutch = new MailContent({ + getHTML: (context) => `

Beste ${context.name},

+

Wat is dit nou? Heb je het borrelrooster voor ${context.eventName} nog niet ingevuld? Foei!
+Ga snel naar SudoSOS om je fouten recht te zetten!

+ +

Kusjes,
+De SudoSOS borrelrooster invulrobot

`, + getText: (context) => `Beste ${context.name}, + +Wat is dit nou? Heb je het borrelrooster voor ${context.eventName} nog niet ingevuld? Foei! +Ga snel naar SudoSOS om je fouten recht te zetten! + +Kusjes, +De SudoSOS borrelrooster invulrobot`, + getSubject: ({ eventName }) => `Borrelrooster ${eventName}`, +}); + +const mailContents: MailLanguageMap = { + [Language.DUTCH]: forgotEventPlanningDutch, + [Language.ENGLISH]: forgotEventPlanningEnglish, +}; + +export default class ForgotEventPlanning extends MailTemplate { + public constructor(options: ForgotEventPlanningOptions) { + super(options, mailContents); + } +} diff --git a/src/service/event-service.ts b/src/service/event-service.ts new file mode 100644 index 000000000..8568bba29 --- /dev/null +++ b/src/service/event-service.ts @@ -0,0 +1,507 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Between, createQueryBuilder, FindManyOptions, In, IsNull, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { + BaseEventAnswerResponse, + BaseEventResponse, + BaseEventShiftResponse, + EventInShiftResponse, EventPlanningSelectedCount, + EventResponse, + EventShiftResponse, + PaginatedBaseEventResponse, + PaginatedEventShiftResponse, +} from '../controller/response/event-response'; +import { + EventAnswerAssignmentRequest, + EventAnswerAvailabilityRequest, + EventShiftRequest, +} from '../controller/request/event-request'; +import Event, { EventType } from '../entity/event/event'; +import EventShift from '../entity/event/event-shift'; +import EventShiftAnswer from '../entity/event/event-shift-answer'; +import User from '../entity/user/user'; +import { parseUserToBaseResponse } from '../helpers/revision-to-response'; +import QueryFilter, { FilterMapping } from '../helpers/query-filter'; +import { RequestWithToken } from '../middleware/token-middleware'; +import { asArrayOfNumbers, asDate, asEventType, asNumber } from '../helpers/validators'; +import { PaginationParameters } from '../helpers/pagination'; +import AssignedRole from '../entity/roles/assigned-role'; +import Mailer from '../mailer'; +import ForgotEventPlanning from '../mailer/templates/forgot-event-planning'; +import { Language } from '../mailer/templates/mail-template'; + +export interface EventFilterParameters { + name?: string; + id?: number; + createdById?: number; + beforeDate?: Date; + afterDate?: Date; + type?: EventType; +} + +export interface UpdateEventParams { + name: string, + startDate: Date, + endDate: Date, + shiftIds: number[], + type: EventType, +} + +export interface CreateEventParams extends UpdateEventParams { + createdById: number, +} + +export interface UpdateEventAnswerParams extends EventAnswerAssignmentRequest, EventAnswerAvailabilityRequest {} + +export interface ShiftSelectedCountParams { + eventType?: EventType; + afterDate?: Date; + beforeDate?: Date; +} + +export function parseEventFilterParameters( + req: RequestWithToken, +): EventFilterParameters { + return { + name: req.query.name?.toString(), + id: asNumber(req.query.eventId), + createdById: asNumber(req.query.createdById), + beforeDate: asDate(req.query.beforeDate), + afterDate: asDate(req.query.afterDate), + type: asEventType(req.query.type), + }; +} + +/** + * Parse the body of a request to an UpdateEventParams object + * @param req + * @param partial - Whether all attributes are required or not + * @param id + * @throws Error - validation failed + */ +export async function parseUpdateEventRequestParameters( + req: RequestWithToken, partial = false, id?: number, +): Promise { + const params: UpdateEventParams = { + name: req.body.name !== undefined ? req.body.name.toString() : undefined, + startDate: asDate(req.body.startDate), + endDate: asDate(req.body.endDate), + shiftIds: asArrayOfNumbers(req.body.shiftIds), + type: asEventType(req.body.type), + }; + if (!partial && (!params.name || !params.startDate || !params.endDate)) throw new Error('Not all attributes are defined.'); + if (req.body.shiftIds !== undefined && (params.shiftIds === undefined || params.shiftIds.length === 0)) throw new Error('No shifts provided.'); + + if (params.name === '') throw new Error('Invalid name.'); + if (params.startDate && params.startDate.getTime() < new Date().getTime()) throw new Error('EndDate is in the past.'); + if (params.endDate && params.endDate.getTime() < new Date().getTime()) throw new Error('StartDate is in the past.'); + if (params.startDate && params.endDate && params.endDate.getTime() < params.startDate.getTime()) throw new Error('EndDate is before startDate.'); + if (!params.startDate && params.endDate !== undefined && id !== undefined) { + const event = await Event.findOne({ where: { id } }); + if (event.startDate.getTime() > params.endDate.getTime()) throw new Error('EndDate is before existing startDate.'); + } + + if (params.shiftIds !== undefined) { + const shifts = await EventShift.find({ where: { id: In(params.shiftIds) } }); + if (shifts.length !== params.shiftIds.length) throw new Error('Not all given shifts exist.'); + + // Check that every shift has at least 1 person to do the shift + // First, get an array with tuples. The first item is the ID, the second whether the shift has any users. + const shiftsWithUsers = await Promise.all(shifts.map(async (s) => { + const roles = await AssignedRole.find({ where: { role: In(s.roles) }, relations: ['user'] }); + return [s.id, roles.length > 0]; + })); + // Then, apply a filter to only get the shifts without users + const shiftsWithoutUsers = shiftsWithUsers.filter((s) => s[1] === false); + // If there is more than one, return an error. + if (shiftsWithoutUsers.length > 0) { + throw new Error(`Shift with ID ${shiftsWithUsers.map((s) => s[0]).join(', ')} has no users. Make sure the shift's roles are correct.`); + } + } + return params; +} + +/** + * Wrapper for all Borrel-schema related logic. + */ +export default class EventService { + private static asBaseEventResponse(entity: Event): BaseEventResponse { + return { + id: entity.id, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), + version: entity.version, + createdBy: parseUserToBaseResponse(entity.createdBy, false), + name: entity.name, + endDate: entity.endDate.toISOString(), + startDate: entity.startDate.toISOString(), + type: entity.type, + }; + } + + private static asEventResponse(entity: Event): EventResponse { + return { + ...this.asBaseEventResponse(entity), + shifts: entity.shifts.map((s) => this.asShiftInEventResponse(entity, s, entity.answers)), + }; + } + + private static asBaseEventShiftResponse(entity: EventShift): + BaseEventShiftResponse { + return { + createdAt: entity.createdAt.toISOString(), + id: entity.id, + name: entity.name, + updatedAt: entity.updatedAt.toISOString(), + version: entity.version, + }; + } + + private static asEventShiftResponse(entity: EventShift): + EventShiftResponse { + return { + ...this.asBaseEventShiftResponse(entity), + roles: entity.roles, + }; + } + + private static asShiftInEventResponse( + event: Event, shift: EventShift, answers: EventShiftAnswer[], + ): EventInShiftResponse { + return { + ...this.asEventShiftResponse(shift), + answers: answers + .filter((a) => a.eventId === event.id && a.shiftId === shift.id) + .map((a) => this.asBaseEventAnswerResponse(a)), + }; + } + + private static asBaseEventAnswerResponse(entity: EventShiftAnswer): BaseEventAnswerResponse { + return { + availability: entity.availability, + selected: entity.selected, + user: parseUserToBaseResponse(entity.user, false), + }; + } + + /** + * Get all borrel schemas. + */ + public static async getEvents(params: EventFilterParameters = {}, { take, skip }: PaginationParameters = {}) + :Promise { + const filterMapping: FilterMapping = { + name: '%name', + id: 'id', + createdById: 'createdBy.id', + type: 'type', + }; + + const options: FindManyOptions = { + where: QueryFilter.createFilterWhereClause(filterMapping, params), + relations: ['createdBy'], + order: { startDate: 'desc' }, + }; + + if (params.beforeDate) { + options.where = { + ...options.where, + startDate: LessThanOrEqual(params.beforeDate), + }; + } + if (params.afterDate) { + options.where = { + ...options.where, + startDate: MoreThanOrEqual(params.afterDate), + }; + } + if (params.beforeDate && params.afterDate) { + options.where = { + ...options.where, + startDate: Between(params.afterDate, params.beforeDate), + }; + } + + const events = await Event.find({ ...options, take, skip }); + const count = await Event.count(options); + return { + _pagination: { take, skip, count }, + records: events.map((e) => this.asBaseEventResponse(e)), + }; + } + + /** + * Get a single event with its corresponding shifts and answers + * @param id + */ + public static async getSingleEvent(id: number): Promise { + const event = await Event.findOne({ + where: { id }, + relations: ['createdBy', 'shifts', 'answers', 'answers.user'], + withDeleted: true, + }); + + return event ? this.asEventResponse(event) : undefined; + } + + /** + * Synchronize all answer sheets with the corresponding users and shifts + */ + public static async syncAllEventShiftAnswers() { + const events = await Event.find({ where: { startDate: MoreThanOrEqual(new Date()) }, relations: ['answers', 'shifts'] }); + await Promise.all(events.map((e) => this.syncEventShiftAnswers(e))); + } + + /** + * Create and/or remove answer sheets given an event and a list of shifts that + * should belong to this event. If a shift is changed or a user loses a role + * that belongs to a shift, their answer sheet is removed from the database. + * @param event + * @param shiftIds + */ + public static async syncEventShiftAnswers(event: Event, shiftIds?: number[]) { + if (!shiftIds) { + shiftIds = event.shifts.map((s) => s.id); + } + + const shifts = await EventShift.find({ where: { id: In(shiftIds) } }); + if (shifts.length != shiftIds.length) throw new Error('Invalid list of shiftIds provided.'); + + // Get the answer sheet for every user that can do a shift + // Create it if it does not exist + const answers = (await Promise.all(shifts.map(async (shift) => { + const users = await User.find({ where: { roles: { role: In(shift.roles) } } }); + return Promise.all(users.map(async (user) => { + // Find the answer sheet in the database + const dbAnswer = await EventShiftAnswer.findOne({ + where: { user: { id: user.id }, event: { id: event.id }, shift: { id: shift.id } }, + relations: ['shift', 'user'], + }); + // Return it if it exists. Otherwise create a new one + if (dbAnswer != null) return dbAnswer; + const newAnswer = Object.assign(new EventShiftAnswer(), { + user, + shift, + event, + }); + return (await newAnswer.save()) as any as Promise; + })); + }))).flat(); + + const answersToRemove = (event.answers ?? []) + .filter((a1) => answers + .findIndex((a2) => a1.userId === a2.userId && a1.eventId === a2.eventId && a1.shiftId == a2.shiftId) === -1); + + await EventShiftAnswer.remove(answersToRemove); + + return answers; + } + + /** + * Create a new event. + */ + public static async createEvent(params: CreateEventParams) + : Promise { + const createdBy = await User.findOne({ where: { id: params.createdById } }); + const shifts = await EventShift.find({ where: { id: In(params.shiftIds) } }); + + if (shifts.length !== params.shiftIds.length) throw new Error('Invalid list of shiftIds provided.'); + + const event: Event = Object.assign(new Event(), { + name: params.name, + createdBy, + startDate: params.startDate, + endDate: params.endDate, + type: params.type, + shifts, + }); + await Event.save(event); + + event.answers = await this.syncEventShiftAnswers(event, params.shiftIds); + return this.asEventResponse(event); + } + + /** + * Update an existing event. + */ + public static async updateEvent(id: number, params: Partial) { + const event = await Event.findOne({ + where: { id }, + }); + if (!event) return undefined; + + const { shiftIds, ...rest } = params; + await Event.update(id, rest); + await event.reload(); + + if (params.shiftIds != null) { + const shifts = await EventShift.find({ where: { id: In(params.shiftIds) } }); + if (shifts.length !== params.shiftIds.length) throw new Error('Invalid list of shiftIds provided.'); + event.shifts = shifts; + await Event.save(event); + + await this.syncEventShiftAnswers(event, params.shiftIds); + } + + return this.getSingleEvent(id); + } + + /** + * Delete borrel schema. + */ + public static async deleteEvent(id: number): Promise { + // check if event exists in database + const event = await Event.findOne({ where: { id } }); + + // return undefined if not found + if (!event) { + return; + } + await Event.remove(event); + } + + /** + * Get all event shifts + */ + public static async getEventShifts({ take, skip }: PaginationParameters): Promise { + const shifts = await EventShift.find({ take, skip }); + const count = await EventShift.count(); + return { + _pagination: { take, skip, count }, + records: shifts.map((s) => this.asEventShiftResponse(s)), + }; + } + + /** + * Delete an event shift. Soft remove it if it has at least one corresponding answer + */ + public static async deleteEventShift(id: number): Promise { + const shift = await EventShift.findOne({ + where: { id }, + withDeleted: true, + }); + if (shift == null) return; + + const answers = await EventShiftAnswer.find({ where: { shiftId: shift.id } }); + + if (answers.length === 0) { + await shift.remove(); + } else { + await shift.softRemove(); + } + } + + /** + * Create borrel schema shift. + */ + public static async createEventShift(eventShiftRequest + : EventShiftRequest): Promise { + const newEventShift: EventShift = Object.assign(new EventShift(), { + name: eventShiftRequest.name, + roles: eventShiftRequest.roles, + }); + await EventShift.save(newEventShift); + return this.asEventShiftResponse(newEventShift); + } + + /** + * Update borrel schema shift. + */ + public static async updateEventShift(id: number, update: Partial) { + const shift = await EventShift.findOne({ where: { id } }); + if (!shift) return undefined; + if (update.name) shift.name = update.name; + if (update.roles) shift.roles = update.roles; + await EventShift.save(shift); + return this.asEventShiftResponse(shift); + } + + /** + * Update borrel schema answer + */ + public static async updateEventShiftAnswer( + eventId: number, shiftId: number, userId: number, update: Partial, + ) { + const answer = await EventShiftAnswer.findOne({ where: { + userId, shiftId, eventId, + } }); + if (!answer) return undefined; + + if (update.availability !== undefined) answer.availability = update.availability; + if (update.selected !== undefined) answer.selected = update.selected; + + await answer.save(); + return this.asBaseEventAnswerResponse(answer); + } + + /** + * Send a reminder + * @param date + */ + public static async sendEventPlanningReminders(date = new Date()) { + const inTwoDays = new Date(date.getTime() + 1000 * 3600 * 24 * 2); + const inThreeDays = new Date(date.getTime() + 1000 * 3600 * 24 * 3); + const events = await EventService.getEvents({ beforeDate: inThreeDays, afterDate: inTwoDays }); + + await Promise.all(events.records.map(async (event) => { + const answers = await EventShiftAnswer.find({ where: { eventId: event.id, availability: IsNull() }, relations: ['user'] }); + // Get all users and remove duplicates + const users = answers + .map((a) => a.user) + .filter((user, i, all) => i === all.findIndex((u) => u.id === user.id)); + + // Send every user an email + return Promise.all(users.map(async (user) => Mailer.getInstance().send(user, + new ForgotEventPlanning({ name: user.firstName, eventName: event.name }), Language.DUTCH), + )); + })); + } + + /** + * Get amount of times a user was selected for the given shift in the given time interval + * @param shiftId + * @param eventType + * @param afterDate + * @param beforeDate + */ + public static async getShiftSelectedCount( + shiftId: number, { eventType, afterDate, beforeDate }: ShiftSelectedCountParams = {}, + ): Promise { + let query = createQueryBuilder() + .select('user.id', 'id') + .addSelect('count(event.id)', 'count') + .addSelect('max(user.firstName)', 'firstName') + .addSelect('max(user.lastName)', 'lastName') + .addSelect('max(user.nickname)', 'nickname') + .from(EventShiftAnswer, 'answer') + .innerJoin(Event, 'event', 'event.id = answer.eventId') + .innerJoin(User, 'user', 'user.id = answer.userId') + .where('answer.shiftId = :shiftId', { shiftId }) + .andWhere('answer.selected = 1') + .groupBy('user.id'); + if (eventType) query = query.andWhere('event.type = :eventType', { eventType }); + if (afterDate) query = query.andWhere('event.startDate >= :afterDate', { afterDate }); + if (beforeDate) query = query.andWhere('event.startDate <= :afterDate', { beforeDate }); + + const result = await query.getRawMany(); + + return result.map((r: any) => ({ + ...parseUserToBaseResponse(r, false), + count: r.count, + })); + } +} diff --git a/test/seed.ts b/test/seed.ts index 1d6700b8c..e3f8d8970 100644 --- a/test/seed.ts +++ b/test/seed.ts @@ -45,7 +45,9 @@ import InvoiceUser from '../src/entity/user/invoice-user'; import Invoice from '../src/entity/invoices/invoice'; import InvoiceEntry from '../src/entity/invoices/invoice-entry'; import InvoiceStatus, { InvoiceState } from '../src/entity/invoices/invoice-status'; -import GewisUser from '../src/gewis/entity/gewis-user'; +import Event, { EventType } from '../src/entity/event/event'; +import EventShift from '../src/entity/event/event-shift'; +import EventShiftAnswer, { Availability } from '../src/entity/event/event-shift-answer'; import seedGEWISUsers from '../src/gewis/database/seed'; import PinAuthenticator from '../src/entity/authenticator/pin-authenticator'; import VatGroup from '../src/entity/vat-group'; @@ -56,6 +58,8 @@ import UserFineGroup from '../src/entity/fine/userFineGroup'; import FineHandoutEvent from '../src/entity/fine/fineHandoutEvent'; import Fine from '../src/entity/fine/fine'; import { calculateBalance } from './helpers/balance'; +import GewisUser from '../src/gewis/entity/gewis-user'; +import AssignedRole from '../src/entity/roles/assigned-role'; function getDate(startDate: Date, endDate: Date, i: number): Date { const diff = endDate.getTime() - startDate.getTime(); @@ -165,6 +169,23 @@ export async function seedUsers(): Promise { return users; } +/** + * Seed some roles, where every user has at most one role. + * @param users + */ +export async function seedRoles(users: User[]): Promise { + const roleStrings = ['BAC', 'BAC feut', 'BAC PM', 'Bestuur', 'Kasco']; + return (await Promise.all(users.map(async (user, i) => { + if (i % 3 === 0) return undefined; + + const role = Object.assign(new AssignedRole(), { + user, + role: roleStrings[i % 5], + }); + return AssignedRole.save(role); + }))).filter((r) => r != null); +} + export function defineInvoiceEntries(invoiceId: number, startEntryId: number, transactions: Transaction[]): { invoiceEntries: InvoiceEntry[], cost: number } { const invoiceEntries: InvoiceEntry[] = []; @@ -256,6 +277,99 @@ export async function seedInvoices(users: User[], transactions: Transaction[]): return { invoices, invoiceTransfers }; } +/** + * Seeds a default dataset of borrelSchemaShifts and stores them in the database + */ +export async function seedEventShifts() { + const shifts: EventShift[] = []; + shifts.push(Object.assign(new EventShift(), { + name: 'Borrelen', + roles: ['BAC', 'BAC feut'], + })); + shifts.push(Object.assign(new EventShift(), { + name: 'Portier', + roles: ['BAC', 'BAC feut'], + })); + shifts.push(Object.assign(new EventShift(), { + name: 'Bier halen voor Job en Sjoerd', + roles: ['BAC feut'], + })); + shifts.push(Object.assign(new EventShift(), { + name: 'Roy slaan', + roles: [], + })); + shifts.push(Object.assign(new EventShift(), { + name: '900 euro kwijtraken', + roles: ['BAC PM', 'BAC'], + })); + shifts.push(Object.assign(new EventShift(), { + name: 'Wassen', + roles: ['Bestuur'], + deletedAt: new Date(), + })); + await EventShift.save(shifts); + return shifts; +} + +export async function createEventShiftAnswer(user: User, event: Event, shift: EventShift, type: number) { + const availabilities = [Availability.YES, Availability.MAYBE, Availability.NO, Availability.LATER, Availability.NA, null]; + + const answer: EventShiftAnswer = Object.assign(new EventShiftAnswer(), { + user, + availability: availabilities[type + 1 % availabilities.length], + selected: false, + eventId: event.id, + shiftId: shift.id, + }); + return EventShiftAnswer.save(answer); +} + +export async function seedEvents(rolesWithUsers: AssignedRole[]) { + const events: Event[] = []; + const eventShifts = await seedEventShifts(); + const eventShiftAnswers: EventShiftAnswer[] = []; + for (let i = 0; i < 5; i += 1) { + // const startDate = getRandomDate(new Date(), new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365)); + const startDate = new Date(new Date().getTime() + ((i * 1000000) % (3600 * 24 * 365)) * 1000 + 60000); + // Add 2,5 hours + const endDate = new Date(startDate.getTime() + (1000 * 60 * 60 * 2.5)); + + const event = Object.assign(new Event(), { + name: `${i}-Testborrel-${i}`, + createdBy: rolesWithUsers[i].user, + startDate, + endDate, + type: EventType.BORREL, + shifts: [], + id: i, + }); + await Event.save(event); + + const eventShifts1: EventShift[] = []; + const eventShiftAnswers1: EventShiftAnswer[] = []; + for (let j = 0; j < ((i + 1) * 243) % 4; j += 1) { + const shift = eventShifts[((i + j) * 13) % (eventShifts.length)]; + const users = rolesWithUsers.filter((r) => shift.roles.includes(r.role)); + await Promise.all(users.map(async (r, k) => { + const answer = await createEventShiftAnswer(r.user, event, shift, k); + answer.event = event; + answer.shift = shift; + eventShifts1.push(shift); + eventShiftAnswers.push(answer); + eventShiftAnswers1.push(answer); + })); + } + + event.shifts = eventShifts1.filter((s, j, all) => j === all.findIndex((s2) => s.id === s2.id)); + await event.save(); + + event.answers = eventShiftAnswers1; + events.push(event); + } + + return { events, eventShifts, eventShiftAnswers }; +} + /** * Seeds a default dataset of product categories, and stores them in the database. */ @@ -1220,10 +1334,14 @@ export async function seedBanners(users: User[]): Promise<{ export interface DatabaseContent { users: User[], + roles: AssignedRole[], categories: ProductCategory[], vatGroups: VatGroup[], products: Product[], productRevisions: ProductRevision[], + events: Event[], + eventShifts: EventShift[], + eventShiftAnswers: EventShiftAnswer[], containers: Container[], containerRevisions: ContainerRevision[], pointsOfSale: PointOfSale[], @@ -1257,6 +1375,8 @@ export default async function seedDatabase(): Promise { const { pointsOfSale, pointOfSaleRevisions } = await seedPointsOfSale( users, containerRevisions, ); + const roles = await seedRoles(users); + const { events, eventShifts, eventShiftAnswers } = await seedEvents(roles); const { transactions } = await seedTransactions(users, pointOfSaleRevisions); const transfers = await seedTransfers(users); const { fines, fineTransfers, userFineGroups } = await seedFines(users, transactions, transfers); @@ -1267,6 +1387,7 @@ export default async function seedDatabase(): Promise { return { users, + roles, categories, vatGroups, products, @@ -1286,5 +1407,8 @@ export default async function seedDatabase(): Promise { gewisUsers, pinUsers, localUsers, + events, + eventShifts, + eventShiftAnswers, }; } diff --git a/test/unit/controller/event-controller.ts b/test/unit/controller/event-controller.ts new file mode 100644 index 000000000..312d0d34a --- /dev/null +++ b/test/unit/controller/event-controller.ts @@ -0,0 +1,1062 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import express, { Application } from 'express'; +import { Connection } from 'typeorm'; +import { SwaggerSpecification } from 'swagger-model-validator'; +import EventController from '../../../src/controller/event-controller'; +import User, { TermsOfServiceStatus, UserType } from '../../../src/entity/user/user'; +import Event, { EventType } from '../../../src/entity/event/event'; +import EventShift from '../../../src/entity/event/event-shift'; +import EventShiftAnswer, { Availability } from '../../../src/entity/event/event-shift-answer'; +import AssignedRole from '../../../src/entity/roles/assigned-role'; +import Database from '../../../src/database/database'; +import { seedEvents, seedRoles, seedUsers } from '../../seed'; +import TokenHandler from '../../../src/authentication/token-handler'; +import Swagger from '../../../src/start/swagger'; +import { json } from 'body-parser'; +import fileUpload from 'express-fileupload'; +import TokenMiddleware from '../../../src/middleware/token-middleware'; +import RoleManager from '../../../src/rbac/role-manager'; +import { expect, request } from 'chai'; +import { + BaseEventAnswerResponse, + BaseEventResponse, + EventResponse, +} from '../../../src/controller/response/event-response'; +import EventService from '../../../src/service/event-service'; +import { EventRequest } from '../../../src/controller/request/event-request'; + +describe('EventController', () => { + let ctx: { + connection: Connection, + app: Application + specification: SwaggerSpecification, + controller: EventController, + adminUser: User, + localUser: User, + adminToken: string; + userToken: string; + users: User[], + events: Event[], + eventShifts: EventShift[], + eventShiftAnswers: EventShiftAnswer[], + roles: AssignedRole[], + }; + + before(async () => { + const connection = await Database.initialize(); + + // create dummy users + const adminUser = { + id: 1, + firstName: 'Admin', + type: UserType.LOCAL_ADMIN, + active: true, + acceptedToS: TermsOfServiceStatus.ACCEPTED, + } as User; + + const localUser = { + id: 2, + firstName: 'User', + type: UserType.LOCAL_USER, + active: true, + acceptedToS: TermsOfServiceStatus.ACCEPTED, + } as User; + + await User.save(adminUser); + await User.save(localUser); + + const users = await seedUsers(); + const roles = await seedRoles(users); + const { events, eventShifts, eventShiftAnswers } = await seedEvents(roles); + + // create bearer tokens + const tokenHandler = new TokenHandler({ + algorithm: 'HS256', publicKey: 'test', privateKey: 'test', expiry: 3600, + }); + const adminToken = await tokenHandler.signToken({ user: adminUser, roles: ['Admin'], lesser: false }, 'nonce admin'); + const userToken = await tokenHandler.signToken({ user: localUser, roles: [], lesser: false }, 'nonce'); + + // start app + const app = express(); + const specification = await Swagger.initialize(app); + + const all = { all: new Set(['*']) }; + const own = { all: new Set(['*']) }; + const roleManager = new RoleManager(); + roleManager.registerRole({ + name: 'Admin', + permissions: { + Event: { + create: all, + get: all, + update: all, + delete: all, + }, + EventAnswer: { + update: all, + assign: all, + }, + }, + assignmentCheck: async (user: User) => user.type === UserType.LOCAL_ADMIN, + }); + roleManager.registerRole({ + name: 'User', + permissions: { + Event: { + get: own, + }, + EventAnswer: { + update: own, + }, + }, + assignmentCheck: async (user: User) => user.type === UserType.LOCAL_USER, + }); + + const controller = new EventController({ specification, roleManager }); + app.use(json()); + app.use(fileUpload()); + app.use(new TokenMiddleware({ tokenHandler, refreshFactor: 0.5 }).getMiddleware()); + app.use('/events', controller.getRouter()); + + ctx = { + connection, + app, + controller, + specification, + adminUser, + localUser, + adminToken, + userToken, + users, + events, + eventShifts, + eventShiftAnswers, + roles, + }; + }); + + after(async () => { + await ctx.connection.destroy(); + }); + + describe('GET /events', () => { + it('should correctly return list of events', async () => { + const res = await request(ctx.app) + .get('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as BaseEventResponse[]; + const validation = ctx.specification.validateModel('PaginatedBaseEventResponse', res.body, false, true); + expect(validation.valid).to.be.true; + + expect(records.length).to.be.at.most(res.body._pagination.take); + }); + it('should get events based on fuzzy search on name', async () => { + const name = ctx.events[0].name.substring(0, 3); + + const res = await request(ctx.app) + .get('/events') + .query({ name }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as BaseEventResponse[]; + records.forEach((e) => { + expect(e.name).to.include(name); + }); + }); + it('should get all events when fuzzy search on empty name', async () => { + const name = ''; + + const res = await request(ctx.app) + .get('/events') + .query({ name }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as BaseEventResponse[]; + expect(records.length).to.equal(ctx.events.length); + }); + it('should get events created by user', async () => { + const createdById = ctx.events[0].createdBy.id; + + const res = await request(ctx.app) + .get('/events') + .query({ createdById }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as BaseEventResponse[]; + records.forEach((e) => { + expect(e.createdBy.id).to.equal(createdById); + }); + }); + it('should get events by type', async () => { + const type = EventType.OTHER; + + const res = await request(ctx.app) + .get('/events') + .query({ type }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as BaseEventResponse[]; + expect(records.length).to.equal(ctx.events.filter((e) => e.type === type).length); + records.forEach((e) => { + expect(e.type).to.equal(type); + }); + }); + it('should return 400 when filtering by invalid type', async () => { + const type = 'AAAAAAAAAA'; + + const res = await request(ctx.app) + .get('/events') + .query({ type }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Input \'AAAAAAAAAA\' is not a valid EventType.'); + }); + it('should get events before date', async () => { + const beforeDate = new Date('2020-07-01'); + + const res = await request(ctx.app) + .get('/events') + .query({ beforeDate }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as BaseEventResponse[]; + records.forEach((e) => { + expect(new Date(e.startDate)).to.be.at.most(beforeDate); + }); + }); + it('should get events after date', async () => { + const afterDate = new Date('2020-07-01'); + + const res = await request(ctx.app) + .get('/events') + .query({ afterDate }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as BaseEventResponse[]; + records.forEach((e) => { + expect(new Date(e.startDate)).to.be.at.least(afterDate); + }); + }); + it('should adhere to pagination', async () => { + const take = 3; + const skip = 1; + const events = await EventService.getEvents({}, { take, skip }); + expect(events.records.length).to.equal(take); + expect(events._pagination.take).to.equal(take); + expect(events._pagination.skip).to.equal(skip); + expect(events._pagination.count).to.equal(ctx.events.length); + + const ids = ctx.events + .sort((a, b) => b.startDate.getTime() - a.startDate.getTime()) + .slice(skip, skip + take) + .map((e) => e.id); + events.records.forEach((event, i) => { + expect(event.id).to.equal(ids[i]); + }); + }); + it('should return 403 if not admin', async () => { + const res = await request(ctx.app) + .get('/events') + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('GET /events/{id}', () => { + it('should correctly return single event', async () => { + const event = ctx.events[0]; + const res = await request(ctx.app) + .get(`/events/${event.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + + const validation = ctx.specification.validateModel('EventResponse', eventResponse, false, true); + expect(validation.valid).to.be.true; + }); + it('should return 404 if event does not exist', async () => { + const res = await request(ctx.app) + .get('/events/999999') + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const event = ctx.events[0]; + const res = await request(ctx.app) + .get(`/events/${event.id}`) + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('POST /events', () => { + let req: EventRequest; + + before(() => { + req = { + name: 'Vergadering', + startDate: new Date(new Date().getTime() + 1000 * 60 * 60).toISOString(), + endDate: new Date(new Date().getTime() + 1000 * 60 * 60 * 4).toISOString(), + type: EventType.EXTERNAL_BORREL, + shiftIds: ctx.eventShifts.slice(1, 3).map((s) => s.id), + }; + }); + + it('should correctly create event', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send(req); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + + const validation = ctx.specification.validateModel('EventResponse', eventResponse, false, true); + expect(validation.valid).to.be.true; + expect(eventResponse.createdBy.id).to.equal(ctx.adminUser.id); + + expect(eventResponse.shifts.map((s) => s.id)).to.deep.equalInAnyOrder(req.shiftIds); + expect(eventResponse.name).to.equal(req.name); + expect(eventResponse.startDate).to.equal(req.startDate); + expect(eventResponse.endDate).to.equal(req.endDate); + + // Cleanup + await EventShiftAnswer.delete({ eventId: eventResponse.id }); + await Event.delete(eventResponse.id); + }); + it('should return 400 if name is empty string', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + name: '', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Not all attributes are defined.'); + }); + it('should return 400 if startDate is invalid', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + startDate: 'hihaho', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal("Input 'hihaho' is not a date."); + }); + it('should return 400 if startDate is in the past', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + startDate: new Date('2023-08-25'), + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('EndDate is in the past.'); + }); + it('should return 400 if endDate is invalid', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + endDate: 'hihaho', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal("Input 'hihaho' is not a date."); + }); + it('should return 400 if endDate is in the past', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + endDate: new Date('2023-08-25'), + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('StartDate is in the past.'); + }); + it('should return 400 if endDate is before the startDate', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + startDate: new Date('2023-08-25'), + endDate: new Date('2023-08-24'), + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('EndDate is in the past.'); + }); + it('should return 400 if shiftIds is not an array', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + shiftIds: 'Ollie', + }); + expect(res.status).to.equal(400); + // Swagger validation fail + expect(res.body.valid).to.equal(false); + }); + it('should return 400 if shiftIds is an empty array', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + shiftIds: [], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('No shifts provided.'); + }); + it('should return 400 if shiftIds is an array of strings', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + shiftIds: ['Ollie'], + }); + expect(res.status).to.equal(400); + // Swagger validation fail + expect(res.body.valid).to.equal(false); + }); + it('should return 400 if shiftIds has duplicates', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + shiftIds: [1, 1], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Not all given shifts exist.'); + }); + it('should return 400 if shiftIds has ids that do not exist', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + shiftIds: [1, 99999999], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Not all given shifts exist.'); + }); + it('should return 400 when shift has no users', async () => { + const shift = ctx.eventShifts.find((s) => s.roles.length === 0); + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + shiftIds: [shift.id], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal(`Shift with ID ${shift.id} has no users. Make sure the shift's roles are correct.`); + }); + it('should return 403 if not admin', async () => { + const res = await request(ctx.app) + .post('/events') + .set('Authorization', `Bearer ${ctx.userToken}`) + .send(req); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('PUT /events/{eventId}/shift/{shiftId}/user/{userId}/assign', async () => { + it('should correctly change shift assignment', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ selected: !answer.selected }); + expect(res.status).to.equal(200); + + let answerResponse = res.body as BaseEventAnswerResponse; + + const validation = ctx.specification.validateModel('BaseEventAnswerResponse', answerResponse, false, true); + expect(validation.valid).to.be.true; + expect(answerResponse.selected).to.equal(!answer.selected); + expect(answerResponse.availability).to.equal(answer.availability); + expect((await EventShiftAnswer.findOne({ where: { eventId, shiftId, userId } })).selected).to.equal(!answer.selected); + + res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ selected: answer.selected }); + expect(res.status).to.equal(200); + + answerResponse = res.body as BaseEventAnswerResponse; + + expect(answerResponse.selected).to.equal(answer.selected); + expect(answerResponse.availability).to.equal(answer.availability); + expect((await EventShiftAnswer.findOne({ where: { eventId, shiftId, userId } })).selected).to.equal(answer.selected); + }); + it('should return 400 if selected is missing', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({}); + expect(res.status).to.equal(400); + // Swagger validation fail + expect(res.body.valid).to.equal(false); + }); + it('should return 400 if selected is not a boolean', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ selected: 'AAAAAAAA' }); + expect(res.status).to.equal(400); + }); + it('should return 400 if event has expired', async () => { + const event = ctx.events[0]; + const answer = event.answers[0]; + const { eventId, shiftId, userId } = answer; + + await Event.update(event.id, { startDate: new Date(new Date().getTime() - 1000) }); + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ selected: true }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Event has already started or is already over.'); + + // Cleanup + await Event.update(event.id, { startDate: event.startDate }); + }); + it('should return 404 if event does not exist', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${999999}/shift/${shiftId}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ selected: true }); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 404 if shift does not exist', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${9999999}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ selected: true }); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 404 if user does not exist', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${999999}/assign`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ selected: true }); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/assign`) + .set('Authorization', `Bearer ${ctx.userToken}`) + .send({ selected: true }); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('PUT /events/{eventId}/shift/{shiftId}/user/{userId}/availability', async () => { + it('should correctly change shift assignment', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + const availability = Availability.YES; + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ availability }); + expect(res.status).to.equal(200); + + let answerResponse = res.body as BaseEventAnswerResponse; + + const validation = ctx.specification.validateModel('BaseEventAnswerResponse', answerResponse, false, true); + expect(validation.valid).to.be.true; + expect(answerResponse.selected).to.equal(answer.selected); + expect(answerResponse.availability).to.equal(availability); + expect((await EventShiftAnswer.findOne({ where: { eventId, shiftId, userId } })).availability).to.equal(availability); + }); + it('should return 400 if availability is missing', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({}); + expect(res.status).to.equal(400); + // Swagger validation fail + expect(res.body.valid).to.equal(false); + }); + it('should return 400 if availability is not valid', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ availability: 'AAAAAAAA' }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Invalid event availability.'); + }); + it('should return 400 if availability is null', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ availability: null }); + expect(res.status).to.equal(400); + // Swagger validation fail + expect(res.body.valid).to.equal(false); + }); + it('should return 400 if event has expired', async () => { + const event = ctx.events[0]; + const answer = event.answers[0]; + const { eventId, shiftId, userId } = answer; + + await Event.update(event.id, { startDate: new Date(new Date().getTime() - 1000) }); + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ availability: Availability.NO }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Event has already started or is already over.'); + + // Cleanup + await Event.update(event.id, { startDate: event.startDate }); + }); + it('should return 404 if event does not exist', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${999999}/shift/${shiftId}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ availability: Availability.NO }); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 404 if shift does not exist', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${9999999}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ availability: Availability.NO }); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 404 if user does not exist', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${999999}/availability`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ availability: Availability.NO }); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const answer = ctx.eventShiftAnswers[0]; + const { eventId, shiftId, userId } = answer; + + let res = await request(ctx.app) + .put(`/events/${eventId}/shift/${shiftId}/user/${userId}/availability`) + .set('Authorization', `Bearer ${ctx.userToken}`) + .send({ availability: Availability.NO }); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('PATCH /events/{id}', () => { + let req: EventRequest; + let originalEvent: Event; + + before(async () => { + req = { + name: 'Vergadering', + startDate: new Date(new Date().getTime() + 1000 * 60 * 60).toISOString(), + endDate: new Date(new Date().getTime() + 1000 * 60 * 60 * 4).toISOString(), + shiftIds: ctx.eventShifts.slice(1, 3).map((s) => s.id), + type: EventType.OTHER, + }; + + originalEvent = await Event.findOne({ + where: { answers: { shift: { deletedAt: null } } }, + relations: ['answers', 'answers.shift'], + }); + }); + + after(async () => { + await EventService.updateEvent(originalEvent.id, { + name: originalEvent.name, + startDate: originalEvent.startDate, + endDate: originalEvent.endDate, + shiftIds: originalEvent.answers + .map((a) => a.shiftId) + .filter((a1, i, all) => i === all + .findIndex((a2) => a2 === a1)), + }); + }); + + it('should correctly update event', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send(req); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + + const validation = ctx.specification.validateModel('EventResponse', eventResponse, false, true); + expect(validation.valid).to.be.true; + + expect(eventResponse.shifts.map((s) => s.id)).to.deep.equalInAnyOrder(req.shiftIds); + expect(eventResponse.name).to.equal(req.name); + expect(eventResponse.startDate).to.equal(req.startDate); + expect(eventResponse.endDate).to.equal(req.endDate); + }); + it('should correctly update name', async () => { + const name: string = 'Echte vergadering'; + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ name }); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + expect(eventResponse.name).to.equal(name); + }); + it('should correctly update startDate', async () => { + const startDate = new Date(new Date().getTime() + 60000); + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ startDate }); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + expect(eventResponse.startDate).to.equal(startDate.toISOString()); + + // Cleanup + await Event.update(originalEvent.id, { + startDate: originalEvent.startDate, + }); + }); + it('should correctly update endDate', async () => { + const endDate = new Date(new Date().getTime() + 120000); + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ startDate: endDate }); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + expect(eventResponse.startDate).to.equal(endDate.toISOString()); + + // Cleanup + await Event.update(originalEvent.id, { + endDate: originalEvent.endDate, + }); + }); + it('should correctly update type', async () => { + const type = EventType.EXTERNAL_BORREL; + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ type }); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + expect(eventResponse.type).to.equal(type); + }); + it('should correctly update shifts', async () => { + const shiftIds = [1, 5]; + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ shiftIds }); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + expect(eventResponse.shifts.map((s) => s.id)).to.deep.equalInAnyOrder(shiftIds); + }); + it('should return 400 if name is empty string', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + name: '', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Invalid name.'); + }); + it('should return 400 if startDate is invalid', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + startDate: 'hihaho', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal("Input 'hihaho' is not a date."); + }); + it('should return 400 if startDate is in the past', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + startDate: new Date('2023-08-25'), + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('EndDate is in the past.'); + }); + it('should return 400 if endDate is invalid', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + endDate: 'hihaho', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal("Input 'hihaho' is not a date."); + }); + it('should return 400 if endDate is in the past', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + endDate: new Date('2023-08-25'), + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('StartDate is in the past.'); + }); + it('should return 400 if endDate is before the startDate', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + startDate: new Date(new Date().getTime() + 60000), + endDate: new Date(new Date().getTime() + 30000), + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('EndDate is before startDate.'); + }); + it('should return 400 if endDate is before the existing startDate', async () => { + const startDate = new Date(new Date().getTime() + 1000 * 3600 * 24 * 2); + const endDate = new Date(new Date().getTime() + 1000 * 3600 * 24); + await Event.update(originalEvent.id, { + startDate, + }); + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + endDate, + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('EndDate is before existing startDate.'); + }); + it('should return 400 if shiftIds is not an array', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + shiftIds: 'Ollie', + }); + expect(res.status).to.equal(400); + // Swagger validation fail + expect(res.body.valid).to.equal(false); + }); + it('should return 400 if shiftIds is an empty array', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + shiftIds: [], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('No shifts provided.'); + }); + it('should return 400 if shiftIds is an array of strings', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + shiftIds: ['Ollie'], + }); + expect(res.status).to.equal(400); + // Swagger validation fail + expect(res.body.valid).to.equal(false); + }); + it('should return 400 if shiftIds has duplicates', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + shiftIds: [1, 1], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Not all given shifts exist.'); + }); + it('should return 400 if shiftIds has ids that do not exist', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + shiftIds: [1, 99999999], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Not all given shifts exist.'); + }); + it('should return 400 when shift has no users', async () => { + const shift = ctx.eventShifts.find((s) => s.roles.length === 0); + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + shiftIds: [shift.id], + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal(`Shift with ID ${shift.id} has no users. Make sure the shift's roles are correct.`); + }); + it('should return 404 if event does not exist', async () => { + const res = await request(ctx.app) + .patch('/events/999999') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send(req); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const res = await request(ctx.app) + .patch(`/events/${originalEvent.id}`) + .set('Authorization', `Bearer ${ctx.userToken}`) + .send({ name: 'yeeee' }); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('POST /events/{id}/sync', () => { + it('should correctly sync event shift answers', async () => { + const event = ctx.events.filter((e) => e.answers.length > 0 && e.answers.every((a) => a.availability != null))[0]; + const res = await request(ctx.app) + .post(`/events/${event.id}/sync`) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const eventResponse = res.body as EventResponse; + + const validation = ctx.specification.validateModel('EventResponse', eventResponse, false, true); + expect(validation.valid).to.be.true; + }); + it('should return 404 if event does not exist', async () => { + const res = await request(ctx.app) + .post('/events/999999/sync') + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const event = ctx.events[0]; + const res = await request(ctx.app) + .post(`/events/${event.id}/sync`) + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('DELETE /events/{id}', () => { + it('should correctly delete single event', async () => { + const event = ctx.events[0]; + const res = await request(ctx.app) + .delete(`/events/${event.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(204); + expect(res.body).to.be.empty; + + expect(await Event.findOne({ where: { id: event.id } })).to.be.null; + }); + it('should return 404 if event does not exist', async () => { + const res = await request(ctx.app) + .delete('/events/999999') + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const event = ctx.events[0]; + const res = await request(ctx.app) + .delete(`/events/${event.id}`) + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); +}); diff --git a/test/unit/controller/event-shift-controller.ts b/test/unit/controller/event-shift-controller.ts new file mode 100644 index 000000000..eb27f33af --- /dev/null +++ b/test/unit/controller/event-shift-controller.ts @@ -0,0 +1,521 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import express, { Application } from 'express'; +import { Connection } from 'typeorm'; +import { SwaggerSpecification } from 'swagger-model-validator'; +import User, { TermsOfServiceStatus, UserType } from '../../../src/entity/user/user'; +import EventShift from '../../../src/entity/event/event-shift'; +import EventShiftAnswer from '../../../src/entity/event/event-shift-answer'; +import AssignedRole from '../../../src/entity/roles/assigned-role'; +import Database from '../../../src/database/database'; +import { seedEvents, seedRoles, seedUsers } from '../../seed'; +import TokenHandler from '../../../src/authentication/token-handler'; +import Swagger from '../../../src/start/swagger'; +import { json } from 'body-parser'; +import fileUpload from 'express-fileupload'; +import TokenMiddleware from '../../../src/middleware/token-middleware'; +import RoleManager from '../../../src/rbac/role-manager'; +import { expect, request } from 'chai'; +import { + EventPlanningSelectedCount, + EventShiftResponse, + PaginatedEventShiftResponse, +} from '../../../src/controller/response/event-response'; +import { EventShiftRequest } from '../../../src/controller/request/event-request'; +import EventShiftController from '../../../src/controller/event-shift-controller'; +import { describe } from 'mocha'; +import Event, { EventType } from '../../../src/entity/event/event'; + +describe('EventShiftController', () => { + let ctx: { + connection: Connection, + app: Application + specification: SwaggerSpecification, + controller: EventShiftController, + adminUser: User, + localUser: User, + adminToken: string; + userToken: string; + users: User[], + events: Event[], + eventShifts: EventShift[], + eventShiftAnswers: EventShiftAnswer[], + roles: AssignedRole[], + }; + + before(async () => { + const connection = await Database.initialize(); + + // create dummy users + const adminUser = { + id: 1, + firstName: 'Admin', + type: UserType.LOCAL_ADMIN, + active: true, + acceptedToS: TermsOfServiceStatus.ACCEPTED, + } as User; + + const localUser = { + id: 2, + firstName: 'User', + type: UserType.LOCAL_USER, + active: true, + acceptedToS: TermsOfServiceStatus.ACCEPTED, + } as User; + + await User.save(adminUser); + await User.save(localUser); + + const users = await seedUsers(); + const roles = await seedRoles(users); + const { events, eventShifts, eventShiftAnswers } = await seedEvents(roles); + + // create bearer tokens + const tokenHandler = new TokenHandler({ + algorithm: 'HS256', publicKey: 'test', privateKey: 'test', expiry: 3600, + }); + const adminToken = await tokenHandler.signToken({ user: adminUser, roles: ['Admin'], lesser: false }, 'nonce admin'); + const userToken = await tokenHandler.signToken({ user: localUser, roles: [], lesser: false }, 'nonce'); + + // start app + const app = express(); + const specification = await Swagger.initialize(app); + + const all = { all: new Set(['*']) }; + const own = { all: new Set(['*']) }; + const roleManager = new RoleManager(); + roleManager.registerRole({ + name: 'Admin', + permissions: { + Event: { + create: all, + get: all, + update: all, + delete: all, + }, + EventAnswer: { + update: all, + }, + }, + assignmentCheck: async (user: User) => user.type === UserType.LOCAL_ADMIN, + }); + roleManager.registerRole({ + name: 'User', + permissions: { + Event: { + get: own, + }, + EventAnswer: { + update: own, + }, + }, + assignmentCheck: async (user: User) => user.type === UserType.LOCAL_USER, + }); + + const controller = new EventShiftController({ specification, roleManager }); + app.use(json()); + app.use(fileUpload()); + app.use(new TokenMiddleware({ tokenHandler, refreshFactor: 0.5 }).getMiddleware()); + app.use('/eventshifts', controller.getRouter()); + + ctx = { + connection, + app, + controller, + specification, + adminUser, + localUser, + adminToken, + userToken, + users, + events, + eventShifts, + eventShiftAnswers, + roles, + }; + }); + + after(async () => { + await ctx.connection.destroy(); + }); + + describe('GET /eventshifts', () => { + it('should correctly return list of events', async () => { + const res = await request(ctx.app) + .get('/eventshifts') + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const records = res.body.records as EventShiftResponse[]; + const validation = ctx.specification.validateModel('PaginatedEventShiftResponse', res.body, false, true); + expect(validation.valid).to.be.true; + + expect(records.length).to.be.at.most(res.body._pagination.take); + }); + it('should adhere to pagination', async () => { + const take = 3; + const skip = 1; + const response = await request(ctx.app) + .get('/eventshifts') + .query({ take, skip }) + .set('Authorization', `Bearer ${ctx.adminToken}`); + + const shifts = response.body as PaginatedEventShiftResponse; + expect(shifts.records.length).to.equal(take); + expect(shifts._pagination.take).to.equal(take); + expect(shifts._pagination.skip).to.equal(skip); + expect(shifts._pagination.count).to + .equal(ctx.eventShifts.filter((s) => s.deletedAt == null).length); + }); + it('should return 403 if not admin', async () => { + const res = await request(ctx.app) + .get('/eventshifts') + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('POST /eventshifts', () => { + let req: EventShiftRequest; + + before(() => { + req = { + name: 'Zitten', + roles: ['BAC', 'Bestuur'], + }; + }); + + it('should correctly create shift', async () => { + const res = await request(ctx.app) + .post('/eventshifts') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send(req); + expect(res.status).to.equal(200); + + const shiftResponse = res.body as EventShiftResponse; + + const validation = ctx.specification.validateModel('EventShiftResponse', shiftResponse, false, true); + expect(validation.valid).to.be.true; + + expect(shiftResponse.name).to.equal(req.name); + expect(shiftResponse.roles).to.deep.equalInAnyOrder(req.roles); + + // Cleanup + await EventShiftAnswer.delete({ eventId: shiftResponse.id }); + await EventShift.delete(shiftResponse.id); + }); + it('should return 400 if empty name', async () => { + const res = await request(ctx.app) + .post('/eventshifts') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + name: '', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Invalid shift.'); + }); + it('should return 400 if roles is not a list', async () => { + const res = await request(ctx.app) + .post('/eventshifts') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + roles: 'Role1', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Invalid shift.'); + }); + it('should return 200 if roles is an empty list', async () => { + const res = await request(ctx.app) + .post('/eventshifts') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + roles: [], + }); + expect(res.status).to.equal(200); + + const shiftResponse = res.body as EventShiftResponse; + + const validation = ctx.specification.validateModel('EventShiftResponse', shiftResponse, false, true); + expect(validation.valid).to.be.true; + + // Cleanup + await EventShiftAnswer.delete({ eventId: shiftResponse.id }); + await EventShift.delete(shiftResponse.id); + }); + it('should return 403 if not admin', async () => { + const res = await request(ctx.app) + .post('/eventshifts') + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('PATCH /eventshifts/{id}', () => { + let req: Partial; + let originalShift: EventShift; + + before(async () => { + req = { + name: 'Penningmeesteren', + roles: ['Oud-BAC', 'Sjaars'], + }; + + originalShift = await EventShift.findOne({ where: { id: ctx.eventShifts[0].id } }); + }); + + after(async () => { + await EventShift.update(originalShift.id, { + name: originalShift.name, + roles: originalShift.roles, + }); + }); + + it('should correctly update shift', async () => { + const res = await request(ctx.app) + .patch(`/eventshifts/${originalShift.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send(req); + expect(res.status).to.equal(200); + + const shiftResponse = res.body as EventShiftResponse; + + const validation = ctx.specification.validateModel('EventShiftResponse', shiftResponse, false, true); + expect(validation.valid).to.be.true; + + expect(shiftResponse.name).to.equal(req.name); + expect(shiftResponse.roles).to.deep.equalInAnyOrder(req.roles); + }); + it('should return 400 if empty name', async () => { + const res = await request(ctx.app) + .patch(`/eventshifts/${originalShift.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + name: '', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Invalid shift.'); + }); + it('should return 400 if roles is not a list', async () => { + const res = await request(ctx.app) + .patch(`/eventshifts/${originalShift.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + roles: 'Role1', + }); + expect(res.status).to.equal(400); + expect(res.body).to.equal('Invalid shift.'); + }); + it('should return 200 if roles is an empty list', async () => { + const res = await request(ctx.app) + .patch(`/eventshifts/${originalShift.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ + ...req, + roles: [], + }); + expect(res.status).to.equal(200); + + const shiftResponse = res.body as EventShiftResponse; + + const validation = ctx.specification.validateModel('EventShiftResponse', shiftResponse, false, true); + expect(validation.valid).to.be.true; + }); + it('should return 403 if not admin', async () => { + const res = await request(ctx.app) + .patch(`/eventshifts/${originalShift.id}`) + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('GET /eventshifts/{id}/counts', () => { + let answersSelected: EventShiftAnswer[]; + + before(async () => { + expect(await EventShiftAnswer.count({ where: { selected: true } })).to.equal(0); + + const users = Array.from(new Set(ctx.eventShiftAnswers.map((a) => a.userId))).slice(0, 2); + + const answers = ctx.eventShiftAnswers.filter((a) => users.includes(a.userId)); + answersSelected = await Promise.all(answers.map(async (a) => { + a.selected = true; + return a.save(); + })); + }); + + after(async () => { + await Promise.all(answersSelected.map(async (a) => { + a.selected = false; + return a.save(); + })); + expect(await EventShiftAnswer.count({ where: { selected: true } })).to.equal(0); + }); + + it('should correctly give the number of times a user is selected for a shift', async () => { + const shiftsIds = Array.from(new Set(answersSelected.map((a) => a.shiftId))); + + await Promise.all(shiftsIds.map(async (shiftId) => { + const shiftAnswers = answersSelected.filter((a) => a.shiftId === shiftId); + const userIds = Array.from(new Set(shiftAnswers.map((a) => a.userId))); + const res = await request(ctx.app) + .get(`/eventshifts/${shiftId}/counts`) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(200); + + const counts = res.body as EventPlanningSelectedCount[]; + + expect(counts.length).to.equal(userIds.length); + counts.forEach((c) => { + const validation = ctx.specification.validateModel('EventPlanningSelectedCount', c, false, true); + expect(validation.valid).to.be.true; + expect(c.count).to.equal(shiftAnswers.filter((a) => a.userId === c.id).length); + }); + })); + }); + it('should correctly only account for certain event types', async () => { + const event = ctx.events[0]; + await Event.update(event.id, { + type: EventType.OTHER, + }); + + const shiftsIds = Array.from(new Set(answersSelected + .filter((a) => a.eventId === event.id) + .map((a) => a.shiftId))); + expect(shiftsIds.length).to.be.at.least(1); + + const eventType = EventType.OTHER; + await Promise.all(shiftsIds.map(async (shiftId) => { + const res = await request(ctx.app) + .get(`/eventshifts/${shiftId}/counts`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .query({ eventType }); + expect(res.status).to.equal(200); + + const counts = res.body as EventPlanningSelectedCount[]; + counts.forEach((c) => { + expect(c.count).to.equal(1); + }); + })); + + // Cleanup + await Event.update(event.id, { + type: event.type, + }); + }); + it('should correctly only account for event after a certain date', async () => { + const events = answersSelected + .map((a) => a.event) + .filter((e, i, all) => all.findIndex((e2) => e2.id === e.id) === i) + .sort((e1, e2) => e2.startDate.getTime() - e1.startDate.getTime()); + + const shiftsIds = Array.from(new Set(answersSelected + .filter((a) => a.eventId === events[0].id) + .map((a) => a.shiftId))); + expect(shiftsIds.length).to.be.at.least(1); + + const afterDate = new Date(events[0].startDate.getTime() - 1000); + await Promise.all(shiftsIds.map(async (shiftId) => { + const res = await request(ctx.app) + .get(`/eventshifts/${shiftId}/counts`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .query({ afterDate }); + expect(res.status).to.equal(200); + + const counts = res.body as EventPlanningSelectedCount[]; + counts.forEach((c) => { + expect(c.count).to.equal(1); + }); + })); + }); + it('should correctly only account for event before a certain date', async () => { + const events = answersSelected + .map((a) => a.event) + .filter((e, i, all) => all.findIndex((e2) => e2.id === e.id) === i) + .sort((e1, e2) => e1.startDate.getTime() - e2.startDate.getTime()); + + const shiftsIds = Array.from(new Set(answersSelected + .filter((a) => a.eventId === events[0].id) + .map((a) => a.shiftId))); + expect(shiftsIds.length).to.be.at.least(1); + + const beforeDate = new Date(events[0].startDate.getTime() + 1000); + await Promise.all(shiftsIds.map(async (shiftId) => { + const res = await request(ctx.app) + .get(`/eventshifts/${shiftId}/counts`) + .set('Authorization', `Bearer ${ctx.adminToken}`) + .query({ beforeDate }); + expect(res.status).to.equal(200); + + const counts = res.body as EventPlanningSelectedCount[]; + counts.forEach((c) => { + expect(c.count).to.equal(1); + }); + })); + }); + it('should return 404 if shift does not exist', async () => { + const res = await request(ctx.app) + .get('/eventshifts/999999/counts') + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const res = await request(ctx.app) + .get(`/eventshifts/${ctx.eventShifts[0].id}/counts`) + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + + describe('DELETE /eventshifts/{id}', () => { + it('should correctly delete single shift', async () => { + const event = ctx.eventShifts[0]; + const res = await request(ctx.app) + .delete(`/eventshifts/${event.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(204); + expect(res.body).to.be.empty; + + expect(await EventShift.findOne({ where: { id: event.id } })).to.be.null; + }); + it('should return 404 if event does not exist', async () => { + const res = await request(ctx.app) + .delete('/eventshifts/999999') + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const event = ctx.eventShifts[0]; + const res = await request(ctx.app) + .delete(`/eventshifts/${event.id}`) + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); +}); diff --git a/test/unit/service/event-service.ts b/test/unit/service/event-service.ts new file mode 100644 index 000000000..a20d5f5fe --- /dev/null +++ b/test/unit/service/event-service.ts @@ -0,0 +1,918 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { describe } from 'mocha'; +import { Connection } from 'typeorm'; +import User from '../../../src/entity/user/user'; +import { seedEvents, seedRoles, seedUsers } from '../../seed'; +import Event, { EventType } from '../../../src/entity/event/event'; +import EventShift from '../../../src/entity/event/event-shift'; +import EventShiftAnswer, { Availability } from '../../../src/entity/event/event-shift-answer'; +import Database from '../../../src/database/database'; +import EventService, { CreateEventParams } from '../../../src/service/event-service'; +import { expect } from 'chai'; +import { BaseEventResponse, BaseEventShiftResponse } from '../../../src/controller/response/event-response'; +import AssignedRole from '../../../src/entity/roles/assigned-role'; +import sinon, { SinonSandbox, SinonSpy } from 'sinon'; +import Mailer from '../../../src/mailer'; +import nodemailer, { Transporter } from 'nodemailer'; + +describe('eventService', () => { + let ctx: { + connection: Connection, + users: User[], + events: Event[], + eventShifts: EventShift[], + deletedEventShifts: EventShift[], + eventShiftAnswers: EventShiftAnswer[], + roles: AssignedRole[], + }; + + before(async () => { + const connection = await Database.initialize(); + + const users = await seedUsers(); + const roles = await seedRoles(users); + const { events, eventShifts: allEventShifts, eventShiftAnswers } = await seedEvents(roles); + + const eventShifts = allEventShifts.filter((s) => s.deletedAt == null); + const deletedEventShifts = allEventShifts.filter((s) => s.deletedAt != null); + + ctx = { + connection, + users, + events, + eventShifts, + deletedEventShifts, + eventShiftAnswers, + roles, + }; + }); + + const checkEvent = (actual: BaseEventResponse, expected: Event) => { + expect(actual.id).to.equal(expected.id); + expect(actual.createdAt).to.equal(expected.createdAt.toISOString()); + expect(actual.updatedAt).to.equal(expected.updatedAt.toISOString()); + expect(actual.startDate).to.equal(expected.startDate.toISOString()); + expect(actual.endDate).to.equal(expected.endDate.toISOString()); + expect(actual.name).to.equal(expected.name); + expect(actual.type).to.equal(expected.type); + expect(actual.createdBy.id).to.equal(expected.createdBy.id); + }; + + const checkEventShift = (actual: BaseEventShiftResponse, expected: EventShift) => { + expect(actual.id).to.equal(expected.id); + expect(actual.createdAt).to.equal(expected.createdAt.toISOString()); + expect(actual.updatedAt).to.equal(expected.updatedAt.toISOString()); + expect(actual.name).to.equal(expected.name); + }; + + after(async () => { + await ctx.connection.dropDatabase(); + await ctx.connection.destroy(); + }); + + describe('getEvents', () => { + it('should return all events ordered by startDate descending', async () => { + const { records: events } = await EventService.getEvents(); + expect(events.length).be.greaterThan(0); + expect(events.length).to.equal(ctx.events.length); + + events.forEach((e) => { + const dbEvent = ctx.events.find((e2) => e2.id === e.id); + expect(dbEvent).to.not.be.undefined; + checkEvent(e, dbEvent); + }); + + const dates = events.map((e) => new Date(e.startDate).getTime()); + expect(dates, 'Expected startDate to be sorted in descending order').to.be.descending; + }); + it('should filter on id', async () => { + const id = ctx.events[0].id; + const { records: events } = await EventService.getEvents({ id }); + + expect(events.length).to.equal(1); + expect(events[0].id).to.equal(id); + }); + it('should filter on exact name', async () => { + const name = ctx.events[0].name; + const actualEvents = ctx.events.filter((e) => e.name === name); + const { records: events } = await EventService.getEvents({ name }); + + expect(actualEvents.length).to.equal(events.length); + events.forEach((e) => { + expect(e.name).to.equal(name); + }); + }); + it('should filter on like name', async () => { + const name = ctx.events[0].name.substring(0, 6); + const actualEvents = ctx.events.filter((e) => e.name.substring(0, 6) === name); + const { records: events } = await EventService.getEvents({ name }); + + expect(actualEvents.length).to.equal(events.length); + events.forEach((e) => { + expect(e.name).to.include(name); + }); + }); + it('should filter on created by ID', async () => { + const createdById = ctx.events[0].createdBy.id; + const { records: events } = await EventService.getEvents({ createdById }); + + expect(events.length).to.equal(1); + expect(events[0].createdBy.id).to.equal(createdById); + }); + it('should filter on date (before)', async () => { + const beforeDate = ctx.events[0].startDate; + const actualEvents = ctx.events.filter((e) => e.startDate.getTime() <= beforeDate.getTime()); + const { records: events } = await EventService.getEvents({ beforeDate }); + + expect(actualEvents.length).to.equal(events.length); + const ids = actualEvents.map((e) => e.id); + events.forEach((e) => { + expect(ids).to.include(e.id); + expect(new Date(e.startDate)).to.be.lessThanOrEqual(beforeDate); + }); + }); + it('should filter on date (after)', async () => { + const afterDate = ctx.events[0].startDate; + const actualEvents = ctx.events.filter((e) => e.startDate.getTime() >= afterDate.getTime()); + const { records: events } = await EventService.getEvents({ afterDate }); + + expect(actualEvents.length).to.equal(events.length); + const ids = actualEvents.map((e) => e.id); + events.forEach((e) => { + expect(ids).to.include(e.id); + expect(new Date(e.startDate)).to.be.greaterThanOrEqual(afterDate); + }); + }); + it('should filter based on date range', async () => { + const afterDate = ctx.events[0].startDate; + const beforeDate = ctx.events[1].startDate; + const actualEvents = ctx.events.filter((e) => e.startDate.getTime() >= afterDate.getTime() && e.startDate.getTime() <= beforeDate.getTime()); + const { records: events } = await EventService.getEvents({ beforeDate, afterDate }); + + expect(actualEvents.length).to.equal(events.length); + const ids = actualEvents.map((e) => e.id); + events.forEach((e) => { + expect(ids).to.include(e.id); + expect(new Date(e.startDate)).to.be.greaterThanOrEqual(afterDate); + }); + }); + it('should filter on createdById', async () => { + const createdById = ctx.events[0].createdBy.id; + const actualEvents = ctx.events.filter((e) => e.createdBy.id === createdById); + const { records: events } = await EventService.getEvents({ createdById }); + + expect(events.length).to.equal(actualEvents.length); + const ids = actualEvents.map((e) => e.id); + events.forEach((e) => { + expect(ids).to.include(e.id); + expect(e.createdBy.id).to.equal(createdById); + }); + }); + it('should adhere to pagination', async () => { + const take = 3; + const skip = 1; + const events = await EventService.getEvents({}, { take, skip }); + expect(events.records.length).to.equal(take); + expect(events._pagination.take).to.equal(take); + expect(events._pagination.skip).to.equal(skip); + expect(events._pagination.count).to.equal(ctx.events.length); + + const ids = ctx.events + .sort((a, b) => b.startDate.getTime() - a.startDate.getTime()) + .slice(skip, skip + take) + .map((e) => e.id); + events.records.forEach((event, i) => { + expect(event.id).to.equal(ids[i]); + }); + }); + }); + + describe('getSingleEvent', () => { + it('should return correct event', async () => { + const actualEvent = ctx.events[0]; + const event = await EventService.getSingleEvent(actualEvent.id); + + expect(event).to.not.be.undefined; + checkEvent(event, actualEvent); + }); + it('should return correct event with soft deleted shift', async () => { + const actualEvent = ctx.events.find((e) => e.answers.some((a) => a.shift.deletedAt != null)); + expect(actualEvent).to.not.be.undefined; + const event = await EventService.getSingleEvent(actualEvent.id); + + expect(event).to.not.be.undefined; + checkEvent(event, actualEvent); + }); + it('should return undefined if event does not exist', async () => { + const event = await EventService.getSingleEvent(99999999); + expect(event).to.be.undefined; + }); + }); + + describe('sendEventPlanningReminders', () => { + let sandbox: SinonSandbox; + let sendMailFake: SinonSpy; + + before(() => { + Mailer.reset(); + + sandbox = sinon.createSandbox(); + sendMailFake = sandbox.spy(); + sandbox.stub(nodemailer, 'createTransport').returns({ + sendMail: sendMailFake, + } as any as Transporter); + }); + + after(() => { + sandbox.restore(); + }); + + afterEach(() => { + sendMailFake.resetHistory(); + }); + + it('should send a reminder to all users that have not given up their availability', async () => { + const { startDate, answers } = ctx.events[0]; + const users = Array.from(new Set(answers.filter((a) => a.availability == null).map((a) => a.user))); + const referenceDate = new Date(startDate.getTime() - 1000 * 3600 * 24 * 2.5); + + await EventService.sendEventPlanningReminders(referenceDate); + + expect(sendMailFake.callCount).to.equal(users.length); + }); + it('should send a reminder to events starting between two to three days when no date given', async () => { + const event = ctx.events[0]; + const users = Array.from(new Set(event.answers.filter((a) => a.availability == null).map((a) => a.user))); + // Add a second to the two days to account for running time of the test case + const startDate = new Date(new Date().getTime() + 1000 * 3600 * 24 * 2.5); + await EventService.updateEvent(event.id, { + startDate, + }); + + await EventService.sendEventPlanningReminders(); + + expect(sendMailFake.callCount).to.equal(users.length); + + // Cleanup + await EventService.updateEvent(event.id, { + startDate: event.startDate, + }); + }); + it('should send a reminder to events starting in two days from now', async () => { + const event = ctx.events[0]; + const now = new Date(); + const users = Array.from(new Set(event.answers.filter((a) => a.availability == null).map((a) => a.user))); + const startDate = new Date(now.getTime() + 1000 * 3600 * 24 * 2); + await EventService.updateEvent(event.id, { + startDate, + }); + + await EventService.sendEventPlanningReminders(now); + + expect(sendMailFake.callCount).to.equal(users.length); + + // Cleanup + await EventService.updateEvent(event.id, { + startDate: event.startDate, + }); + }); + it('should send a reminder to events starting in three days from now', async () => { + const event = ctx.events[0]; + const now = new Date(); + const users = Array.from(new Set(event.answers.filter((a) => a.availability == null).map((a) => a.user))); + const startDate = new Date(now.getTime() + 1000 * 3600 * 24 * 3); + await EventService.updateEvent(event.id, { + startDate, + }); + + await EventService.sendEventPlanningReminders(now); + + expect(sendMailFake.callCount).to.equal(users.length); + + // Cleanup + await EventService.updateEvent(event.id, { + startDate: event.startDate, + }); + }); + it('should not send a reminder to events starting in three days and one millisecond from now', async () => { + const event = ctx.events[0]; + const now = new Date(); + const startDate = new Date(now.getTime() + 1000 * 3600 * 24 * 3 + 1); + await EventService.updateEvent(event.id, { + startDate, + }); + + await EventService.sendEventPlanningReminders(now); + + expect(sendMailFake.callCount).to.equal(0); + + // Cleanup + await EventService.updateEvent(event.id, { + startDate: event.startDate, + }); + }); + }); + + describe('syncEventShiftAnswers', () => { + it('should correctly change answers when users change role', async () => { + const event = ctx.events.find((e) => e.answers.length > 0 && e.answers.every((a) => ctx.eventShifts.map((s) => s.id).includes(a.shiftId))); + const answer = event.answers[event.answers.length - 1]; + const shiftIds = event.shifts.map((a) => a.id); + const roleWithUsers = ctx.roles.filter((r) => r.userId === answer.userId); + const roleWithUser = roleWithUsers[0]; + expect(event).to.not.be.undefined; + expect(roleWithUsers).to.not.be.undefined; + expect(roleWithUsers.length).to.equal(1); + expect(await EventShiftAnswer.findOne({ where: { eventId: event.id, shiftId: answer.shiftId, userId: answer.userId } })).to.not.be.null; + + const eventResponse1 = await EventService.getSingleEvent(event.id); + const answers1 = await EventService.syncEventShiftAnswers(event); + expect(eventResponse1.shifts.reduce((t, e) => t + e.answers.length, 0)).to.equal(answers1.length); + + await AssignedRole.delete({ userId: roleWithUser.userId, role: roleWithUser.role }); + + const answers2 = await EventService.syncEventShiftAnswers(event); + const removedAnswers = answers1.filter((a1) => answers2.findIndex((a2) => a2.userId === a1.user.id) === -1); + + expect(removedAnswers.length).to.be.greaterThan(0); + removedAnswers.forEach((r) => { + expect(r.user.id).to.equal(roleWithUser.userId); + }); + + expect(await EventShiftAnswer.findOne({ where: { eventId: event.id, userId: roleWithUser.userId } })).to.be.null; + + // Cleanup + await AssignedRole.insert({ + userId: roleWithUser.userId, + role: roleWithUser.role, + createdAt: roleWithUser.createdAt, + updatedAt: roleWithUser.updatedAt, + version: roleWithUser.version, + }); + await EventService.syncEventShiftAnswers(event, shiftIds); + }); + it('should throw an error when a non-existent shift is provided', async () => { + await expect(EventService.syncEventShiftAnswers(ctx.events[0], [9999999])) + .to.eventually.be.rejectedWith('Invalid list of shiftIds provided.'); + }); + }); + + describe('syncAllEventShiftAnswers', () => { + it('should correctly change answers when users change role', async () => { + const event = ctx.events.find((e) => e.answers.length > 0 && e.answers.every((a) => ctx.eventShifts.map((s) => s.id).includes(a.shiftId))); + const answer = event.answers[event.answers.length - 1]; + const shiftIds = event.shifts.map((a) => a.id); + const roleWithUsers = ctx.roles.filter((r) => r.userId === answer.userId); + const roleWithUser = roleWithUsers[0]; + expect(event).to.not.be.undefined; + expect(roleWithUsers).to.not.be.undefined; + expect(roleWithUsers.length).to.equal(1); + expect(await EventShiftAnswer.findOne({ where: { eventId: event.id, shiftId: answer.shiftId, userId: answer.userId } })).to.not.be.null; + + await EventService.syncAllEventShiftAnswers(); + expect(await EventShiftAnswer.findOne({ where: { eventId: event.id, shiftId: answer.shiftId, userId: answer.userId } })).to.not.be.null; + + await AssignedRole.delete({ userId: roleWithUser.userId, role: roleWithUser.role }); + + await EventService.syncAllEventShiftAnswers(); + expect(await EventShiftAnswer.findOne({ where: { eventId: event.id, userId: roleWithUser.userId } })).to.be.null; + + // Cleanup + await AssignedRole.insert({ + userId: roleWithUser.userId, + role: roleWithUser.role, + createdAt: roleWithUser.createdAt, + updatedAt: roleWithUser.updatedAt, + version: roleWithUser.version, + }); + await EventService.syncEventShiftAnswers(event, shiftIds); + }); + }); + + describe('createEvent', () => { + it('should correctly create event', async () => { + const shift = ctx.eventShifts[0]; + const params: CreateEventParams = { + createdById: ctx.users[0].id, + name: 'TestEvent', + startDate: new Date(), + endDate: new Date(new Date().getTime() + 1000 * 60 * 60), + shiftIds: [shift.id], + type: EventType.BORREL, + }; + const event = await EventService.createEvent(params); + + expect(event).to.not.be.undefined; + expect(event.name).to.equal(params.name); + expect(event.createdBy.id).to.equal(params.createdById); + expect(event.startDate).to.equal(params.startDate.toISOString()); + expect(event.endDate).to.equal(params.endDate.toISOString()); + expect(event.shifts.length).to.equal(params.shiftIds.length); + + const users = ctx.roles + .filter((r) => shift.roles.includes(r.role)) + .map((r) => r.user); + const userIds = users.map((u) => u.id); + const actualUserIds = Array.from(new Set( + event.shifts.map((s) => s.answers.map((a) => a.user.id)).flat(), + )); + expect(actualUserIds.length).to.be.greaterThan(0); + expect(actualUserIds).to.deep.equalInAnyOrder(userIds); + + // Cleanup + await Event.delete(event.id); + }); + it('should create event with no shifts', async () => { + const params: CreateEventParams = { + createdById: ctx.users[0].id, + name: 'TestEvent', + startDate: new Date(), + endDate: new Date(new Date().getTime() + 1000 * 60 * 60), + shiftIds: [], + type: EventType.BORREL, + }; + const event = await EventService.createEvent(params); + + expect(event).to.not.be.undefined; + expect(event.shifts).to.be.empty; + + // Cleanup + await Event.delete(event.id); + }); + it('should throw an error when a non-existent shift is provided', async () => { + const params: CreateEventParams = { + createdById: ctx.users[0].id, + name: 'TestEvent', + startDate: new Date(), + endDate: new Date(new Date().getTime() + 1000 * 60 * 60), + shiftIds: [9999999], + type: EventType.BORREL, + }; + await expect(EventService.createEvent(params)).to.eventually.be.rejectedWith('Invalid list of shiftIds provided.'); + }); + }); + + describe('updateEvent', () => { + let originalEvent: Event; + let newShift: EventShift; + + before(async () => { + originalEvent = await Event.findOne({ + where: { answers: { shift: { deletedAt: null } } }, + relations: ['answers', 'answers.shift'], + }); + }); + + after(async () => { + await EventService.updateEvent(originalEvent.id, { + name: originalEvent.name, + startDate: originalEvent.startDate, + endDate: originalEvent.endDate, + shiftIds: originalEvent.answers + .map((a) => a.shiftId) + .filter((a1, i, all) => i === all + .findIndex((a2) => a2 === a1)), + }); + }); + + it('should correctly update name', async () => { + const name = 'AViCo Centurion Marathon'; + const event = await EventService.updateEvent(originalEvent.id, { + name, + }); + expect(event.name).to.equal(name); + expect((await Event.findOne({ where: { id: originalEvent.id } })).name).to.equal(name); + }); + it('should correctly update startDate', async () => { + const startDate = new Date('2020-07-01'); + const event = await EventService.updateEvent(originalEvent.id, { + startDate: startDate, + }); + expect(event.startDate).to.equal(startDate.toISOString()); + expect((await Event.findOne({ where: { id: originalEvent.id } })).startDate.getTime()).to.equal(startDate.getTime()); + }); + it('should correctly update endDate', async () => { + const endDate = new Date('2020-07-01'); + const event = await EventService.updateEvent(originalEvent.id, { + endDate: endDate, + }); + expect(event.endDate).to.equal(endDate.toISOString()); + expect((await Event.findOne({ where: { id: originalEvent.id } })).endDate.getTime()).to.equal(endDate.getTime()); + }); + it('should correctly update shiftIds by adding a shift', async () => { + const shifts = originalEvent.answers + .map((a) => a.shift) + .filter((a1, i, all) => i === all + .findIndex((a2) => a2.id === a1.id)); + expect(shifts.length).to.be.greaterThan(1); + + newShift = ctx.eventShifts + .find((s1) => !shifts.map((s2) => s2.id).includes(s1.id) && s1.roles.length > 0); + expect(newShift).to.not.be.undefined; + + const shiftIds = [...shifts.map((s) => s.id), newShift.id]; + const event = await EventService.updateEvent(originalEvent.id, { + shiftIds, + }); + + // Answer sheets should include new shift + expect(event.shifts.map((s) => s.id)).to.deep.equalInAnyOrder(shiftIds); + event.shifts.forEach((s) => { + expect(s.answers.length).to.be.greaterThan(0); + }); + + }); + it('should correctly update shiftIds by removing a shift', async function () { + // Skip this test case if newShift is undefined, i.e. we did not run the previous test case + if (newShift == null) { + this.skip(); + return; + } + + const shifts = originalEvent.answers + .map((a) => a.shift) + .filter((a1, i, all) => i === all + .findIndex((a2) => a2.id === a1.id)); + expect(shifts.length).to.be.greaterThan(1); + + const shiftIds = [...shifts.map((s) => s.id)]; + const event = await EventService.updateEvent(originalEvent.id, { + shiftIds, + }); + + // Answer sheets should no longer include new shift + const actualIds = event.shifts.map((s) => s.id); + expect(actualIds).to.deep.equalInAnyOrder(shiftIds); + expect(actualIds.findIndex((i) => i === newShift.id)).to.equal(-1); + }); + it('should throw an error when a non-existent shift is provided', async () => { + await expect(EventService.updateEvent(originalEvent.id, { + shiftIds: [9999999], + })).to.eventually.be.rejectedWith('Invalid list of shiftIds provided.'); + }); + it('should return undefined if event does not exist', async () => { + const event = await EventService.updateEvent(9999999999, { name: 'does not matter' }); + expect(event).to.be.undefined; + }); + }); + + describe('getShifts', () => { + it('should return all shifts', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { records: shifts, _pagination } = await EventService.getEventShifts({}); + expect(shifts.length).to.equal(ctx.eventShifts.filter((s) => s.deletedAt == null).length); + + shifts.forEach((s) => { + const actualShift = ctx.eventShifts.find((s2) => s2.id === s.id); + expect(actualShift).to.not.be.undefined; + checkEventShift(s, actualShift); + }); + + expect(_pagination.count).to.equal(shifts.length); + expect(_pagination.take).to.be.undefined; + }); + }); + + describe('createShift', () => { + it('should correctly create a new shift', async () => { + const name = 'Feuten op dweilen zetten'; + const roles = ['BAC Veurzitter', 'BAC Oud-veurzitter']; + const shift = await EventService.createEventShift({ + name, + roles, + }); + expect(shift).to.not.be.undefined; + expect(shift.name).to.equal(name); + expect(shift.roles).to.deep.equalInAnyOrder(roles); + + const dbShift = await EventShift.findOne({ where: { id: shift.id } }); + expect(dbShift).to.not.be.undefined; + expect(dbShift.name).to.equal(name); + + // Cleanup + await dbShift.remove(); + }); + }); + + describe('updateShift', () => { + let originalShift: EventShift; + + before(async () => { + originalShift = await EventShift.findOne({ + where: { id: ctx.eventShifts[0].id }, + }); + }); + + after(async () => { + await EventShift.update(originalShift.id, { + name: originalShift.name, + roles: originalShift.roles, + }); + }); + + it('should correctly update nothing', async () => { + const shift = await EventService.updateEventShift(originalShift.id, {}); + expect(shift.name).to.equal(originalShift.name); + expect(shift.roles).to.deep.equalInAnyOrder(originalShift.roles); + }); + + it('should correctly update name', async () => { + const name = 'UpdatedName'; + const shift = await EventService.updateEventShift(originalShift.id, { + name, + }); + + expect(shift.name).to.equal(name); + expect((await EventShift.findOne({ where: { id: originalShift.id } })).name) + .to.equal(name); + }); + + it('should correctly update roles', async () => { + const roles = ['A', 'B', 'C']; + const shift = await EventService.updateEventShift(originalShift.id, { + roles, + }); + + expect(shift.roles).to.equal(roles); + expect((await EventShift.findOne({ where: { id: originalShift.id } })).roles) + .to.deep.equalInAnyOrder(roles); + }); + it('should return undefined if shift does not exist', async () => { + const shift = await EventService.updateEventShift(99999, {}); + expect(shift).to.be.undefined; + }); + }); + + describe('updateEventShiftAnswer', () => { + let originalAnswer: EventShiftAnswer; + + before(async () => { + const answer = ctx.eventShiftAnswers.find((a) => a.availability === null && a.selected === false); + expect(answer).to.not.be.undefined; + + const { userId, shiftId, eventId } = answer; + originalAnswer = await EventShiftAnswer.findOne({ where: { + userId, shiftId, eventId, + } }); + }); + + after(async () => { + await EventShiftAnswer.update({ + userId: ctx.eventShiftAnswers[0].userId, + shiftId: ctx.eventShiftAnswers[0].shiftId, + eventId: ctx.eventShiftAnswers[0].eventId, + }, { + availability: originalAnswer.availability, + selected: originalAnswer.selected, + }); + }); + + it('should correctly update nothing', async () => { + const answer = await EventService.updateEventShiftAnswer(originalAnswer.eventId, originalAnswer.shiftId, originalAnswer.userId, {}); + expect(answer.user.id).to.equal(originalAnswer.userId); + expect(answer.availability).to.equal(originalAnswer.availability); + expect(answer.selected).to.equal(originalAnswer.selected); + }); + it('should correctly update availability', async () => { + const { eventId, shiftId, userId } = originalAnswer; + + let availability = Availability.YES; + let answer = await EventService.updateEventShiftAnswer(originalAnswer.eventId, originalAnswer.shiftId, originalAnswer.userId, { availability }); + expect(answer.availability).to.equal(availability); + expect((await EventShiftAnswer.findOne({ where: { + eventId, shiftId, userId, + } })).availability).to.equal(availability); + + availability = Availability.LATER; + answer = await EventService.updateEventShiftAnswer(originalAnswer.eventId, originalAnswer.shiftId, originalAnswer.userId, { availability }); + expect(answer.availability).to.equal(availability); + expect((await EventShiftAnswer.findOne({ where: { + eventId, shiftId, userId, + } })).availability).to.equal(availability); + + // Cleanup + availability = originalAnswer.availability; + answer = await EventService.updateEventShiftAnswer(originalAnswer.eventId, originalAnswer.shiftId, originalAnswer.userId, { availability }); + expect(answer.availability).to.equal(null); + }); + it('should correctly update selection', async () => { + const { eventId, shiftId, userId } = originalAnswer; + + let selected = true; + let answer = await EventService.updateEventShiftAnswer(originalAnswer.eventId, originalAnswer.shiftId, originalAnswer.userId, { selected }); + expect(answer.selected).to.equal(selected); + expect((await EventShiftAnswer.findOne({ where: { + eventId, shiftId, userId, + } })).selected).to.equal(selected); + + // Cleanup + await EventService.updateEventShiftAnswer(originalAnswer.eventId, originalAnswer.shiftId, originalAnswer.userId, { selected: !selected }); + }); + it('should return undefined if answer does not exist', async () => { + const answer = await EventService.updateEventShiftAnswer(99999, 99999, 99999, {}); + expect(answer).to.be.undefined; + }); + }); + + describe('getShiftSelectedCount', () => { + let answersSelected: EventShiftAnswer[]; + + before(async () => { + expect(await EventShiftAnswer.count({ where: { selected: true } })).to.equal(0); + + const users = Array.from(new Set(ctx.eventShiftAnswers.map((a) => a.userId))).slice(0, 2); + + const answers = ctx.eventShiftAnswers.filter((a) => users.includes(a.userId)); + answersSelected = await Promise.all(answers.map(async (a) => { + a.selected = true; + return a.save(); + })); + }); + + after(async () => { + await Promise.all(answersSelected.map(async (a) => { + a.selected = false; + return a.save(); + })); + expect(await EventShiftAnswer.count({ where: { selected: true } })).to.equal(0); + }); + + it('should correctly give the number of times a user is selected for a shift', async () => { + const shiftsIds = Array.from(new Set(answersSelected.map((a) => a.shiftId))); + + await Promise.all(shiftsIds.map(async (shiftId) => { + const shiftAnswers = answersSelected.filter((a) => a.shiftId === shiftId); + const userIds = Array.from(new Set(shiftAnswers.map((a) => a.userId))); + const counts = await EventService.getShiftSelectedCount(shiftId); + expect(counts.length).to.equal(userIds.length); + counts.forEach((c) => { + expect(c.count).to.equal(shiftAnswers.filter((a) => a.userId === c.id).length); + }); + })); + }); + it('should correctly only account for certain event types', async () => { + const event = ctx.events[0]; + await Event.update(event.id, { + type: EventType.OTHER, + }); + + const shiftsIds = Array.from(new Set(answersSelected + .filter((a) => a.eventId === event.id) + .map((a) => a.shiftId))); + expect(shiftsIds.length).to.be.at.least(1); + + const eventType = EventType.OTHER; + await Promise.all(shiftsIds.map(async (shiftId) => { + const counts = await EventService.getShiftSelectedCount(shiftId, { eventType }); + counts.forEach((c) => { + expect(c.count).to.equal(1); + }); + })); + + // Cleanup + await Event.update(event.id, { + type: event.type, + }); + }); + it('should correctly only account for event after a certain date', async () => { + const events = answersSelected + .map((a) => a.event) + .filter((e, i, all) => all.findIndex((e2) => e2.id === e.id) === i) + .sort((e1, e2) => e2.startDate.getTime() - e1.startDate.getTime()); + + const shiftsIds = Array.from(new Set(answersSelected + .filter((a) => a.eventId === events[0].id) + .map((a) => a.shiftId))); + expect(shiftsIds.length).to.be.at.least(1); + + const afterDate = new Date(events[0].startDate.getTime() - 1000); + await Promise.all(shiftsIds.map(async (shiftId) => { + const counts = await EventService.getShiftSelectedCount(shiftId, { afterDate }); + counts.forEach((c) => { + expect(c.count).to.equal(1); + }); + })); + }); + it('should correctly only account for event before a certain date', async () => { + const events = answersSelected + .map((a) => a.event) + .filter((e, i, all) => all.findIndex((e2) => e2.id === e.id) === i) + .sort((e1, e2) => e1.startDate.getTime() - e2.startDate.getTime()); + + const shiftsIds = Array.from(new Set(answersSelected + .filter((a) => a.eventId === events[0].id) + .map((a) => a.shiftId))); + expect(shiftsIds.length).to.be.at.least(1); + + const beforeDate = new Date(events[0].startDate.getTime() + 1000); + await Promise.all(shiftsIds.map(async (shiftId) => { + const counts = await EventService.getShiftSelectedCount(shiftId, { beforeDate }); + counts.forEach((c) => { + expect(c.count).to.equal(1); + }); + })); + }); + }); + + describe('deleteShift', () => { + it('should soft delete shift if it has answers', async () => { + const shift = ctx.eventShiftAnswers[0].shift; + const dbShift = await EventShift.findOne({ where: { id: shift.id }, withDeleted: true }); + expect(dbShift).to.not.be.null; + expect(dbShift.deletedAt).to.be.null; + expect((await EventShiftAnswer.find({ where: { shiftId: shift.id } })).length).to.be.greaterThan(0); + + await EventService.deleteEventShift(shift.id); + + const dbShift2 = await EventShift.findOne({ where: { id: shift.id }, withDeleted: true }); + expect(dbShift2).to.not.be.null; + expect(dbShift2.deletedAt).to.not.be.null; + expect(new Date().getTime() - dbShift2.deletedAt.getTime()).to.be.at.most(1000); + + // Cleanup + dbShift2.deletedAt = null; + await dbShift2.save(); + }); + it('should delete shift if it has no answers', async () => { + const shift = ctx.eventShifts[3]; + const dbShift = await EventShift.findOne({ where: { id: shift.id }, withDeleted: true }); + expect(dbShift).to.not.be.null; + expect(dbShift.deletedAt).to.be.null; + expect((await EventShiftAnswer.find({ where: { shiftId: shift.id } })).length).to.equal(0); + + await EventService.deleteEventShift(shift.id); + + const dbShift2 = await EventShift.findOne({ where: { id: shift.id }, withDeleted: true }); + expect(dbShift2).to.be.null; + + // Cleanup + await EventShift.insert({ + id: shift.id, + name: shift.name, + roles: [], + }); + }); + it('should delete shift if it was soft-deleted before', async () => { + const shift = ctx.eventShifts[3]; + let dbShift = await EventShift.findOne({ where: { id: shift.id }, withDeleted: true }); + expect(dbShift).to.not.be.null; + expect(dbShift.deletedAt).to.be.null; + expect((await EventShiftAnswer.find({ where: { shiftId: shift.id } })).length).to.equal(0); + + await dbShift.softRemove(); + dbShift = await EventShift.findOne({ where: { id: dbShift.id }, withDeleted: true }); + expect(dbShift).to.not.be.null; + expect(dbShift.deletedAt).to.not.be.null; + + await EventService.deleteEventShift(dbShift.id); + + dbShift = await EventShift.findOne({ where: { id: dbShift.id }, withDeleted: true }); + expect(dbShift).to.be.null; + + // Cleanup + await EventShift.insert({ + id: shift.id, + name: shift.name, + roles: [], + }); + }); + it('should simply return when shift does not exist', async () => { + expect(EventService.deleteEventShift(9999999)).to.eventually.not.throw; + }); + }); + + describe('deleteEvent', () => { + it('should correctly delete an event with its answer sheets', async () => { + const event = ctx.events[0]; + const dbEvent = await Event.findOne({ where: { id: event.id } }); + expect(dbEvent).to.not.be.null; + + await EventService.deleteEvent(event.id); + + const dbEvent2 = await Event.findOne({ where: { id: event.id } }); + expect(dbEvent2).to.be.null; + + const answers = await EventShiftAnswer.find({ where: { eventId: event.id } }); + expect(answers.length).to.equal(0); + }); + it('should simply return when event does not exist', async () => { + expect(await EventService.deleteEvent(9999999)).to.not.throw; + }); + }); +}); From 817832e5e752bf2b186c538a714026cd0340a894 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:50:26 +0200 Subject: [PATCH 04/13] Fix trace logs missing from production container (#80) * Possibly fix trace logs missing from production container * Fix docker build failing * Fix docker build failing --- Dockerfile | 4 ++-- init_scripts/start.sh | 2 +- pm2.json | 13 +++++++++++++ process.json | 10 ---------- src/entity/user/user.ts | 11 +++++++++++ src/index.ts | 7 +++++++ 6 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 pm2.json delete mode 100644 process.json diff --git a/Dockerfile b/Dockerfile index eaafd4be2..d0fb4528e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,13 +15,13 @@ RUN apk add openssl WORKDIR /app COPY ./package.json ./package-lock.json ./ RUN npm ci -RUN npm install pm2 -g +RUN npm install pm2 pm2-graceful-intercom -g ARG TYPEORM_USERNAME ARG TYPEORM_PASSWORD ARG TYPEORM_DATABASE COPY --from=build --chown=node /app/init_scripts /app/init_scripts -COPY --from=build --chown=node /app/process.json /app/process.json +COPY --from=build --chown=node /app/pm2.json /app/pm2.json RUN chmod +x /app/init_scripts/start.sh COPY --from=build --chown=node /app/out/src /app/out/src diff --git a/init_scripts/start.sh b/init_scripts/start.sh index 7e46e78a9..9b9b0396c 100644 --- a/init_scripts/start.sh +++ b/init_scripts/start.sh @@ -3,4 +3,4 @@ chmod +x /app/init_scripts/00_make_sudosos_data_dirs.sh chmod +x /app/init_scripts/00_regen_sudosos_secrets.sh sh /app/init_scripts/00_make_sudosos_data_dirs.sh sh /app/init_scripts/00_regen_sudosos_secrets.sh -pm2-runtime start /app/process.json +pm2-runtime start /app/pm2.json diff --git a/pm2.json b/pm2.json new file mode 100644 index 000000000..518dd920e --- /dev/null +++ b/pm2.json @@ -0,0 +1,13 @@ +{ + "apps": [{ + "name": "http-worker", + "script": "./out/src/index.js", + "instances": 4, + "exec_mode": "cluster" + }, { + "name": "cron", + "script": "./out/src/cron.js", + "instances": 1, + "exec_mode": "cluster" + }] +} diff --git a/process.json b/process.json deleted file mode 100644 index c22bacb77..000000000 --- a/process.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "apps": [{ - "name": "http-worker", - "script": "./out/src/index.js", - "instances": 4 - }, { - "name": "cron", - "script": "./out/src/cron.js" - }] -} diff --git a/src/entity/user/user.ts b/src/entity/user/user.ts index d2e93a5a4..0256f8129 100644 --- a/src/entity/user/user.ts +++ b/src/entity/user/user.ts @@ -127,4 +127,15 @@ export default class User extends BaseEntity { @OneToMany(() => AssignedRole, (role) => role.user) public roles: AssignedRole[]; + + public fullName(): string { + let name = this.firstName; + if (this.nickname) name += ` "${this.nickname}"`; + if (this.lastName) name += ` ${this.lastName}`; + return name; + } + + public toString(): string { + return `${this.fullName()} (SudoSOS ID: ${this.id})`; + } } diff --git a/src/index.ts b/src/index.ts index 9c64999a4..e9a5749a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,6 +170,13 @@ async function setupAuthentication(tokenHandler: TokenHandler, application: Appl export default async function createApp(): Promise { const application = new Application(); + log4js.configure({ + pm2: true, + appenders: { + out: { type: 'stdout' }, + }, + categories: { default: { appenders: ['out'], level: 'all' } }, + }); application.logger = log4js.getLogger('Application'); application.logger.level = process.env.LOG_LEVEL; application.logger.info('Starting application instance...'); From 9ff5f9710cfcbe482a945e5742db32f788216076 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:23:29 +0200 Subject: [PATCH 05/13] Voucher auto ToS accept & restrict users who can go into debt (#79) * Fix voucher users being required to accept ToS * Refactor and rename borrelkaart to voucher * Add attribute to user entity whether user can go into debt * Change that GEWIS members can go into debt by default * Add check when making transaction whether user can go into debt * Lint fix * Fix endpoint documentation --- src/controller/balance-controller.ts | 2 +- .../borrelkaart-group-controller.ts | 212 --------------- src/controller/request/create-user-request.ts | 31 --- ...update-user-request.ts => user-request.ts} | 35 ++- .../request/validators/user-request-spec.ts | 7 +- ...up-request.ts => voucher-group-request.ts} | 10 +- .../response/transaction-response.ts | 10 +- src/controller/response/user-response.ts | 2 + ...-response.ts => voucher-group-response.ts} | 24 +- src/controller/transaction-controller.ts | 3 +- src/controller/user-controller.ts | 11 +- src/controller/voucher-group-controller.ts | 212 +++++++++++++++ src/database/database.ts | 8 +- ...elkaart-group.ts => user-voucher-group.ts} | 12 +- src/entity/user/user.ts | 11 +- ...{borrelkaart-group.ts => voucher-group.ts} | 12 +- .../gewis-authentication-controller.ts | 2 +- src/gewis/gewis.ts | 9 +- src/helpers/revision-to-response.ts | 3 + src/index.ts | 4 +- src/service/authentication-service.ts | 4 +- src/service/transaction-service.ts | 6 - src/service/user-service.ts | 3 +- ...up-service.ts => voucher-group-service.ts} | 153 +++++------ test/helpers/user-factory.ts | 1 + .../borrelkaart-group-controller.ts | 244 +++++++++--------- .../unit/controller/transaction-controller.ts | 14 +- test/unit/controller/user-controller.ts | 40 +-- test/unit/service/authentication-service.ts | 1 + ...up-service.ts => voucher-group-service.ts} | 195 +++++++------- test/unit/validators.ts | 18 +- 31 files changed, 645 insertions(+), 654 deletions(-) delete mode 100644 src/controller/borrelkaart-group-controller.ts delete mode 100644 src/controller/request/create-user-request.ts rename src/controller/request/{update-user-request.ts => user-request.ts} (62%) rename src/controller/request/{borrelkaart-group-request.ts => voucher-group-request.ts} (89%) rename src/controller/response/{borrelkaart-group-response.ts => voucher-group-response.ts} (71%) create mode 100644 src/controller/voucher-group-controller.ts rename src/entity/user/{user-borrelkaart-group.ts => user-voucher-group.ts} (77%) rename src/entity/user/{borrelkaart-group.ts => voucher-group.ts} (82%) rename src/service/{borrelkaart-group-service.ts => voucher-group-service.ts} (52%) rename test/unit/service/{borrelkaart-group-service.ts => voucher-group-service.ts} (66%) diff --git a/src/controller/balance-controller.ts b/src/controller/balance-controller.ts index 13fdfb853..ba4287272 100644 --- a/src/controller/balance-controller.ts +++ b/src/controller/balance-controller.ts @@ -98,7 +98,7 @@ export default class BalanceController extends BaseController { * @param {boolean} hasFine.query - Only users with(out) fines * @param {integer} minFine.query - Minimum fine * @param {integer} maxFine.query - Maximum fine - * @param {string} userType[].query.enum{MEMBER,ORGAN,BORRELKAART,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE} - Filter based on user type. + * @param {string} userType[].query.enum{MEMBER,ORGAN,VOUCHER,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE} - Filter based on user type. * @param {enum} orderBy.query - Column to order balance by - eg: id,amount * @param {enum} orderDirection.query - Order direction - eg: ASC,DESC * @param {integer} take.query - How many transactions the endpoint should return diff --git a/src/controller/borrelkaart-group-controller.ts b/src/controller/borrelkaart-group-controller.ts deleted file mode 100644 index a00459bb7..000000000 --- a/src/controller/borrelkaart-group-controller.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * SudoSOS back-end API service. - * Copyright (C) 2020 Study association GEWIS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import { Response } from 'express'; -import log4js, { Logger } from 'log4js'; -import BaseController, { BaseControllerOptions } from './base-controller'; -import Policy from './policy'; -import { BorrelkaartGroupRequest } from './request/borrelkaart-group-request'; -import { RequestWithToken } from '../middleware/token-middleware'; -import BorrelkaartGroup from '../entity/user/borrelkaart-group'; -import BorrelkaartGroupService from '../service/borrelkaart-group-service'; -import { parseRequestPagination } from '../helpers/pagination'; - -export default class BorrelkaartGroupController extends BaseController { - private logger: Logger = log4js.getLogger('BorrelkaartGroupController'); - - public constructor(options: BaseControllerOptions) { - super(options); - this.logger.level = process.env.LOG_LEVEL; - } - - /** - * @inheritdoc - */ - public getPolicy(): Policy { - return { - '/': { - GET: { - policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'BorrelkaartGroup', ['*']), - handler: this.getAllBorrelkaartGroups.bind(this), - }, - POST: { - body: { modelName: 'BorrelkaartGroupRequest' }, - policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'BorrelkaartGroup', ['*']), - handler: this.createBorrelkaartGroup.bind(this), - }, - }, - '/:id(\\d+)': { - GET: { - policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'BorrelkaartGroup', ['*']), - handler: this.getBorrelkaartGroupById.bind(this), - }, - PATCH: { - body: { modelName: 'BorrelkaartGroupRequest' }, - policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'BorrelkaartGroup', ['*']), - handler: this.updateBorrelkaartGroup.bind(this), - }, - }, - }; - } - - /** - * Returns all existing borrelkaart groups - * @route GET /borrelkaartgroups - * @operationId getAllBorrelkaartgroups - * @group borrelkaartgroups - Operations of borrelkaart group controller - * @security JWT - * @param {integer} take.query - How many borrelkaart groups the endpoint should return - * @param {integer} skip.query - How many borrelkaart groups should be skipped (for pagination) - * @returns {PaginatedBorrelkaartGroupResponse.model} 200 - All existingborrelkaart - * groups without users - * @returns {string} 500 - Internal server error - */ - public async getAllBorrelkaartGroups(req: RequestWithToken, res: Response): Promise { - const { body } = req; - this.logger.trace('Get all borrelkaart groups', body, 'by user', req.token.user); - - let take; - let skip; - try { - const pagination = parseRequestPagination(req); - take = pagination.take; - skip = pagination.skip; - } catch (e) { - res.status(400).send(e.message); - return; - } - - // handle request - try { - res.json(await BorrelkaartGroupService.getBorrelkaartGroups({}, { take, skip })); - } catch (error) { - this.logger.error('Could not return all borrelkaart groups:', error); - res.status(500).json('Internal server error.'); - } - } - - /** - * Creates a new borrelkaart group - * @route POST /borrelkaartgroups - * @operationId createBorrelkaartgroup - * @group borrelkaartgroups - Operations of borrelkaart group controller - * @param {BorrelkaartGroupRequest.model} borrelkaartgroup.body.required - - * The borrelkaart group which should be created - * @security JWT - * @returns {BorrelkaartGroupResponse.model} 200 - The created borrelkaart group entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error - */ - public async createBorrelkaartGroup(req: RequestWithToken, res: Response): Promise { - const body = req.body as BorrelkaartGroupRequest; - this.logger.trace('Create borrelkaart group', body, 'by user', req.token.user); - - const borrelkaartGroupParams = BorrelkaartGroupService.asBorrelkaartGroupParams(body); - - // handle request - try { - if (!BorrelkaartGroupService.validateBorrelkaartGroup(borrelkaartGroupParams)) { - res.status(400).json('Invalid borrelkaart group.'); - return; - } - res.json(await BorrelkaartGroupService.createBorrelkaartGroup(borrelkaartGroupParams)); - } catch (error) { - this.logger.error('Could not create borrelkaart group:', error); - res.status(500).json('Internal server error.'); - } - } - - /** - * Returns the requested borrelkaart group - * @route GET /borrelkaartgroups/{id} - * @operationId getBorrelkaartgroupId - * @group borrelkaartgroups - Operations of borrelkaart group controller - * @param {integer} id.path.required - The id of the borrelkaart group which should be returned - * @security JWT - * @returns {BorrelkaartGroupResponse.model} 200 - The requested borrelkaart group entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error - */ - public async getBorrelkaartGroupById(req: RequestWithToken, res: Response): Promise { - const { id } = req.params; - const bkgId = Number.parseInt(id, 10); - this.logger.trace('Get single borrelkaart group', id, 'by user', req.token.user); - - // handle request - try { - const bkg = await BorrelkaartGroupService.getBorrelkaartGroups({ bkgId }); - if (bkg.records[0]) { - res.json(bkg.records[0]); - } else { - res.status(404).json('Borrelkaart group not found.'); - } - } catch (error) { - this.logger.error('Could not get borrelkaart group:', error); - res.status(500).json('Internal server error.'); - } - } - - /** - * Updates the requested borrelkaart group - * @route PATCH /borrelkaartgroups/{id} - * @operationId updateBorrelkaartGroup - * @group borrelkaartgroups - Operations of borrelkaart group controller - * @param {integer} id.path.required - The id of the borrelkaart group which should be updated - * @param {BorrelkaartGroupRequest.model} borrelkaartgroup.body.required - - * The updated borrelkaart group - * @security JWT - * @returns {BorrelkaartGroupResponse.model} 200 - The requested borrelkaart group entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error - */ - public async updateBorrelkaartGroup(req: RequestWithToken, res: Response): Promise { - const body = req.body as BorrelkaartGroupRequest; - const { id } = req.params; - const bkgId = Number.parseInt(id, 10); - this.logger.trace('Update borrelkaart group', id, 'with', body, 'by user', req.token.user); - - const borrelkaartGroupParams = BorrelkaartGroupService.asBorrelkaartGroupParams(body); - - // handle request - try { - if (!BorrelkaartGroupService.validateBorrelkaartGroup(borrelkaartGroupParams)) { - res.status(400).json('Invalid borrelkaart group.'); - return; - } - const bkg = await BorrelkaartGroup.findOne({ where: { id: bkgId } }); - if (!bkg) { - res.status(404).json('Borrelkaart group not found.'); - return; - } - if (bkg.activeStartDate <= new Date()) { - res.status(403).json('Borrelkaart StartDate has already passed.'); - return; - } - if (borrelkaartGroupParams.amount < bkg.amount) { - res.status(400).json('Cannot decrease number of BorrelkaartGroupUsers'); - return; - } - res.status(200).json( - await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, borrelkaartGroupParams), - ); - } catch (error) { - this.logger.error('Could not update borrelkaart group:', error); - res.status(500).json('Internal server error.'); - } - } -} diff --git a/src/controller/request/create-user-request.ts b/src/controller/request/create-user-request.ts deleted file mode 100644 index 2fde8d59a..000000000 --- a/src/controller/request/create-user-request.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * SudoSOS back-end API service. - * Copyright (C) 2020 Study association GEWIS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import { UserType } from '../../entity/user/user'; -import UpdateUserRequest from './update-user-request'; - -/** - * @typedef CreateUserRequest - * @property {string} firstName.required - * @property {string} lastName - * @property {boolean} active - * @property {number} type.required - * @property {string} email - */ -export default interface CreateUserRequest extends UpdateUserRequest { - type: UserType; -} diff --git a/src/controller/request/update-user-request.ts b/src/controller/request/user-request.ts similarity index 62% rename from src/controller/request/update-user-request.ts rename to src/controller/request/user-request.ts index d96ac75ed..4af4b6dd1 100644 --- a/src/controller/request/update-user-request.ts +++ b/src/controller/request/user-request.ts @@ -16,22 +16,43 @@ * along with this program. If not, see . */ +import { UserType } from '../../entity/user/user'; + +export default interface BaseUserRequest { + firstName: string; + lastName?: string; + nickname?: string; + canGoIntoDebt: boolean; + ofAge: boolean; + email: string; +} + +/** + * @typedef CreateUserRequest + * @property {string} firstName.required + * @property {string} lastName + * @property {string} nickname + * @property {boolean} canGoIntoDebt.required + * @property {boolean} ofAge.required + * @property {string} email.required + * @property {number} type.required + */ +export interface CreateUserRequest extends BaseUserRequest { + type: UserType; +} + /** * @typedef UpdateUserRequest * @property {string} firstName * @property {string} lastName * @property {string} nickname - * @property {boolean} active + * @property {boolean} canGoIntoDebt * @property {boolean} ofAge * @property {string} email * @property {boolean} deleted + * @property {boolean} active */ -export default interface UpdateUserRequest { - firstName?: string; - lastName?: string; - nickname?: string; +export interface UpdateUserRequest extends Partial { active?: boolean; - ofAge?: boolean; - email?: string; deleted?: boolean; } diff --git a/src/controller/request/validators/user-request-spec.ts b/src/controller/request/validators/user-request-spec.ts index 4890a49a3..8caba27f8 100644 --- a/src/controller/request/validators/user-request-spec.ts +++ b/src/controller/request/validators/user-request-spec.ts @@ -19,11 +19,10 @@ import { Specification, toFail, toPass, validateSpecification, ValidationError, } from '../../../helpers/specification-validation'; import { maxLength, nonZeroString } from './string-spec'; -import CreateUserRequest from '../create-user-request'; import { UserType } from '../../../entity/user/user'; import { INVALID_USER_TYPE } from './validation-errors'; -import UpdateUserRequest from '../update-user-request'; import validator from 'validator'; +import BaseUserRequest, { CreateUserRequest } from '../user-request'; /** * Most names cannot be '' or longer than 64. @@ -52,7 +51,7 @@ function validUserType(u: CreateUserRequest) { /** * When updating we check firstName and lastName. */ -const updateUserSpec: () => Specification = () => [ +const updateUserSpec: () => Specification = () => [ [nameSpec(), 'firstName', new ValidationError('Firstname: ')], [[validEmail], 'email', new ValidationError('E-mail: ')], [[maxLength(64)], 'lastName', new ValidationError('Lastname: ')], @@ -70,6 +69,6 @@ export async function verifyCreateUserRequest(createUserRequest: CreateUserReque return Promise.resolve(await validateSpecification(createUserRequest, createUserSpec())); } -export async function verifyUpdateUserRequest(createUserRequest: UpdateUserRequest) { +export async function verifyUpdateUserRequest(createUserRequest: CreateUserRequest) { return Promise.resolve(await validateSpecification(createUserRequest, updateUserSpec())); } diff --git a/src/controller/request/borrelkaart-group-request.ts b/src/controller/request/voucher-group-request.ts similarity index 89% rename from src/controller/request/borrelkaart-group-request.ts rename to src/controller/request/voucher-group-request.ts index 3975d5608..59f3f4cd4 100644 --- a/src/controller/request/borrelkaart-group-request.ts +++ b/src/controller/request/voucher-group-request.ts @@ -20,15 +20,15 @@ import DineroFactory from 'dinero.js'; import { DineroObjectRequest } from './dinero-request'; /** - * @typedef BorrelkaartGroupRequest + * @typedef VoucherGroupRequest * @property {string} name.required - Name of the group * @property {string} activeStartDate.required - Date from which the included cards are active * @property {string} activeEndDate.required - Date from which cards are no longer active * @property {DineroObjectRequest.model} balance.required - Start balance to be assigned - * to the borrelkaart users - * @property {number} amount.required - Amount of users to be assigned to the borrelkaart group + * to the voucher users + * @property {number} amount.required - Amount of users to be assigned to the voucher group */ -export interface BorrelkaartGroupRequest { +export interface VoucherGroupRequest { name: string, activeStartDate: string, activeEndDate: string, @@ -36,7 +36,7 @@ export interface BorrelkaartGroupRequest { amount: number, } -export interface BorrelkaartGroupParams { +export interface VoucherGroupParams { name: string, activeStartDate: Date, activeEndDate: Date, diff --git a/src/controller/response/transaction-response.ts b/src/controller/response/transaction-response.ts index 694d952c0..2aefc0f76 100644 --- a/src/controller/response/transaction-response.ts +++ b/src/controller/response/transaction-response.ts @@ -20,23 +20,23 @@ import BaseResponse from './base-response'; import { BasePointOfSaleResponse } from './point-of-sale-response'; import { BaseContainerResponse } from './container-response'; import { BaseProductResponse } from './product-response'; -import { BaseUserResponse, UserResponse } from './user-response'; +import { BaseUserResponse } from './user-response'; import { DineroObjectResponse } from './dinero-response'; import { PaginationResult } from '../../helpers/pagination'; /** * @typedef {BaseResponse} BaseTransactionResponse - * @property {UserResponse.model} from.required - The account from which the transaction + * @property {BaseUserResponse.model} from.required - The account from which the transaction * is subtracted. - * @property {UserResponse.model} createdBy - The user that created the transaction, if not + * @property {BaseUserResponse.model} createdBy - The user that created the transaction, if not * same as 'from'.. * @property {BasePointOfSaleResponse.model} pointOfSale.required - The POS at which this transaction * has been created * @property {Dinero.model} value.required - Total sum of subtransactions */ export interface BaseTransactionResponse extends BaseResponse { - from: UserResponse, - createdBy?: UserResponse, + from: BaseUserResponse, + createdBy?: BaseUserResponse, pointOfSale: BasePointOfSaleResponse, value: DineroObject, } diff --git a/src/controller/response/user-response.ts b/src/controller/response/user-response.ts index d06843d4c..30a663b31 100644 --- a/src/controller/response/user-response.ts +++ b/src/controller/response/user-response.ts @@ -41,6 +41,7 @@ export interface BaseUserResponse extends BaseResponse { * @property {boolean} extensiveDataProcessing - Whether data about this * user can be used (non-anonymously) for more data science! * @property {boolean} ofAge - Whether someone is old enough to drink beer + * @property {boolean} canGoIntoDebt.required - Whether this user can get a negative balance */ export interface UserResponse extends BaseUserResponse { active: boolean; @@ -50,6 +51,7 @@ export interface UserResponse extends BaseUserResponse { acceptedToS?: TermsOfServiceStatus, extensiveDataProcessing?: boolean; ofAge?: boolean; + canGoIntoDebt: boolean; } /** diff --git a/src/controller/response/borrelkaart-group-response.ts b/src/controller/response/voucher-group-response.ts similarity index 71% rename from src/controller/response/borrelkaart-group-response.ts rename to src/controller/response/voucher-group-response.ts index 076e436c8..134986152 100644 --- a/src/controller/response/borrelkaart-group-response.ts +++ b/src/controller/response/voucher-group-response.ts @@ -21,16 +21,16 @@ import { PaginationResult } from '../../helpers/pagination'; import { DineroObjectResponse } from './dinero-response'; /** - * @typedef {BaseResponse} BorrelkaartGroupResponse - * @property {string} name.required - Name of the borrelkaart group - * @property {string} activeStartDate - Start date of the borrelkaart group - * @property {string} activeEndDate.required - End date of the borrelkaart group - * @property {Array.} users.required - Users in the borrelkaart group + * @typedef {BaseResponse} VoucherGroupResponse + * @property {string} name.required - Name of the voucher group + * @property {string} activeStartDate - Start date of the voucher group + * @property {string} activeEndDate.required - End date of the voucher group + * @property {Array.} users.required - Users in the voucher group * @property {DineroObjectRequest.model} balance.required - Start balance to be assigned - * to the borrelkaart users - * @property {number} amount.required - Amount of users to be assigned to the borrelkaart group + * to the voucher users + * @property {number} amount.required - Amount of users to be assigned to the voucher group */ -export default interface BorrelkaartGroupResponse extends BaseResponse { +export default interface VoucherGroupResponse extends BaseResponse { name: string, activeStartDate?: string, activeEndDate: string, @@ -40,11 +40,11 @@ export default interface BorrelkaartGroupResponse extends BaseResponse { } /** - * @typedef PaginatedBorrelkaartGroupResponse + * @typedef PaginatedVoucherGroupResponse * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned borrelkaart groups + * @property {Array.} records.required - Returned voucher groups */ -export interface PaginatedBorrelkaartGroupResponse { +export interface PaginatedVoucherGroupResponse { _pagination: PaginationResult, - records: BorrelkaartGroupResponse[], + records: VoucherGroupResponse[], } diff --git a/src/controller/transaction-controller.ts b/src/controller/transaction-controller.ts index e1f65c6e7..11fcae088 100644 --- a/src/controller/transaction-controller.ts +++ b/src/controller/transaction-controller.ts @@ -157,8 +157,7 @@ export default class TransactionController extends BaseController { // verify balance if user cannot have negative balance. const user = await User.findOne({ where: { id: body.from } }); - const allowNegative = this.roleManager.can(await this.roleManager.getRoles(user), 'update', 'own', 'Balance', ['negative']); - if (!allowNegative && !await TransactionService.verifyBalance(body)) { + if (!user.canGoIntoDebt && !await TransactionService.verifyBalance(body)) { res.status(403).json('Insufficient balance.'); } else { // create the transaction diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index f36dde318..d1132549b 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -21,8 +21,7 @@ import BaseController, { BaseControllerOptions } from './base-controller'; import Policy from './policy'; import { RequestWithToken } from '../middleware/token-middleware'; import User, { UserType } from '../entity/user/user'; -import CreateUserRequest from './request/create-user-request'; -import UpdateUserRequest from './request/update-user-request'; +import BaseUserRequest, { CreateUserRequest, UpdateUserRequest } from './request/user-request'; import { parseRequestPagination } from '../helpers/pagination'; import ProductService from '../service/product-service'; import PointOfSaleService from '../service/point-of-sale-service'; @@ -302,7 +301,7 @@ export default class UserController extends BaseController { static getAttributes(req: RequestWithToken): string[] { const attributes: string[] = []; - const body = req.body as UpdateUserRequest; + const body = req.body as BaseUserRequest; for (const key in body) { if (body.hasOwnProperty(key)) { attributes.push(key); @@ -323,7 +322,7 @@ export default class UserController extends BaseController { * @param {boolean} active.query - Filter based if the user is active * @param {boolean} ofAge.query - Filter based if the user is 18+ * @param {integer} id.query - Filter based on user ID - * @param {string} type.query.enum{MEMBER,ORGAN,BORRELKAART,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE} - Filter based on user type. + * @param {string} type.query.enum{MEMBER,ORGAN,VOUCHER,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE} - Filter based on user type. * @returns {PaginatedUserResponse.model} 200 - A list of all users */ public async getAllUsers(req: RequestWithToken, res: Response): Promise { @@ -724,10 +723,10 @@ export default class UserController extends BaseController { * @operationId updateUser * @group users - Operations of user controller * @param {integer} id.path.required - The id of the user - * @param {UpdateUserRequest.model} user.body.required - + * @param {UserRequest.model} user.body.required - * The user which should be updated * @security JWT - * @returns {UpdateUserRequest.model} 200 - New user + * @returns {UserRequest.model} 200 - New user * @returns {string} 400 - Bad request */ public async updateUser(req: RequestWithToken, res: Response): Promise { diff --git a/src/controller/voucher-group-controller.ts b/src/controller/voucher-group-controller.ts new file mode 100644 index 000000000..cc04cd427 --- /dev/null +++ b/src/controller/voucher-group-controller.ts @@ -0,0 +1,212 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Response } from 'express'; +import log4js, { Logger } from 'log4js'; +import BaseController, { BaseControllerOptions } from './base-controller'; +import Policy from './policy'; +import { VoucherGroupRequest } from './request/voucher-group-request'; +import { RequestWithToken } from '../middleware/token-middleware'; +import VoucherGroup from '../entity/user/voucher-group'; +import VoucherGroupService from '../service/voucher-group-service'; +import { parseRequestPagination } from '../helpers/pagination'; + +export default class VoucherGroupController extends BaseController { + private logger: Logger = log4js.getLogger('VoucherGroupController'); + + public constructor(options: BaseControllerOptions) { + super(options); + this.logger.level = process.env.LOG_LEVEL; + } + + /** + * @inheritdoc + */ + public getPolicy(): Policy { + return { + '/': { + GET: { + policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'VoucherGroup', ['*']), + handler: this.getAllVoucherGroups.bind(this), + }, + POST: { + body: { modelName: 'VoucherGroupRequest' }, + policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'VoucherGroup', ['*']), + handler: this.createVoucherGroup.bind(this), + }, + }, + '/:id(\\d+)': { + GET: { + policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'VoucherGroup', ['*']), + handler: this.getVoucherGroupById.bind(this), + }, + PATCH: { + body: { modelName: 'VoucherGroupRequest' }, + policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'VoucherGroup', ['*']), + handler: this.updateVoucherGroup.bind(this), + }, + }, + }; + } + + /** + * Returns all existing voucher groups + * @route GET /vouchergroups + * @operationId getAllVouchergroups + * @group vouchergroups - Operations of voucher group controller + * @security JWT + * @param {integer} take.query - How many voucher groups the endpoint should return + * @param {integer} skip.query - How many voucher groups should be skipped (for pagination) + * @returns {PaginatedVoucherGroupResponse.model} 200 - All existingvoucher + * groups without users + * @returns {string} 500 - Internal server error + */ + public async getAllVoucherGroups(req: RequestWithToken, res: Response): Promise { + const { body } = req; + this.logger.trace('Get all voucher groups', body, 'by user', req.token.user); + + let take; + let skip; + try { + const pagination = parseRequestPagination(req); + take = pagination.take; + skip = pagination.skip; + } catch (e) { + res.status(400).send(e.message); + return; + } + + // handle request + try { + res.json(await VoucherGroupService.getVoucherGroups({}, { take, skip })); + } catch (error) { + this.logger.error('Could not return all voucher groups:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Creates a new voucher group + * @route POST /vouchergroups + * @operationId createVouchergroup + * @group vouchergroups - Operations of voucher group controller + * @param {VoucherGroupRequest.model} vouchergroup.body.required - + * The voucher group which should be created + * @security JWT + * @returns {VoucherGroupResponse.model} 200 - The created voucher group entity + * @returns {string} 400 - Validation error + * @returns {string} 500 - Internal server error + */ + public async createVoucherGroup(req: RequestWithToken, res: Response): Promise { + const body = req.body as VoucherGroupRequest; + this.logger.trace('Create voucher group', body, 'by user', req.token.user); + + const voucherGroupParams = VoucherGroupService.asVoucherGroupParams(body); + + // handle request + try { + if (!VoucherGroupService.validateVoucherGroup(voucherGroupParams)) { + res.status(400).json('Invalid voucher group.'); + return; + } + res.json(await VoucherGroupService.createVoucherGroup(voucherGroupParams)); + } catch (error) { + this.logger.error('Could not create voucher group:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Returns the requested voucher group + * @route GET /vouchergroups/{id} + * @operationId getVouchergroupId + * @group vouchergroups - Operations of voucher group controller + * @param {integer} id.path.required - The id of the voucher group which should be returned + * @security JWT + * @returns {VoucherGroupResponse.model} 200 - The requested voucher group entity + * @returns {string} 404 - Not found error + * @returns {string} 500 - Internal server error + */ + public async getVoucherGroupById(req: RequestWithToken, res: Response): Promise { + const { id } = req.params; + const bkgId = Number.parseInt(id, 10); + this.logger.trace('Get single voucher group', id, 'by user', req.token.user); + + // handle request + try { + const bkg = await VoucherGroupService.getVoucherGroups({ bkgId }); + if (bkg.records[0]) { + res.json(bkg.records[0]); + } else { + res.status(404).json('Voucher group not found.'); + } + } catch (error) { + this.logger.error('Could not get voucher group:', error); + res.status(500).json('Internal server error.'); + } + } + + /** + * Updates the requested voucher group + * @route PATCH /vouchergroups/{id} + * @operationId updateVoucherGroup + * @group vouchergroups - Operations of voucher group controller + * @param {integer} id.path.required - The id of the voucher group which should be updated + * @param {VoucherGroupRequest.model} vouchergroup.body.required - + * The updated voucher group + * @security JWT + * @returns {VoucherGroupResponse.model} 200 - The requested voucher group entity + * @returns {string} 400 - Validation error + * @returns {string} 404 - Not found error + * @returns {string} 500 - Internal server error + */ + public async updateVoucherGroup(req: RequestWithToken, res: Response): Promise { + const body = req.body as VoucherGroupRequest; + const { id } = req.params; + const bkgId = Number.parseInt(id, 10); + this.logger.trace('Update voucher group', id, 'with', body, 'by user', req.token.user); + + const voucherGroupParams = VoucherGroupService.asVoucherGroupParams(body); + + // handle request + try { + if (!VoucherGroupService.validateVoucherGroup(voucherGroupParams)) { + res.status(400).json('Invalid voucher group.'); + return; + } + const bkg = await VoucherGroup.findOne({ where: { id: bkgId } }); + if (!bkg) { + res.status(404).json('Voucher group not found.'); + return; + } + if (bkg.activeStartDate <= new Date()) { + res.status(403).json('Voucher StartDate has already passed.'); + return; + } + if (voucherGroupParams.amount < bkg.amount) { + res.status(400).json('Cannot decrease number of VoucherGroupUsers'); + return; + } + res.status(200).json( + await VoucherGroupService.updateVoucherGroup(bkgId, voucherGroupParams), + ); + } catch (error) { + this.logger.error('Could not update voucher group:', error); + res.status(500).json('Internal server error.'); + } + } +} diff --git a/src/database/database.ts b/src/database/database.ts index c390e290e..3775ca24e 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -27,10 +27,10 @@ import SubTransactionRow from '../entity/transactions/sub-transaction-row'; import PointOfSale from '../entity/point-of-sale/point-of-sale'; import Container from '../entity/container/container'; import FlaggedTransaction from '../entity/transactions/flagged-transaction'; -import BorrelkaartGroup from '../entity/user/borrelkaart-group'; +import VoucherGroup from '../entity/user/voucher-group'; import LocalUser from '../entity/user/local-user'; import GewisUser from '../gewis/entity/gewis-user'; -import UserBorrelkaartGroup from '../entity/user/user-borrelkaart-group'; +import UserVoucherGroup from '../entity/user/user-voucher-group'; import EanAuthenticator from '../entity/authenticator/ean-authenticator'; import MemberAuthenticator from '../entity/authenticator/member-authenticator'; import NfcAuthenticator from '../entity/authenticator/nfc-authenticator'; @@ -105,11 +105,11 @@ export default class Database { SubTransaction, SubTransactionRow, FlaggedTransaction, - BorrelkaartGroup, + VoucherGroup, User, LocalUser, GewisUser, - UserBorrelkaartGroup, + UserVoucherGroup, EanAuthenticator, MemberAuthenticator, NfcAuthenticator, diff --git a/src/entity/user/user-borrelkaart-group.ts b/src/entity/user/user-voucher-group.ts similarity index 77% rename from src/entity/user/user-borrelkaart-group.ts rename to src/entity/user/user-voucher-group.ts index a7ddd207e..09eec0cbb 100644 --- a/src/entity/user/user-borrelkaart-group.ts +++ b/src/entity/user/user-voucher-group.ts @@ -21,16 +21,16 @@ import { // eslint-disable-next-line import/no-cycle import User from './user'; // eslint-disable-next-line import/no-cycle -import BorrelkaartGroup from './borrelkaart-group'; +import VoucherGroup from './voucher-group'; /** - * @typedef {BaseEntity} UserBorrelkaartGroup + * @typedef {BaseEntity} UserVoucherGroup * @property {User.model} user.required - The user that belongs to the group. - * @property {BorrelkaartGroup.model} borrelkaartGroup.required - The borrelkaartGroup the user + * @property {VoucherGroup.model} voucherGroup.required - The voucherGroup the user * belongs to. */ @Entity() -export default class UserBorrelkaartGroup extends BaseEntity { +export default class UserVoucherGroup extends BaseEntity { @PrimaryColumn() public userId: number; @@ -38,7 +38,7 @@ export default class UserBorrelkaartGroup extends BaseEntity { @JoinColumn({ name: 'userId' }) public user: User; - @ManyToOne(() => BorrelkaartGroup, { nullable: false }) + @ManyToOne(() => VoucherGroup, { nullable: false }) @JoinColumn() - public borrelkaartGroup: BorrelkaartGroup; + public voucherGroup: VoucherGroup; } diff --git a/src/entity/user/user.ts b/src/entity/user/user.ts index 0256f8129..ba4697b6a 100644 --- a/src/entity/user/user.ts +++ b/src/entity/user/user.ts @@ -31,7 +31,7 @@ export enum TermsOfServiceStatus { export enum UserType { MEMBER = 1, ORGAN = 2, - BORRELKAART = 3, + VOUCHER = 3, LOCAL_USER = 4, LOCAL_ADMIN = 5, INVOICE = 6, @@ -58,6 +58,7 @@ export const TOSRequired = [ * @property {string} lastName - Last name of the user. * @property {string} nickname - Nickname of the user. * @property {boolean} active - Whether the user has accepted the TOS. Defaults to false. + * @property {boolean} canGoIntoDebt - Whether the user can have a negative balance. Defaults to false * @property {boolean} ofAge - Whether the user is 18+ or not. * @property {string} email - The email of the user. * @property {boolean} deleted - Whether the user was deleted. Defaults to false. @@ -87,6 +88,14 @@ export default class User extends BaseEntity { }) public active: boolean; + /** + * Whether this user can have a negative balance + */ + @Column({ + default: false, + }) + public canGoIntoDebt: boolean; + @Column({ default: false, }) diff --git a/src/entity/user/borrelkaart-group.ts b/src/entity/user/voucher-group.ts similarity index 82% rename from src/entity/user/borrelkaart-group.ts rename to src/entity/user/voucher-group.ts index 53de43d4e..7089c5325 100644 --- a/src/entity/user/borrelkaart-group.ts +++ b/src/entity/user/voucher-group.ts @@ -22,17 +22,17 @@ import { import BaseEntity from '../base-entity'; import DineroTransformer from '../transformer/dinero-transformer'; // eslint-disable-next-line import/no-cycle -import UserBorrelkaartGroup from './user-borrelkaart-group'; +import UserVoucherGroup from './user-voucher-group'; /** - * @typedef {BaseEntity} BorrelkaartGroup + * @typedef {BaseEntity} VoucherGroup * @property {string} name.required - Name of the group. * @property {string} activeStartDate.required - Date after which the included cards are active. * @property {string} activeEndDate - Date after which cards are no longer active. - * @property {Array.} borrelkaarten.required - Cards included in this group. + * @property {Array.} vouchers.required - Cards included in this group. */ @Entity() -export default class BorrelkaartGroup extends BaseEntity { +export default class VoucherGroup extends BaseEntity { @Column({ unique: true, length: 64, @@ -61,6 +61,6 @@ export default class BorrelkaartGroup extends BaseEntity { }) public balance: Dinero; - @OneToMany(() => UserBorrelkaartGroup, (user) => user.borrelkaartGroup) - public borrelkaarten: UserBorrelkaartGroup[]; + @OneToMany(() => UserVoucherGroup, (user) => user.voucherGroup) + public vouchers: UserVoucherGroup[]; } diff --git a/src/gewis/controller/gewis-authentication-controller.ts b/src/gewis/controller/gewis-authentication-controller.ts index 473e72b4f..700bc1aaf 100644 --- a/src/gewis/controller/gewis-authentication-controller.ts +++ b/src/gewis/controller/gewis-authentication-controller.ts @@ -33,7 +33,7 @@ import Gewis from '../gewis'; import User from '../../entity/user/user'; import wrapInManager from '../../helpers/database'; import UserService from '../../service/user-service'; -import UpdateUserRequest from '../../controller/request/update-user-request'; +import { UpdateUserRequest } from '../../controller/request/user-request'; /** * The GEWIS authentication controller is responsible for: diff --git a/src/gewis/gewis.ts b/src/gewis/gewis.ts index 2451f812a..144e47a34 100644 --- a/src/gewis/gewis.ts +++ b/src/gewis/gewis.ts @@ -94,7 +94,8 @@ export default class Gewis { active: true, email: token.email, ofAge: token.is_18_plus, - }) as User; + canGoIntoDebt: true, + } as User) as User; return manager.save(user).then((u) => Gewis.createGEWISUser(manager, u, token.lidnr)); } @@ -202,7 +203,7 @@ export default class Gewis { const buyerUserTypes = new Set([ UserType.LOCAL_USER, UserType.MEMBER, - UserType.BORRELKAART, + UserType.VOUCHER, UserType.INVOICE, UserType.AUTOMATIC_INVOICE, ]); @@ -340,7 +341,7 @@ export default class Gewis { update: { own: star, all: star }, delete: { own: star, all: star }, }, - BorrelkaartGroup: { + VoucherGroup: { get: { all: star }, update: { all: star }, delete: { all: star }, @@ -377,7 +378,7 @@ export default class Gewis { Banner: { ...admin, }, - BorrelkaartGroup: { + VoucherGroup: { ...admin, }, User: { diff --git a/src/helpers/revision-to-response.ts b/src/helpers/revision-to-response.ts index ce001e123..3d7c631cc 100644 --- a/src/helpers/revision-to-response.ts +++ b/src/helpers/revision-to-response.ts @@ -110,6 +110,7 @@ export function parseUserToResponse(user: User, timestamps = false): UserRespons email: user.type === UserType.LOCAL_USER ? user.email : undefined, extensiveDataProcessing: user.extensiveDataProcessing, ofAge: user.ofAge, + canGoIntoDebt: user.canGoIntoDebt, }; } @@ -128,6 +129,7 @@ export interface RawUser { type: number, acceptedToS: TermsOfServiceStatus, extensiveDataProcessing: number, + canGoIntoDebt: number, } /** @@ -150,5 +152,6 @@ export function parseRawUserToResponse(user: RawUser, timestamps = false): UserR acceptedToS: user.acceptedToS, extensiveDataProcessing: user.extensiveDataProcessing === 1, ofAge: user.ofAge === 1, + canGoIntoDebt: user.canGoIntoDebt === 1, }; } diff --git a/src/index.ts b/src/index.ts index e9a5749a2..c4da42c72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,7 @@ import UserController from './controller/user-controller'; import ProductController from './controller/product-controller'; import ProductCategoryController from './controller/product-category-controller'; import TransactionController from './controller/transaction-controller'; -import BorrelkaartGroupController from './controller/borrelkaart-group-controller'; +import VoucherGroupController from './controller/voucher-group-controller'; import BalanceService from './service/balance-service'; import BalanceController from './controller/balance-controller'; import RbacController from './controller/rbac-controller'; @@ -289,7 +289,7 @@ export default async function createApp(): Promise { application.app.use('/v1/productcategories', new ProductCategoryController(options).getRouter()); application.app.use('/v1/pointsofsale', new PointOfSaleController(options).getRouter()); application.app.use('/v1/transactions', new TransactionController(options).getRouter()); - application.app.use('/v1/borrelkaartgroups', new BorrelkaartGroupController(options).getRouter()); + application.app.use('/v1/vouchergroups', new VoucherGroupController(options).getRouter()); application.app.use('/v1/transfers', new TransferController(options).getRouter()); application.app.use('/v1/fines', new DebtorController(options).getRouter()); application.app.use('/v1/stripe', new StripeController(options).getRouter()); diff --git a/src/service/authentication-service.ts b/src/service/authentication-service.ts index 6f634bc35..0f706a6d0 100644 --- a/src/service/authentication-service.ts +++ b/src/service/authentication-service.ts @@ -163,7 +163,9 @@ export default class AuthenticationService { lastName: ADUser.sn, type: UserType.MEMBER, active: true, - }) as User; + canGoIntoDebt: true, + ofAge: false, + } as User) as User; account = await manager.save(account); const auth = await bindUser(manager, ADUser, account); diff --git a/src/service/transaction-service.ts b/src/service/transaction-service.ts index 07584dc1a..5fc0acbb4 100644 --- a/src/service/transaction-service.ts +++ b/src/service/transaction-service.ts @@ -612,9 +612,6 @@ export default class TransactionService { updatedAt: new Date(o.from_updatedAt).toISOString(), firstName: o.from_firstName, lastName: o.from_lastName, - active: o.from_active === 1, - deleted: o.from_deleted === 1, - type: UserType[o.from_type], }, createdBy: o.createdBy_id ? { id: o.createdBy_id, @@ -622,9 +619,6 @@ export default class TransactionService { updatedAt: new Date(o.createdBy_updatedAt).toISOString(), firstName: o.createdBy_firstName, lastName: o.createdBy_lastName, - active: o.createdBy_active === 1, - deleted: o.createdBy_deleted === 1, - type: UserType[o.createdBy_type], } : undefined, pointOfSale: { id: o.pointOfSale_id, diff --git a/src/service/user-service.ts b/src/service/user-service.ts index 6a05e6cd4..8caa94c3e 100644 --- a/src/service/user-service.ts +++ b/src/service/user-service.ts @@ -21,9 +21,8 @@ import { PaginationParameters } from '../helpers/pagination'; import { PaginatedUserResponse, UserResponse } from '../controller/response/user-response'; import QueryFilter, { FilterMapping } from '../helpers/query-filter'; import User, { LocalUserTypes, TermsOfServiceStatus, TOSRequired, UserType } from '../entity/user/user'; -import CreateUserRequest from '../controller/request/create-user-request'; import MemberAuthenticator from '../entity/authenticator/member-authenticator'; -import UpdateUserRequest from '../controller/request/update-user-request'; +import { CreateUserRequest, UpdateUserRequest } from '../controller/request/user-request'; import TransactionService from './transaction-service'; import { FinancialMutationResponse, diff --git a/src/service/borrelkaart-group-service.ts b/src/service/voucher-group-service.ts similarity index 52% rename from src/service/borrelkaart-group-service.ts rename to src/service/voucher-group-service.ts index 873176058..c261cdc79 100644 --- a/src/service/borrelkaart-group-service.ts +++ b/src/service/voucher-group-service.ts @@ -17,31 +17,31 @@ */ import { FindManyOptions } from 'typeorm'; import DineroFactory from 'dinero.js'; -import { BorrelkaartGroupParams, BorrelkaartGroupRequest } from '../controller/request/borrelkaart-group-request'; -import BorrelkaartGroupResponse, { - PaginatedBorrelkaartGroupResponse, -} from '../controller/response/borrelkaart-group-response'; +import { VoucherGroupParams, VoucherGroupRequest } from '../controller/request/voucher-group-request'; +import VoucherGroupResponse, { + PaginatedVoucherGroupResponse, +} from '../controller/response/voucher-group-response'; import { UserResponse } from '../controller/response/user-response'; import Transfer from '../entity/transactions/transfer'; import DineroTransformer from '../entity/transformer/dinero-transformer'; -import BorrelkaartGroup from '../entity/user/borrelkaart-group'; -import User, { UserType } from '../entity/user/user'; -import UserBorrelkaartGroup from '../entity/user/user-borrelkaart-group'; +import VoucherGroup from '../entity/user/voucher-group'; +import User, { TermsOfServiceStatus, UserType } from '../entity/user/user'; +import UserVoucherGroup from '../entity/user/user-voucher-group'; import { PaginationParameters } from '../helpers/pagination'; import QueryFilter, { FilterMapping } from '../helpers/query-filter'; import { parseUserToResponse } from '../helpers/revision-to-response'; -export interface BorrelkaartGroupFilterParameters { +export interface VoucherGroupFilterParameters { bkgId?: number, } -export default class BorrelkaartGroupService { +export default class VoucherGroupService { /** - * Verifies whether the borrelkaart group request translates to a valid object - * @returns {BorrelkaartGroupParams.model} The parameter object from the request + * Verifies whether the voucher group request translates to a valid object + * @returns {VoucherGroupParams.model} The parameter object from the request * @param req */ - static asBorrelkaartGroupParams(req: BorrelkaartGroupRequest): BorrelkaartGroupParams { + static asVoucherGroupParams(req: VoucherGroupRequest): VoucherGroupParams { const startDate = new Date(req.activeStartDate); startDate.setHours(0, 0, 0, 0); const endDate = new Date(req.activeEndDate); @@ -55,11 +55,11 @@ export default class BorrelkaartGroupService { } /** - * Verifies whether the borrelkaart group request translates to a valid object - * @param {BorrelkaartGroupParams.model} bkgReq - The borrelkaart group request - * @returns {boolean} whether the borrelkaart group is ok + * Verifies whether the voucher group request translates to a valid object + * @param {VoucherGroupParams.model} bkgReq - The voucher group request + * @returns {boolean} whether the voucher group is ok */ - static validateBorrelkaartGroup(bkgReq: BorrelkaartGroupParams): boolean { + static validateVoucherGroup(bkgReq: VoucherGroupParams): boolean { return bkgReq.name !== '' && bkgReq.activeEndDate instanceof Date && bkgReq.activeStartDate instanceof Date @@ -71,7 +71,7 @@ export default class BorrelkaartGroupService { && bkgReq.activeEndDate.getTime() > bkgReq.activeStartDate.getTime() && bkgReq.balance.isPositive() && !bkgReq.balance.isZero() - // borrelkaart group must contain users + // voucher group must contain users && bkgReq.amount > 0; } @@ -85,10 +85,10 @@ export default class BorrelkaartGroupService { return Transfer.save(transfers); } - static asBorrelkaartGroup( - bkgReq: BorrelkaartGroupParams, - ): BorrelkaartGroup { - return Object.assign(new BorrelkaartGroup(), { + static asVoucherGroup( + bkgReq: VoucherGroupParams, + ): VoucherGroup { + return Object.assign(new VoucherGroup(), { name: bkgReq.name, activeStartDate: bkgReq.activeStartDate, activeEndDate: bkgReq.activeEndDate, @@ -98,15 +98,15 @@ export default class BorrelkaartGroupService { } /** - * Creates a borrelkaart group from the request - * @param {BorrelkaartGroup.model} bkg - borrelkaart group - * @param {Array.} users - users in the borrelkaart group - * @returns {BorrelkaartGroupResponse.model} a borrelkaart group response + * Creates a voucher group from the request + * @param {VoucherGroup.model} bkg - voucher group + * @param {Array.} users - users in the voucher group + * @returns {VoucherGroupResponse.model} a voucher group response */ - public static asBorrelkaartGroupResponse( - bkg: BorrelkaartGroup, + public static asVoucherGroupResponse( + bkg: VoucherGroup, users: User[], - ): BorrelkaartGroupResponse | undefined { + ): VoucherGroupResponse | undefined { // parse users to user responses if users in request const userResponses: UserResponse[] = []; if (users) { @@ -115,7 +115,7 @@ export default class BorrelkaartGroupService { }); } - // return as borrelkaart group response + // return as voucher group response return { id: bkg.id, amount: bkg.amount, @@ -130,14 +130,14 @@ export default class BorrelkaartGroupService { } /** - * Returns all borrelkaart groups without users + * Returns all voucher groups without users * @param filters * @param {PaginationParameters.model} params - find options - * @returns {PaginatedBorrelkaartGroupResponse} borrelkaart groups without users + * @returns {PaginatedVoucherGroupResponse} voucher groups without users */ - public static async getBorrelkaartGroups( - filters: BorrelkaartGroupFilterParameters, params: PaginationParameters = {}, - ): Promise { + public static async getVoucherGroups( + filters: VoucherGroupFilterParameters, params: PaginationParameters = {}, + ): Promise { const { take, skip } = params; const mapping: FilterMapping = { @@ -146,47 +146,47 @@ export default class BorrelkaartGroupService { const options: FindManyOptions = { where: QueryFilter.createFilterWhereClause(mapping, filters), - relations: ['borrelkaarten.user'], + relations: ['vouchers.user'], }; - const bkgs: BorrelkaartGroup[] = await BorrelkaartGroup.find({ ...options, take, skip }); - const records = bkgs.map((bkg) => this.asBorrelkaartGroupResponse(bkg, bkg.borrelkaarten.map((borrelkaart) => borrelkaart.user))); + const bkgs: VoucherGroup[] = await VoucherGroup.find({ ...options, take, skip }); + const records = bkgs.map((bkg) => this.asVoucherGroupResponse(bkg, bkg.vouchers.map((voucher) => voucher.user))); return { _pagination: { take, skip, - count: await BorrelkaartGroup.count(), + count: await VoucherGroup.count(), }, records, }; } /** - * Saves a borrelkaart group and its user relations to the database - * @param {BorrelkaartGroupRequest.model} bkgReq - borrelkaart group request - * @returns {BorrelkaartGroupResponse.model} saved borrelkaart group + * Saves a voucher group and its user relations to the database + * @param {VoucherGroupRequest.model} bkgReq - voucher group request + * @returns {VoucherGroupResponse.model} saved voucher group */ - public static async createBorrelkaartGroup( - bkgReq: BorrelkaartGroupParams, - ): Promise { - const users = await BorrelkaartGroupService.createBorrelkaartUsers(bkgReq.name, bkgReq.activeStartDate <= new Date(), bkgReq.amount); + public static async createVoucherGroup( + bkgReq: VoucherGroupParams, + ): Promise { + const users = await VoucherGroupService.createVoucherUsers(bkgReq.name, bkgReq.activeStartDate <= new Date(), bkgReq.amount); - // save the borrelkaart group - const bkg = await BorrelkaartGroup.save(this.asBorrelkaartGroup(bkgReq)); + // save the voucher group + const bkg = await VoucherGroup.save(this.asVoucherGroup(bkgReq)); - // create and save user borrelkaart group links + // create and save user voucher group links const userLinks = users.map( - (user) => ({ user, borrelkaartGroup: bkg } as UserBorrelkaartGroup), + (user) => ({ user, voucherGroup: bkg } as UserVoucherGroup), ); - await UserBorrelkaartGroup.save(userLinks); + await UserVoucherGroup.save(userLinks); await this.updateBalance(users, bkgReq.balance); - // return borrelkaart group response with posted borrelkaart group - return this.asBorrelkaartGroupResponse(bkg, users); + // return voucher group response with posted voucher group + return this.asVoucherGroupResponse(bkg, users); } - public static async createBorrelkaartUsers(namePrefix: string, active: Boolean, amount: number, offset: number = 0): Promise { + public static async createVoucherUsers(namePrefix: string, active: Boolean, amount: number, offset: number = 0): Promise { const userObjects = []; for (let i = offset; i < amount; i += 1) { const firstName = `${namePrefix}_${i}`; @@ -194,40 +194,41 @@ export default class BorrelkaartGroupService { Object.assign(new User(), { firstName, active: active, - type: UserType.BORRELKAART, + type: UserType.VOUCHER, ofAge: true, - }), + acceptedToS: TermsOfServiceStatus.NOT_REQUIRED, + } as User), ); } - // create borrelkaart users + // create voucher users return User.save(userObjects); } /** - * Updates a borrelkaart group and its user relations in the database - * @param {string} id - requested borrelkaart group id - * @param {BorrelkaartGroupRequest.model} bkgReq - new borrelkaart group request - * @returns {BorrelkaartGroupResponse.model} updated borrelkaart group - * @returns {undefined} undefined when borrelkaart group not found + * Updates a voucher group and its user relations in the database + * @param {string} id - requested voucher group id + * @param {VoucherGroupRequest.model} bkgReq - new voucher group request + * @returns {VoucherGroupResponse.model} updated voucher group + * @returns {undefined} undefined when voucher group not found */ - public static async updateBorrelkaartGroup( + public static async updateVoucherGroup( id: number, - bkgReq: BorrelkaartGroupParams, - ): Promise { - // current borrelkaart group - const bkgCurrent = await BorrelkaartGroup.findOne({ where: { id } }); + bkgReq: VoucherGroupParams, + ): Promise { + // current voucher group + const bkgCurrent = await VoucherGroup.findOne({ where: { id } }); if (!bkgCurrent) { return undefined; } - // create new borrelkaart group and update database - await BorrelkaartGroup.update(id, this.asBorrelkaartGroup(bkgReq)); - const bkg = await BorrelkaartGroup.findOne({ where: { id } }); + // create new voucher group and update database + await VoucherGroup.update(id, this.asVoucherGroup(bkgReq)); + const bkg = await VoucherGroup.findOne({ where: { id } }); let usersCurrent = ( - await UserBorrelkaartGroup.find({ + await UserVoucherGroup.find({ relations: ['user'], - where: { borrelkaartGroup: { id } }, + where: { voucherGroup: { id } }, }) ).map((ubkg) => ubkg.user); @@ -246,18 +247,18 @@ export default class BorrelkaartGroupService { } if (bkgCurrent.amount < bkgReq.amount) { - const users = await this.createBorrelkaartUsers(bkgReq.name, bkgReq.activeStartDate <= new Date(), bkgReq.amount, bkgCurrent.amount); + const users = await this.createVoucherUsers(bkgReq.name, bkgReq.activeStartDate <= new Date(), bkgReq.amount, bkgCurrent.amount); // save new user relations const userLinks = users.map( - (user) => ({ user, borrelkaartGroup: bkg } as UserBorrelkaartGroup), + (user) => ({ user, voucherGroup: bkg } as UserVoucherGroup), ); - await UserBorrelkaartGroup.save(userLinks); + await UserVoucherGroup.save(userLinks); await this.updateBalance(users, bkgReq.balance); usersCurrent.push(...users); } - // return created borrelkaart group with users - return this.asBorrelkaartGroupResponse(bkg, usersCurrent); + // return created voucher group with users + return this.asVoucherGroupResponse(bkg, usersCurrent); } } diff --git a/test/helpers/user-factory.ts b/test/helpers/user-factory.ts index 14a8bf616..e7709621d 100644 --- a/test/helpers/user-factory.ts +++ b/test/helpers/user-factory.ts @@ -29,6 +29,7 @@ export class Builder { type: UserType.MEMBER, active: true, acceptedToS: TermsOfServiceStatus.ACCEPTED, + canGoIntoDebt: true, } as User); await User.save(this.user); return this; diff --git a/test/unit/controller/borrelkaart-group-controller.ts b/test/unit/controller/borrelkaart-group-controller.ts index 435e53daf..b23a1d70b 100644 --- a/test/unit/controller/borrelkaart-group-controller.ts +++ b/test/unit/controller/borrelkaart-group-controller.ts @@ -21,36 +21,36 @@ import express, { Application } from 'express'; import { SwaggerSpecification } from 'swagger-model-validator'; import { Connection } from 'typeorm'; import TokenHandler from '../../../src/authentication/token-handler'; -import BorrelkaartGroupController from '../../../src/controller/borrelkaart-group-controller'; -import { BorrelkaartGroupRequest } from '../../../src/controller/request/borrelkaart-group-request'; -import BorrelkaartGroupResponse from '../../../src/controller/response/borrelkaart-group-response'; +import VoucherGroupController from '../../../src/controller/voucher-group-controller'; +import { VoucherGroupRequest } from '../../../src/controller/request/voucher-group-request'; +import VoucherGroupResponse from '../../../src/controller/response/voucher-group-response'; import Database from '../../../src/database/database'; -import BorrelkaartGroup from '../../../src/entity/user/borrelkaart-group'; +import VoucherGroup from '../../../src/entity/user/voucher-group'; import User, { TermsOfServiceStatus, UserType, } from '../../../src/entity/user/user'; -import UserBorrelkaartGroup from '../../../src/entity/user/user-borrelkaart-group'; +import UserVoucherGroup from '../../../src/entity/user/user-voucher-group'; import TokenMiddleware from '../../../src/middleware/token-middleware'; import RoleManager from '../../../src/rbac/role-manager'; -import BorrelkaartGroupService from '../../../src/service/borrelkaart-group-service'; +import VoucherGroupService from '../../../src/service/voucher-group-service'; import Swagger from '../../../src/start/swagger'; import { defaultPagination, PaginationResult, } from '../../../src/helpers/pagination'; -import { bkgEq } from '../service/borrelkaart-group-service'; +import { bkgEq } from '../service/voucher-group-service'; import Sinon from 'sinon'; import { DineroObjectRequest } from '../../../src/controller/request/dinero-request'; async function saveBKG( - bkgReq: BorrelkaartGroupRequest, -): Promise { - // save borrelkaart group - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(bkgReq); - const bkgParams = BorrelkaartGroupService.asBorrelkaartGroup(params); - const bkg = await BorrelkaartGroup.save(bkgParams); - const users = await BorrelkaartGroupService.createBorrelkaartUsers( + bkgReq: VoucherGroupRequest, +): Promise { + // save voucher group + const params = VoucherGroupService.asVoucherGroupParams(bkgReq); + const bkgParams = VoucherGroupService.asVoucherGroup(params); + const bkg = await VoucherGroup.save(bkgParams); + const users = await VoucherGroupService.createVoucherUsers( bkgParams.name, bkgParams.activeStartDate <= new Date(), bkgParams.amount, @@ -58,26 +58,26 @@ async function saveBKG( // save new user relations const userLinks = users.map( - (user) => ({ user, borrelkaartGroup: bkg } as UserBorrelkaartGroup), + (user) => ({ user, voucherGroup: bkg } as UserVoucherGroup), ); - await UserBorrelkaartGroup.save(userLinks); + await UserVoucherGroup.save(userLinks); - return BorrelkaartGroupService.asBorrelkaartGroupResponse(bkgParams, users); + return VoucherGroupService.asVoucherGroupResponse(bkgParams, users); } -describe('BorrelkaartGroupController', async (): Promise => { +describe('VoucherGroupController', async (): Promise => { let ctx: { connection: Connection; clock: Sinon.SinonFakeTimers, app: Application; specification: SwaggerSpecification; - controller: BorrelkaartGroupController; + controller: VoucherGroupController; adminUser: User; localUser: User; adminToken: String; token: String; - validBorrelkaartGroupReq: BorrelkaartGroupRequest; - invalidBorrelkaartGroupReq: BorrelkaartGroupRequest; + validVoucherGroupReq: VoucherGroupRequest; + invalidVoucherGroupReq: VoucherGroupRequest; }; // initialize context @@ -131,8 +131,8 @@ describe('BorrelkaartGroupController', async (): Promise => { 'nonce', ); - // test borrelkaart groups - const validBorrelkaartGroupReq: BorrelkaartGroupRequest = { + // test voucher groups + const validVoucherGroupReq: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -144,8 +144,8 @@ describe('BorrelkaartGroupController', async (): Promise => { amount: 4, }; - const invalidBorrelkaartGroupReq: BorrelkaartGroupRequest = { - ...validBorrelkaartGroupReq, + const invalidVoucherGroupReq: VoucherGroupRequest = { + ...validVoucherGroupReq, name: '', }; @@ -158,7 +158,7 @@ describe('BorrelkaartGroupController', async (): Promise => { roleManager.registerRole({ name: 'Admin', permissions: { - BorrelkaartGroup: { + VoucherGroup: { create: all, get: all, update: all, @@ -168,7 +168,7 @@ describe('BorrelkaartGroupController', async (): Promise => { assignmentCheck: async (user: User) => user.type === UserType.LOCAL_ADMIN, }); - const controller = new BorrelkaartGroupController({ + const controller = new VoucherGroupController({ specification, roleManager, }); @@ -176,7 +176,7 @@ describe('BorrelkaartGroupController', async (): Promise => { app.use( new TokenMiddleware({ tokenHandler, refreshFactor: 0.5 }).getMiddleware(), ); - app.use('/borrelkaartgroups', controller.getRouter()); + app.use('/vouchergroups', controller.getRouter()); // initialize context ctx = { @@ -189,8 +189,8 @@ describe('BorrelkaartGroupController', async (): Promise => { localUser, adminToken, token, - validBorrelkaartGroupReq, - invalidBorrelkaartGroupReq, + validVoucherGroupReq, + invalidVoucherGroupReq, }; }); @@ -201,56 +201,56 @@ describe('BorrelkaartGroupController', async (): Promise => { ctx.clock.restore(); }); - describe('GET /borrelkaartgroups', () => { + describe('GET /vouchergroups', () => { it('should return correct model', async () => { - // save borrelkaart group - await saveBKG(ctx.validBorrelkaartGroupReq); + // save voucher group + await saveBKG(ctx.validVoucherGroupReq); - // get borrelkaart groups + // get voucher groups const res = await request(ctx.app) - .get('/borrelkaartgroups') + .get('/vouchergroups') .set('Authorization', `Bearer ${ctx.adminToken}`); expect(res.status).to.equal(200); expect( ctx.specification.validateModel( - 'PaginatedBorrelkaartGroupResponse', + 'PaginatedVoucherGroupResponse', res.body, false, true, ).valid, ).to.be.true; }); - it('should return an HTTP 200 and all borrelkaart groups without users in the database if admin', async () => { - // save borrelkaart group - await saveBKG(ctx.validBorrelkaartGroupReq); + it('should return an HTTP 200 and all voucher groups without users in the database if admin', async () => { + // save voucher group + await saveBKG(ctx.validVoucherGroupReq); - // get borrelkaart groups + // get voucher groups const res = await request(ctx.app) - .get('/borrelkaartgroups') + .get('/vouchergroups') .set('Authorization', `Bearer ${ctx.adminToken}`); - // check if borrelkaart groups are returned without users - const borrelkaartGroups = res.body.records as BorrelkaartGroup[]; + // check if voucher groups are returned without users + const voucherGroups = res.body.records as VoucherGroup[]; // eslint-disable-next-line no-underscore-dangle const pagination = res.body._pagination as PaginationResult; expect( - borrelkaartGroups.length, + voucherGroups.length, 'size of response not equal to size of database', - ).to.equal(await BorrelkaartGroup.count()); + ).to.equal(await VoucherGroup.count()); // success code expect(res.status, 'incorrect status on get').to.equal(200); expect(pagination.take).to.equal(defaultPagination()); expect(pagination.skip).to.equal(0); - expect(pagination.count).to.equal(await BorrelkaartGroup.count()); + expect(pagination.count).to.equal(await VoucherGroup.count()); }); it('should return an HTTP 403 if not admin', async () => { - // save borrelkaart group - await saveBKG(ctx.validBorrelkaartGroupReq); + // save voucher group + await saveBKG(ctx.validVoucherGroupReq); const res = await request(ctx.app) - .get('/borrelkaartgroups') + .get('/vouchergroups') .set('Authorization', `Bearer ${ctx.token}`); // check no response body @@ -261,56 +261,56 @@ describe('BorrelkaartGroupController', async (): Promise => { }); }); - describe('POST /borrelkaartgroups', () => { + describe('POST /vouchergroups', () => { it('should return correct model', async () => { - // post borrelkaart group + // post voucher group const res = await request(ctx.app) - .post('/borrelkaartgroups') + .post('/vouchergroups') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(ctx.validBorrelkaartGroupReq); + .send(ctx.validVoucherGroupReq); expect(res.status).to.equal(200); const validation = ctx.specification.validateModel( - 'BorrelkaartGroupResponse', + 'VoucherGroupResponse', res.body, false, true, ); expect(validation.valid).to.be.true; }); - it('should store the given borrelkaart group and its users in the database and return an HTTP 200 and the borrelkaart group with users if admin', async () => { - // post borrelkaart group + it('should store the given voucher group and its users in the database and return an HTTP 200 and the voucher group with users if admin', async () => { + // post voucher group const res = await request(ctx.app) - .post('/borrelkaartgroups') + .post('/vouchergroups') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(ctx.validBorrelkaartGroupReq); + .send(ctx.validVoucherGroupReq); // success code expect(res.status, 'status incorrect on valid post').to.equal(200); - bkgEq(BorrelkaartGroupService.asBorrelkaartGroupParams(ctx.validBorrelkaartGroupReq), res.body); + bkgEq(VoucherGroupService.asVoucherGroupParams(ctx.validVoucherGroupReq), res.body); }); - it('should return an HTTP 400 if the given borrelkaart group is invalid', async () => { - // post invalid borrelkaart group + it('should return an HTTP 400 if the given voucher group is invalid', async () => { + // post invalid voucher group const res = await request(ctx.app) - .post('/borrelkaartgroups') + .post('/vouchergroups') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(ctx.invalidBorrelkaartGroupReq); + .send(ctx.invalidVoucherGroupReq); - // invalid borrelkaart group response response - expect(res.body, 'borrelkaart group not invalidated').to.equal( - 'Invalid borrelkaart group.', + // invalid voucher group response response + expect(res.body, 'voucher group not invalidated').to.equal( + 'Invalid voucher group.', ); // invalid code expect(res.status, 'status incorrect on invalid post').to.equal(400); }); it('should return an HTTP 403 if not admin', async () => { - // post borrelkaart group + // post voucher group const res = await request(ctx.app) - .post('/borrelkaartgroups') + .post('/vouchergroups') .set('Authorization', `Bearer ${ctx.token}`) - .send(ctx.validBorrelkaartGroupReq); + .send(ctx.validVoucherGroupReq); // forbidden code expect(res.status, 'status incorrect on forbidden post').to.equal(403); @@ -320,46 +320,46 @@ describe('BorrelkaartGroupController', async (): Promise => { }); }); - describe('GET /borrelkaartgroups/:id', () => { + describe('GET /vouchergroups/:id', () => { it('should return correct model', async () => { - // save borrelkaart group - await saveBKG(ctx.validBorrelkaartGroupReq); - // get borrelkaart group by id + // save voucher group + await saveBKG(ctx.validVoucherGroupReq); + // get voucher group by id const res = await request(ctx.app) - .get('/borrelkaartgroups/1') + .get('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.adminToken}`); expect(res.status).to.equal(200); const validation = ctx.specification.validateModel( - 'BorrelkaartGroupResponse', + 'VoucherGroupResponse', res.body, false, true, ); expect(validation.valid).to.be.true; }); - it('should return an HTTP 200 and the borrelkaart group and users with given id if admin', async () => { - // save borrelkaart group - await saveBKG(ctx.validBorrelkaartGroupReq); + it('should return an HTTP 200 and the voucher group and users with given id if admin', async () => { + // save voucher group + await saveBKG(ctx.validVoucherGroupReq); - // get borrelkaart group by id + // get voucher group by id const res = await request(ctx.app) - .get('/borrelkaartgroups/1') + .get('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.adminToken}`); // success code expect(res.status).to.equal(200); - const bkgRes = res.body as BorrelkaartGroupResponse; + const bkgRes = res.body as VoucherGroupResponse; - expect(bkgRes, 'borrelkaart group not found').to.not.be.empty; - bkgEq(BorrelkaartGroupService.asBorrelkaartGroupParams(ctx.validBorrelkaartGroupReq), bkgRes); + expect(bkgRes, 'voucher group not found').to.not.be.empty; + bkgEq(VoucherGroupService.asVoucherGroupParams(ctx.validVoucherGroupReq), bkgRes); }); - it('should return an HTTP 404 if the borrelkaart group with given id does not exist', async () => { - // get borrelkaart group by id + it('should return an HTTP 404 if the voucher group with given id does not exist', async () => { + // get voucher group by id const res = await request(ctx.app) - .get('/borrelkaartgroups/1') + .get('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.adminToken}`); // not found code @@ -367,102 +367,102 @@ describe('BorrelkaartGroupController', async (): Promise => { expect( res.body, - 'borrelkaart group found while id not in database', - ).to.equal('Borrelkaart group not found.'); + 'voucher group found while id not in database', + ).to.equal('Voucher group not found.'); }); it('should return an HTTP 403 if not admin', async () => { - // save borrelkaart group - await saveBKG(ctx.validBorrelkaartGroupReq); + // save voucher group + await saveBKG(ctx.validVoucherGroupReq); - // get borrelkaart group by id + // get voucher group by id const res = await request(ctx.app) - .get('/borrelkaartgroups/1') + .get('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.token}`); // forbidden code expect(res.status).to.equal(403); - const bkgRes = res.body as BorrelkaartGroupResponse; + const bkgRes = res.body as VoucherGroupResponse; - expect(bkgRes, 'borrelkaart group returned').to.be.empty; + expect(bkgRes, 'voucher group returned').to.be.empty; }); }); - describe('PATCH /borrelkaartgroups/:id', () => { - it('should update and return an HTTP 200 and the borrelkaart group and users if admin', async () => { - await saveBKG(ctx.validBorrelkaartGroupReq); + describe('PATCH /vouchergroups/:id', () => { + it('should update and return an HTTP 200 and the voucher group and users if admin', async () => { + await saveBKG(ctx.validVoucherGroupReq); - // update borrelkaart group by id + // update voucher group by id const res = await request(ctx.app) - .patch('/borrelkaartgroups/1') + .patch('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(ctx.validBorrelkaartGroupReq); + .send(ctx.validVoucherGroupReq); // success code expect(res.status).to.equal(200); expect( ctx.specification.validateModel( - 'BorrelkaartGroupResponse', + 'VoucherGroupResponse', res.body, false, true, ).valid, ).to.be.true; - // check returned borrelkaart group + // check returned voucher group bkgEq( - BorrelkaartGroupService.asBorrelkaartGroupParams(ctx.validBorrelkaartGroupReq), - res.body as BorrelkaartGroupResponse, + VoucherGroupService.asVoucherGroupParams(ctx.validVoucherGroupReq), + res.body as VoucherGroupResponse, ); }); - it('should return an HTTP 400 if given borrelkaart group is invalid', async () => { - await saveBKG(ctx.validBorrelkaartGroupReq); + it('should return an HTTP 400 if given voucher group is invalid', async () => { + await saveBKG(ctx.validVoucherGroupReq); - // update borrelkaart group by id + // update voucher group by id const res = await request(ctx.app) - .patch('/borrelkaartgroups/1') + .patch('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(ctx.invalidBorrelkaartGroupReq); + .send(ctx.invalidVoucherGroupReq); // invalid code expect(res.status).to.equal(400); // check empty body - expect(res.body, 'borrelkaart group not invalidated').to.equal( - 'Invalid borrelkaart group.', + expect(res.body, 'voucher group not invalidated').to.equal( + 'Invalid voucher group.', ); }); - it('should return an HTTP 404 if the borrelkaart group with given id does not exist', async () => { - // patch borrelkaart by id + it('should return an HTTP 404 if the voucher group with given id does not exist', async () => { + // patch voucher by id const res = await request(ctx.app) - .patch('/borrelkaartgroups/1') + .patch('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(ctx.validBorrelkaartGroupReq); + .send(ctx.validVoucherGroupReq); // not found code expect(res.status).to.equal(404); expect( res.body, - 'borrelkaart group found while id not in database', - ).to.equal('Borrelkaart group not found.'); + 'voucher group found while id not in database', + ).to.equal('Voucher group not found.'); }); it('should return an HTTP 403 if not admin', async () => { - await saveBKG(ctx.validBorrelkaartGroupReq); + await saveBKG(ctx.validVoucherGroupReq); - // update borrelkaart group by id + // update voucher group by id const res = await request(ctx.app) - .patch('/borrelkaartgroups/1') + .patch('/vouchergroups/1') .set('Authorization', `Bearer ${ctx.token}`) - .send(ctx.validBorrelkaartGroupReq); + .send(ctx.validVoucherGroupReq); // forbidden code expect(res.status).to.equal(403); // check empty body - expect(res.body, 'returned a borrelkaart group').to.be.empty; + expect(res.body, 'returned a voucher group').to.be.empty; }); }); }); diff --git a/test/unit/controller/transaction-controller.ts b/test/unit/controller/transaction-controller.ts index ce3573e8f..b51bc6f85 100644 --- a/test/unit/controller/transaction-controller.ts +++ b/test/unit/controller/transaction-controller.ts @@ -669,23 +669,24 @@ describe('TransactionController', (): void => { .send(badReq); expect(res.status).to.equal(400); }); - it('should return an HTTP 403 if the user is a borrelkaart and has insufficient balance', async () => { - // create borrelkaart user + it('should return an HTTP 403 if the user has insufficient balance and cannot go into debt', async () => { + // create voucher user await User.save({ - firstName: 'borrelkaart', - lastName: 'borrelkaart', + firstName: 'voucher', + lastName: 'voucher', active: true, deleted: false, type: 3, acceptedToS: TermsOfServiceStatus.NOT_REQUIRED, + canGoIntoDebt: false, } as User); - const borrelkaartUser = await User.findOne({ + const voucherUser = await User.findOne({ where: { active: true, deleted: false, type: 3 }, }); const badReq = { ...ctx.validTransReq, - from: borrelkaartUser.id, + from: voucherUser.id, } as TransactionRequest; const res = await request(ctx.app) @@ -693,6 +694,7 @@ describe('TransactionController', (): void => { .set('Authorization', `Bearer ${ctx.adminToken}`) .send(badReq); expect(res.status).to.equal(403); + expect(res.body).to.equal('Insufficient balance.'); }); }); diff --git a/test/unit/controller/user-controller.ts b/test/unit/controller/user-controller.ts index 514e06f66..299c8a104 100644 --- a/test/unit/controller/user-controller.ts +++ b/test/unit/controller/user-controller.ts @@ -56,7 +56,7 @@ import RoleResponse from '../../../src/controller/response/rbac/role-response'; import { FinancialMutationResponse } from '../../../src/controller/response/financial-mutation-response'; import UpdateLocalRequest from '../../../src/controller/request/update-local-request'; import { AcceptTosRequest } from '../../../src/controller/request/accept-tos-request'; -import UpdateUserRequest from '../../../src/controller/request/update-user-request'; +import { CreateUserRequest, UpdateUserRequest } from '../../../src/controller/request/user-request'; import StripeDeposit from '../../../src/entity/deposit/stripe-deposit'; import { StripeDepositResponse } from '../../../src/controller/response/stripe-response'; import { TransactionReportResponse } from '../../../src/controller/response/transaction-report-response'; @@ -77,7 +77,7 @@ describe('UserController', (): void => { adminToken: string, organMemberToken: string, deletedUser: User, - user: User, + user: CreateUserRequest, organ: User, tokenHandler: TokenHandler, users: User[], @@ -115,7 +115,9 @@ describe('UserController', (): void => { lastName: 'Kakkenberg', type: UserType.MEMBER, email: 'spam@gewis.nl', - } as any as User, + canGoIntoDebt: true, + ofAge: true, + } as CreateUserRequest, ...database, }; const deletedUser = Object.assign(new User(), { @@ -378,11 +380,13 @@ describe('UserController', (): void => { }); }); it('should give HTTP 200 when correctly creating and searching for a user', async () => { - const user = { + const user: CreateUserRequest = { firstName: 'Één bier', lastName: 'is geen bier', type: UserType.LOCAL_USER, email: 'spam@gewis.nl', + canGoIntoDebt: true, + ofAge: true, }; // Create the user @@ -744,34 +748,6 @@ describe('UserController', (): void => { .send(userObj); expect(res.status).to.equal(400); }); - - it('should create user when active is true', async () => { - const userObj = { ...ctx.user, active: true }; - - const res = await request(ctx.app) - .post('/users') - .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(userObj); - expect(res.status).to.equal(201); - - const user = res.body as UserResponse; - const spec = await Swagger.importSpecification(); - verifyUserResponse(spec, user); - }); - - it('should create user when active is false', async () => { - const userObj = { ...ctx.user, active: false }; - - const res = await request(ctx.app) - .post('/users') - .set('Authorization', `Bearer ${ctx.adminToken}`) - .send(userObj); - expect(res.status).to.equal(201); - - const user = res.body as UserResponse; - const spec = await Swagger.importSpecification(); - verifyUserResponse(spec, user); - }); }); describe('PATCH /users/:id', () => { diff --git a/test/unit/service/authentication-service.ts b/test/unit/service/authentication-service.ts index 9b0ff53bd..efedb36cd 100644 --- a/test/unit/service/authentication-service.ts +++ b/test/unit/service/authentication-service.ts @@ -42,6 +42,7 @@ export default function userIsAsExpected(user: User | UserResponse, ADResponse: if (isNumber(user.type)) expect(user.type).to.equal(1); expect(user.active).to.equal(true); expect(user.deleted).to.equal(false); + expect(user.canGoIntoDebt).to.equal(true); } describe('AuthenticationService', (): void => { diff --git a/test/unit/service/borrelkaart-group-service.ts b/test/unit/service/voucher-group-service.ts similarity index 66% rename from test/unit/service/borrelkaart-group-service.ts rename to test/unit/service/voucher-group-service.ts index 940083dcb..6da6950d7 100644 --- a/test/unit/service/borrelkaart-group-service.ts +++ b/test/unit/service/voucher-group-service.ts @@ -19,15 +19,15 @@ import { expect } from 'chai'; import Sinon from 'sinon'; import { Connection } from 'typeorm'; -import { BorrelkaartGroupParams, BorrelkaartGroupRequest } from '../../../src/controller/request/borrelkaart-group-request'; -import BorrelkaartGroupResponse from '../../../src/controller/response/borrelkaart-group-response'; +import { VoucherGroupParams, VoucherGroupRequest } from '../../../src/controller/request/voucher-group-request'; +import VoucherGroupResponse from '../../../src/controller/response/voucher-group-response'; import Database from '../../../src/database/database'; import Transfer from '../../../src/entity/transactions/transfer'; -import User, { UserType } from '../../../src/entity/user/user'; +import User, { TermsOfServiceStatus, UserType } from '../../../src/entity/user/user'; import RoleManager from '../../../src/rbac/role-manager'; -import BorrelkaartGroupService from '../../../src/service/borrelkaart-group-service'; +import VoucherGroupService from '../../../src/service/voucher-group-service'; -export function bkgEq(req: BorrelkaartGroupParams, res: BorrelkaartGroupResponse): void { +export function bkgEq(req: VoucherGroupParams, res: VoucherGroupResponse): void { // check if non user fields are equal expect(res.name).to.equal(req.name); expect(res.activeStartDate).to.equal(req.activeStartDate.toISOString()); @@ -36,11 +36,11 @@ export function bkgEq(req: BorrelkaartGroupParams, res: BorrelkaartGroupResponse expect(res.balance.amount).to.equal(req.balance.getAmount()); } -export async function seedBorrelkaartGroups(): Promise<{ paramss: BorrelkaartGroupParams[], bkgIds: number[] }> { - const paramss: BorrelkaartGroupParams[] = []; +export async function seedVoucherGroups(): Promise<{ paramss: VoucherGroupParams[], bkgIds: number[] }> { + const paramss: VoucherGroupParams[] = []; const bkgIds: number[] = []; await Promise.all([...Array(5).keys()].map(async (i) => { - const bkgReq: BorrelkaartGroupRequest = { + const bkgReq: VoucherGroupRequest = { name: `test ${i}`, activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -51,8 +51,8 @@ export async function seedBorrelkaartGroups(): Promise<{ paramss: BorrelkaartGro }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(bkgReq); - const bkgRes = await BorrelkaartGroupService.createBorrelkaartGroup(params); + const params = VoucherGroupService.asVoucherGroupParams(bkgReq); + const bkgRes = await VoucherGroupService.createVoucherGroup(params); // paramss.push(params); bkgIds[bkgRes.id - 1] = bkgRes.id; paramss[bkgRes.id - 1] = params; @@ -60,7 +60,7 @@ export async function seedBorrelkaartGroups(): Promise<{ paramss: BorrelkaartGro return { paramss, bkgIds }; } -describe('BorrelkaartGroupService', async (): Promise => { +describe('VoucherGroupService', async (): Promise => { let ctx: { connection: Connection, clock: Sinon.SinonFakeTimers @@ -77,7 +77,7 @@ describe('BorrelkaartGroupService', async (): Promise => { roleManager.registerRole({ name: 'Admin', permissions: { - BorrelkaartGroup: { + VoucherGroup: { create: all, get: all, update: all, @@ -101,9 +101,9 @@ describe('BorrelkaartGroupService', async (): Promise => { ctx.clock.restore(); }); - describe('validate borrelkaart group', () => { - it('should return true when the borrelkaart is valid', async () => { - const req: BorrelkaartGroupRequest = { + describe('validate voucher group', () => { + it('should return true when the voucher is valid', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -114,11 +114,11 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.true; + const params = VoucherGroupService.asVoucherGroupParams(req); + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.true; }); - it('should return false when the borrelkaart has an invalid name', async () => { - const req: BorrelkaartGroupRequest = { + it('should return false when the voucher has an invalid name', async () => { + const req: VoucherGroupRequest = { name: '', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -129,11 +129,11 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.false; + const params = VoucherGroupService.asVoucherGroupParams(req); + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.false; }); - it('should return false when the borrelkaart has an invalid startDate', async () => { - const req: BorrelkaartGroupRequest = { + it('should return false when the voucher has an invalid startDate', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: 'aasdfasd', activeEndDate: '2000-01-03T00:00:00Z', @@ -144,12 +144,12 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); + const params = VoucherGroupService.asVoucherGroupParams(req); expect(params.activeStartDate.valueOf()).to.NaN; - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.false; + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.false; }); - it('should return false when the borrelkaart has an invalid endDate', async () => { - const req: BorrelkaartGroupRequest = { + it('should return false when the voucher has an invalid endDate', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: 'asdafasd', @@ -160,11 +160,11 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.false; + const params = VoucherGroupService.asVoucherGroupParams(req); + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.false; }); - it('should return false when the borrelkaart endDate is before startDate', async () => { - const req: BorrelkaartGroupRequest = { + it('should return false when the voucher endDate is before startDate', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-03T00:00:00Z', activeEndDate: '2000-01-01T00:00:00Z', @@ -175,11 +175,11 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.false; + const params = VoucherGroupService.asVoucherGroupParams(req); + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.false; }); - it('should return false when the borrelkaart endDate is in the past', async () => { - const req: BorrelkaartGroupRequest = { + it('should return false when the voucher endDate is in the past', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '1999-12-31T00:00:00Z', @@ -190,11 +190,11 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.false; + const params = VoucherGroupService.asVoucherGroupParams(req); + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.false; }); - it('should return false when the borrelkaart has an invalid balance', async () => { - const req: BorrelkaartGroupRequest = { + it('should return false when the voucher has an invalid balance', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -205,11 +205,11 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.false; + const params = VoucherGroupService.asVoucherGroupParams(req); + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.false; }); - it('should return false when the borrelkaart has an invalid amount of users', async () => { - const req: BorrelkaartGroupRequest = { + it('should return false when the voucher has an invalid amount of users', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -220,14 +220,14 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 0, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - expect(BorrelkaartGroupService.validateBorrelkaartGroup(params)).to.be.false; + const params = VoucherGroupService.asVoucherGroupParams(req); + expect(VoucherGroupService.validateVoucherGroup(params)).to.be.false; }); }); - describe('create borrelkaart group', () => { - it('should create a borrelkaart group with inactive members', async () => { - const req: BorrelkaartGroupRequest = { + describe('create voucher group', () => { + it('should create a voucher group with inactive members', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -238,19 +238,20 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.createBorrelkaartGroup(params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.createVoucherGroup(params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user inactive').to.equal(false); + expect(user.acceptedToS).to.equal(TermsOfServiceStatus.NOT_REQUIRED); const transfers = await Transfer.find({ where: { toId: user.id } }); const balanceAmounts = transfers.map((transfer) => transfer.amount.getAmount()); const balance = balanceAmounts.reduce((a, b) => a + b); expect(balance, 'correct transfers').to.equal(params.balance.getAmount()); })); }); - it('should create a borrelkaart group with active members', async () => { - const req: BorrelkaartGroupRequest = { + it('should create a voucher group with active members', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '1999-12-31T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -261,8 +262,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.createBorrelkaartGroup(params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.createVoucherGroup(params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user active').to.equal(true); @@ -274,10 +275,10 @@ describe('BorrelkaartGroupService', async (): Promise => { }); }); - describe('update borrelkaart group', () => { + describe('update voucher group', () => { let bkgId: number; beforeEach(async () => { - const req: BorrelkaartGroupRequest = { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -288,13 +289,13 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.createBorrelkaartGroup(params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.createVoucherGroup(params); bkgId = bkgRes.id; }); - it('should update an existing borrelkaart groups name', async () => { - const req: BorrelkaartGroupRequest = { + it('should update an existing voucher groups name', async () => { + const req: VoucherGroupRequest = { name: 'newTest', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -305,8 +306,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId, params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user inactive').to.equal(false); @@ -317,8 +318,8 @@ describe('BorrelkaartGroupService', async (): Promise => { })); }); - it('should update an existing borrelkaart groups active start date', async () => { - const req: BorrelkaartGroupRequest = { + it('should update an existing voucher groups active start date', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-03T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -329,8 +330,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId, params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user inactive').to.equal(false); @@ -341,8 +342,8 @@ describe('BorrelkaartGroupService', async (): Promise => { })); }); - it('should update an existing borrelkaart groups active end date', async () => { - const req: BorrelkaartGroupRequest = { + it('should update an existing voucher groups active end date', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-04T00:00:00Z', @@ -353,8 +354,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId, params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user inactive').to.equal(false); @@ -365,8 +366,8 @@ describe('BorrelkaartGroupService', async (): Promise => { })); }); - it('should update an existing borrelkaart groups passed active start date', async () => { - const req: BorrelkaartGroupRequest = { + it('should update an existing voucher groups passed active start date', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '1999-12-31T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -377,8 +378,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId, params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user active').to.equal(true); @@ -389,8 +390,8 @@ describe('BorrelkaartGroupService', async (): Promise => { })); }); - it('should update an existing borrelkaart groups increased user amount', async () => { - const req: BorrelkaartGroupRequest = { + it('should update an existing voucher groups increased user amount', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -401,8 +402,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 5, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId, params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user active').to.equal(false); @@ -413,8 +414,8 @@ describe('BorrelkaartGroupService', async (): Promise => { })); }); - it('should update an existing borrelkaart groups increased balance', async () => { - const req: BorrelkaartGroupRequest = { + it('should update an existing voucher groups increased balance', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -425,8 +426,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId, params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user active').to.equal(false); @@ -437,8 +438,8 @@ describe('BorrelkaartGroupService', async (): Promise => { })); }); - it('should update an existing borrelkaart groups decreased balance', async () => { - const req: BorrelkaartGroupRequest = { + it('should update an existing voucher groups decreased balance', async () => { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -449,8 +450,8 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId, params); bkgEq(params, bkgRes); await Promise.all(bkgRes.users.map(async (user) => { expect(user.active, 'user active').to.equal(false); @@ -466,7 +467,7 @@ describe('BorrelkaartGroupService', async (): Promise => { }); it('should return undefined when given an invalid id', async () => { - const req: BorrelkaartGroupRequest = { + const req: VoucherGroupRequest = { name: 'test', activeStartDate: '2000-01-02T00:00:00Z', activeEndDate: '2000-01-03T00:00:00Z', @@ -477,23 +478,23 @@ describe('BorrelkaartGroupService', async (): Promise => { }, amount: 4, }; - const params = BorrelkaartGroupService.asBorrelkaartGroupParams(req); - const bkgRes = await BorrelkaartGroupService.updateBorrelkaartGroup(bkgId + 1, params); + const params = VoucherGroupService.asVoucherGroupParams(req); + const bkgRes = await VoucherGroupService.updateVoucherGroup(bkgId + 1, params); expect(bkgRes).to.be.undefined; }); }); - describe('get borrelkaart groups', () => { - let paramss: BorrelkaartGroupParams[]; + describe('get voucher groups', () => { + let paramss: VoucherGroupParams[]; let bkgIds: number[]; beforeEach(async () => { - const bkgs = await seedBorrelkaartGroups(); + const bkgs = await seedVoucherGroups(); paramss = bkgs.paramss; bkgIds = bkgs.bkgIds; }); - it('should get an borrelkaart group by id', async () => { - const bkgRes = (await BorrelkaartGroupService.getBorrelkaartGroups({ bkgId: bkgIds[0] })) + it('should get an voucher group by id', async () => { + const bkgRes = (await VoucherGroupService.getVoucherGroups({ bkgId: bkgIds[0] })) .records[0]; bkgEq(paramss[0], bkgRes); await Promise.all(bkgRes.users.map(async (user) => { @@ -506,13 +507,13 @@ describe('BorrelkaartGroupService', async (): Promise => { }); it('should return undefined when given a wrong id', async () => { - const bkgRes = (await BorrelkaartGroupService.getBorrelkaartGroups({ bkgId: bkgIds.length + 1 })) + const bkgRes = (await VoucherGroupService.getVoucherGroups({ bkgId: bkgIds.length + 1 })) .records[0]; expect(bkgRes).to.be.undefined; }); - it('should get all borrelkaart groups', async () => { - const bkgRes = (await BorrelkaartGroupService.getBorrelkaartGroups({})).records; + it('should get all voucher groups', async () => { + const bkgRes = (await VoucherGroupService.getVoucherGroups({})).records; await Promise.all(bkgRes.map(async (res, i) => { bkgEq(paramss[i], res); await Promise.all(res.users.map(async (user) => { diff --git a/test/unit/validators.ts b/test/unit/validators.ts index 8f17c897f..054b310c0 100644 --- a/test/unit/validators.ts +++ b/test/unit/validators.ts @@ -22,7 +22,7 @@ import ProductRevision from '../../src/entity/product/product-revision'; import ContainerRevision from '../../src/entity/container/container-revision'; import PointOfSaleRevision from '../../src/entity/point-of-sale/point-of-sale-revision'; import { BaseTransactionResponse } from '../../src/controller/response/transaction-response'; -import { UserResponse } from '../../src/controller/response/user-response'; +import { BaseUserResponse, UserResponse } from '../../src/controller/response/user-response'; import { BasePointOfSaleResponse } from '../../src/controller/response/point-of-sale-response'; export function verifyUserEntity( @@ -38,6 +38,18 @@ export function verifyUserEntity( expect(Object.values(UserType)).to.include(user.type); } +export function verifyBaseUserResponse( + spec: SwaggerSpecification, userResponse: BaseUserResponse, +): void { + const validation = spec.validateModel('BaseUserResponse', userResponse, true, false); + expect(validation.valid).to.be.true; + expect(userResponse.id).to.be.at.least(0); + expect(userResponse.firstName).to.be.not.empty; + expect(userResponse.lastName).to.be.not.undefined; + expect(userResponse.lastName).to.be.not.null; + expect(userResponse.nickname).to.satisfy((nick: string) => nick == null || nick.length >= 1); +} + export function verifyUserResponse( spec: SwaggerSpecification, userResponse: UserResponse, canBeDeleted?: boolean, ): void { @@ -112,7 +124,7 @@ export function verifyBaseTransactionEntity( expect(baseTransaction.value.amount).to.be.at.least(0); expect(baseTransaction.createdAt).to.be.not.undefined; expect(baseTransaction.createdAt).to.be.not.null; - verifyUserResponse(spec, baseTransaction.from, true); - verifyUserResponse(spec, baseTransaction.createdBy, true); + verifyBaseUserResponse(spec, baseTransaction.from); + verifyBaseUserResponse(spec, baseTransaction.createdBy); verifyBasePOSResponse(spec, baseTransaction.pointOfSale); } From 3b8ae4c3bdaf226fc42b2080d435d62ee893f75e Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:45:17 +0200 Subject: [PATCH 06/13] Fine design improvements (#78) * Refactor afterInsert listers to subscribers * Reference dates now required & added balances w/ date to calc results * Fix test suite * Update getBalances method to always return last transaction/transfer ID * Fix transaction subscriber tests * Fix debtor test suite failing * Implement Samuel's feedback * Fix test suite * Fix test suite --- src/controller/debtor-controller.ts | 31 ++-- src/controller/request/debtor-request.ts | 4 +- src/controller/response/balance-response.ts | 9 +- src/controller/response/debtor-response.ts | 23 +-- src/controller/user-controller.ts | 15 +- src/database/database.ts | 5 + src/entity/transactions/transaction.ts | 34 ---- src/entity/transactions/transfer.ts | 19 -- src/helpers/pagination.ts | 6 +- src/helpers/validators.ts | 13 ++ src/service/balance-service.ts | 21 ++- src/service/debtor-service.ts | 63 +++---- src/subscriber/index.ts | 20 +++ src/subscriber/transaction-subscriber.ts | 70 ++++++++ src/subscriber/transfer-subscriber.ts | 49 ++++++ test/seed.ts | 26 +++ test/unit/controller/debtor-controller.ts | 164 +++++++++++------- test/unit/controller/user-controller.ts | 16 +- test/unit/service/balance-service.ts | 45 ++++- test/unit/service/debtor-service.ts | 115 ++++-------- .../transaction-subscriber.ts} | 48 ++--- .../transfer-subscriber.ts} | 35 ++-- 22 files changed, 510 insertions(+), 321 deletions(-) create mode 100644 src/subscriber/index.ts create mode 100644 src/subscriber/transaction-subscriber.ts create mode 100644 src/subscriber/transfer-subscriber.ts rename test/unit/{entity/transactions/transaction.ts => subscribe/transaction-subscriber.ts} (84%) rename test/unit/{entity/transactions/transfer.ts => subscribe/transfer-subscriber.ts} (87%) diff --git a/src/controller/debtor-controller.ts b/src/controller/debtor-controller.ts index 89950c97f..d1f6f6846 100644 --- a/src/controller/debtor-controller.ts +++ b/src/controller/debtor-controller.ts @@ -23,7 +23,7 @@ import { RequestWithToken } from '../middleware/token-middleware'; import { parseRequestPagination } from '../helpers/pagination'; import DebtorService from '../service/debtor-service'; import User from '../entity/user/user'; -import { asArrayOfUserTypes, asDate } from '../helpers/validators'; +import { asArrayOfDates, asArrayOfUserTypes, asDate } from '../helpers/validators'; import { In } from 'typeorm'; import { HandoutFinesRequest } from './request/debtor-request'; import Fine from '../entity/fine/fine'; @@ -101,7 +101,7 @@ export default class DebtorController extends BaseController { take = pagination.take; skip = pagination.skip; } catch (e) { - res.status(400).send(e.message); + res.status(400).json(e.message); return; } @@ -174,8 +174,9 @@ export default class DebtorController extends BaseController { * @group debtors - Operations of the debtor controller * @operationId calculateFines * @security JWT - * @param {Array} userTypes[].query.required - List of all user types fines should be calculated for - * @param {string} referenceDate.query - Date to base fines on. If undefined, use the date of the last fine handout event. If that one also does not exist, use now. + * @param {Array.} userTypes[].query - List of all user types fines should be calculated for + * @param {Array.} referenceDates[].query.required - Dates to base the fines on. Every returned user has at + * least five euros debt on every reference date. The height of the fine is based on the first date in the array. * @returns {Array} 200 - List of eligible fines * @returns {string} 400 - Validation error * @returns {string} 500 - Internal server error @@ -185,14 +186,16 @@ export default class DebtorController extends BaseController { let params; try { + if (req.query.referenceDates === undefined) throw new Error('referenceDates is required'); + const referenceDates = asArrayOfDates(req.query.referenceDates); + if (referenceDates === undefined) throw new Error('referenceDates is not a valid array'); params = { userTypes: asArrayOfUserTypes(req.query.userTypes), - referenceDate: asDate(req.query.referenceDate), + referenceDates, }; - if (params.userTypes === undefined) throw new Error('userTypes is not a valid array of UserTypes'); - if (params.referenceDate === undefined && req.query.referenceDate !== undefined) throw new Error('referenceDate is not a valid date'); + if (params.userTypes === undefined && req.query.userTypes !== undefined) throw new Error('userTypes is not a valid array of UserTypes'); } catch (e) { - res.status(400).send(e.message); + res.status(400).json(e.message); return; } @@ -205,7 +208,7 @@ export default class DebtorController extends BaseController { } /** - * Handout fines to all given users. + * Handout fines to all given users. Fines will be handed out "now" to prevent rewriting history. * @route POST /fines/handout * @group debtors - Operations of the debtor controller * @operationId handoutFines @@ -227,11 +230,10 @@ export default class DebtorController extends BaseController { if (users.length !== body.userIds.length) throw new Error('userIds is not a valid array of user IDs'); if (body.referenceDate !== undefined) { - referenceDate = new Date(body.referenceDate); - if (Number.isNaN(referenceDate.getTime())) throw new Error('referenceDate is not a valid date'); + referenceDate = asDate(body.referenceDate); } } catch (e) { - res.status(400).send(e.message); + res.status(400).json(e.message); return; } @@ -267,11 +269,10 @@ export default class DebtorController extends BaseController { if (users.length !== body.userIds.length) throw new Error('userIds is not a valid array of user IDs'); if (body.referenceDate !== undefined) { - referenceDate = new Date(body.referenceDate); - if (Number.isNaN(referenceDate.getTime())) throw new Error('referenceDate is not a valid date'); + referenceDate = asDate(body.referenceDate); } } catch (e) { - res.status(400).send(e.message); + res.status(400).json(e.message); return; } diff --git a/src/controller/request/debtor-request.ts b/src/controller/request/debtor-request.ts index 8d247d760..f6b1b347b 100644 --- a/src/controller/request/debtor-request.ts +++ b/src/controller/request/debtor-request.ts @@ -19,9 +19,9 @@ /** * @typedef HandoutFinesRequest * @property {Array} userIds.required - Users to fine. If a user is not eligible for a fine, a fine of 0,00 will be handed out. - * @property {string} referenceDate - Reference date to calculate the balance and fine for (and "now", but that is always done and doesn't have to be explicitly specified) + * @property {string} referenceDate.required - Reference date to calculate the balance and thus the height of the fine for. */ export interface HandoutFinesRequest { userIds: number[]; - referenceDate?: string; + referenceDate: string; } diff --git a/src/controller/response/balance-response.ts b/src/controller/response/balance-response.ts index fe4fbcc38..f15861e28 100644 --- a/src/controller/response/balance-response.ts +++ b/src/controller/response/balance-response.ts @@ -21,8 +21,10 @@ import { PaginationResult } from '../../helpers/pagination'; /** * @typedef BalanceResponse * @property {number} id.required - ID of the user this balance belongs to + * @property {string} date.required - Date at which this user had this balance * @property {DineroObjectResponse.model} amount.required - The amount of balance this user has - * @property {DineroObjectResponse.model} fine - The amount of fines this user has, if any + * @property {DineroObjectResponse.model} fine - The amount of fines this user has at the current point in time, + * aka "now" (if any). Should be ignored if date is not now. * @property {string} fineSince - Timestamp of the first fine * @property {number} lastTransactionId - The ID of the last transaction that was * present when the balance was cached @@ -31,11 +33,12 @@ import { PaginationResult } from '../../helpers/pagination'; */ export default interface BalanceResponse { id: number; + date: string; amount: DineroObjectResponse; fine?: DineroObjectResponse | null; fineSince?: string | null; - lastTransactionId?: number | null; - lastTransferId?: number | null; + lastTransactionId: number; + lastTransferId: number; } /** diff --git a/src/controller/response/debtor-response.ts b/src/controller/response/debtor-response.ts index 6ecd61eac..4b6fe8555 100644 --- a/src/controller/response/debtor-response.ts +++ b/src/controller/response/debtor-response.ts @@ -19,21 +19,24 @@ import { DineroObjectResponse } from './dinero-response'; import BaseResponse from './base-response'; import { PaginationResult } from '../../helpers/pagination'; import { BaseUserResponse } from './user-response'; +import BalanceResponse from './balance-response'; /** * @typedef UserToFineResponse - * @property {integer} id - User ID - * @property {DineroObjectResponse.model} amount - Amount to fine + * @property {integer} id.required - User ID + * @property {DineroObjectResponse.model} fineAmount.required - Amount to fine + * @property {Array.} balances.required - Balances at the given reference dates */ export interface UserToFineResponse { id: number; - amount: DineroObjectResponse; + fineAmount: DineroObjectResponse; + balances: BalanceResponse[] } /** * @typedef {BaseResponse} FineResponse - * @property {DineroObjectResponse.model} amount - Fine amount - * @property {BaseUserResponse.model} user - User that got the fine + * @property {DineroObjectResponse.model} amount.required - Fine amount + * @property {BaseUserResponse.model} user.required - User that got the fine */ export interface FineResponse extends BaseResponse { amount: DineroObjectResponse; @@ -43,7 +46,7 @@ export interface FineResponse extends BaseResponse { /** * @typedef {BaseResponse} BaseFineHandoutEventResponse * @property {string} referenceDate.required - Reference date of fines - * @property {BaseUserResponse.model} createdBy - User that handed out the fines + * @property {BaseUserResponse.model} createdBy.required - User that handed out the fines */ export interface BaseFineHandoutEventResponse extends BaseResponse { referenceDate: string; @@ -52,7 +55,7 @@ export interface BaseFineHandoutEventResponse extends BaseResponse { /** * @typedef {BaseFineHandoutEventResponse} FineHandoutEventResponse - * @property {Array.} fines - Fines that have been handed out + * @property {Array.} fines.required - Fines that have been handed out */ export interface FineHandoutEventResponse extends BaseFineHandoutEventResponse { fines: FineResponse[]; @@ -60,8 +63,8 @@ export interface FineHandoutEventResponse extends BaseFineHandoutEventResponse { /** * @typedef PaginatedFineHandoutEventResponse - * @property {PaginationResult.model} _pagination - Pagination metadata - * @property {Array.} records - Returned fine handout events + * @property {PaginationResult.model} _pagination.required - Pagination metadata + * @property {Array.} records.required - Returned fine handout events */ export interface PaginatedFineHandoutEventResponse { _pagination: PaginationResult, @@ -70,7 +73,7 @@ export interface PaginatedFineHandoutEventResponse { /** * @typedef UserFineGroupResponse - * @property {Array.} fines - Fines that have been handed out + * @property {Array.} fines.required - Fines that have been handed out */ export interface UserFineGroupResponse { fines: FineResponse[]; diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index d1132549b..012641357 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -623,6 +623,8 @@ export default class UserController extends BaseController { * @operationId getOrganMembers * @group users - Operations of user controller * @param {integer} id.path.required - The id of the user + * @param {integer} take.query - How many members the endpoint should return + * @param {integer} skip.query - How many members should be skipped (for pagination) * @security JWT * @returns {PaginatedUserResponse.model} 200 - All members of the organ * @returns {string} 404 - Nonexistent user id @@ -632,6 +634,17 @@ export default class UserController extends BaseController { const parameters = req.params; this.logger.trace('Get organ members', parameters, 'by user', req.token.user); + let take; + let skip; + try { + const pagination = parseRequestPagination(req); + take = pagination.take; + skip = pagination.skip; + } catch (e) { + res.status(400).send(e.message); + return; + } + try { const organId = asNumber(parameters.id); // Get the user object if it exists @@ -647,7 +660,7 @@ export default class UserController extends BaseController { return; } - const members = await UserService.getUsers({ organId }); + const members = await UserService.getUsers({ organId }, { take, skip }); res.status(200).json(members); } catch (error) { this.logger.error('Could not get organ members:', error); diff --git a/src/database/database.ts b/src/database/database.ts index 3775ca24e..75802ef12 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -66,6 +66,7 @@ import UserFineGroup from '../entity/fine/userFineGroup'; import Event from '../entity/event/event'; import EventShiftAnswer from '../entity/event/event-shift-answer'; import EventShift from '../entity/event/event-shift'; +import { TransactionSubscriber, TransferSubscriber } from '../subscriber'; export default class Database { public static async initialize(): Promise { @@ -133,6 +134,10 @@ export default class Database { EventShift, EventShiftAnswer, ], + subscribers: [ + TransactionSubscriber, + TransferSubscriber, + ], }; return createConnection(options); } diff --git a/src/entity/transactions/transaction.ts b/src/entity/transactions/transaction.ts index 79c84af68..7ac32be0d 100644 --- a/src/entity/transactions/transaction.ts +++ b/src/entity/transactions/transaction.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ import { - AfterInsert, Entity, ManyToOne, OneToMany, } from 'typeorm'; // eslint-disable-next-line import/no-cycle @@ -24,11 +23,6 @@ import SubTransaction from './sub-transaction'; import User from '../user/user'; import BaseEntity from '../base-entity'; import PointOfSaleRevision from '../point-of-sale/point-of-sale-revision'; -import BalanceService from '../../service/balance-service'; -import Mailer from '../../mailer'; -import UserDebtNotification from '../../mailer/templates/user-debt-notification'; -import DineroTransformer from '../transformer/dinero-transformer'; -import { getLogger } from 'log4js'; /** * @typedef {BaseEntity} Transaction @@ -54,32 +48,4 @@ export default class Transaction extends BaseEntity { @ManyToOne(() => PointOfSaleRevision) public pointOfSale: PointOfSaleRevision; - - @AfterInsert() - // NOTE: this event listener is only called when calling .save() on a new Transaction object instance, - // not .save() on the static method of the Transaction class - async sendEmailNotificationIfNowInDebt() { - if (process.env.NODE_ENV === 'test') return; - - const user = await User.findOne({ where: { id: this.from.id } }); - const balance = await BalanceService.getBalance(user.id); - const currentBalance = balance.amount.amount - this.subTransactions[0].subTransactionRows[0].amount * this.subTransactions[0].subTransactionRows[0].product.priceInclVat.getAmount(); - - if (currentBalance >= 0) return; - // User is now in debt - - const balanceBefore = await BalanceService.getBalance( - user.id, - new Date(this.createdAt.getTime() - 1), - ); - - if (balanceBefore.amount.amount < 0) return; - // User was not in debt before this new transaction - - Mailer.getInstance().send(user, new UserDebtNotification({ - name: user.firstName, - balance: DineroTransformer.Instance.from(currentBalance), - url: '', - })).catch((e) => getLogger('Transaction').error(e)); - } } diff --git a/src/entity/transactions/transfer.ts b/src/entity/transactions/transfer.ts index 7a50c5cfe..56d1f0e34 100644 --- a/src/entity/transactions/transfer.ts +++ b/src/entity/transactions/transfer.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ import { - AfterInsert, Column, Entity, JoinColumn, ManyToOne, OneToOne, } from 'typeorm'; import { Dinero } from 'dinero.js'; @@ -27,7 +26,6 @@ import PayoutRequest from './payout-request'; import StripeDeposit from '../deposit/stripe-deposit'; import Invoice from '../invoices/invoice'; import Fine from '../fine/fine'; -import BalanceService from '../../service/balance-service'; import UserFineGroup from '../fine/userFineGroup'; /** @@ -86,21 +84,4 @@ export default class Transfer extends BaseEntity { @OneToOne(() => UserFineGroup, (g) => g.waivedTransfer, { nullable: true }) public waivedFines: UserFineGroup | null; - - @AfterInsert() - // NOTE: this event listener is only called when calling .save() on a new Transfer object instance, - // not .save() on the static method of the Transfer class - async validateDebtPaid() { - if (this.toId == null) return; - - const user = await User.findOne({ where: { id: this.toId }, relations: ['currentFines'] }); - if (user.currentFines == null) return; - - // Remove currently unpaid fines when new balance is positive. - const balance = await BalanceService.getBalance(user.id); - if (balance.amount.amount >= 0) { - user.currentFines = null; - await user.save(); - } - } } diff --git a/src/helpers/pagination.ts b/src/helpers/pagination.ts index 212c7f239..6e6916894 100644 --- a/src/helpers/pagination.ts +++ b/src/helpers/pagination.ts @@ -33,9 +33,9 @@ export interface PaginationParameters { /** * @typedef PaginationResult - * @property {integer} take Number of records queried - * @property {integer} skip Number of skipped records - * @property {integer} count Total number of resulting records + * @property {integer} take.required Number of records queried + * @property {integer} skip.required Number of skipped records + * @property {integer} count.required Total number of resulting records */ export interface PaginationResult { take?: number; diff --git a/src/helpers/validators.ts b/src/helpers/validators.ts index 1f5ea46cd..f31cc3d50 100644 --- a/src/helpers/validators.ts +++ b/src/helpers/validators.ts @@ -194,3 +194,16 @@ export function asArrayOfNumbers(input: any): number[] | undefined { if (!Array.isArray(input)) return undefined; return input.map((i) => asNumber(i)); } + +/** + * Converts the input to a list of dates + * @param input + * @throws TypeError - If array contains one or more invalid or undefined dates + */ +export function asArrayOfDates(input: any): Date[] | undefined { + if (!input) return undefined; + if (!Array.isArray(input)) input = [input]; + const dates = input.map((i: any[]) => asDate(i)); + if (dates.some((d: (Date | undefined)[]) => d === undefined)) throw new TypeError('Array contains invalid date'); + return dates; +} diff --git a/src/service/balance-service.ts b/src/service/balance-service.ts index f7a306221..cf9c77d68 100644 --- a/src/service/balance-service.ts +++ b/src/service/balance-service.ts @@ -63,7 +63,7 @@ export function asBalanceOrderColumn(input: any): BalanceOrderColumn | undefined } export default class BalanceService { - protected static asBalanceResponse(rawBalance: any): BalanceResponse { + protected static asBalanceResponse(rawBalance: any, date: Date): BalanceResponse { let fineSince = null; // SQLite returns timestamps in UTC, while MariaDB/MySQL returns timestamps in the local timezone if (rawBalance.fineSince) { @@ -73,6 +73,7 @@ export default class BalanceService { return { id: rawBalance.id, + date: date.toISOString(), amount: DineroTransformer.Instance.from(rawBalance.amount).toObject(), lastTransactionId: rawBalance.lastTransactionId, lastTransferId: rawBalance.lastTransferId, @@ -205,18 +206,20 @@ export default class BalanceService { let query = 'SELECT moneys2.id as id, ' + 'moneys2.totalValue + COALESCE(b5.amount, 0) as amount, ' + 'moneys2.count as count, ' - + 'b5.lastTransactionId as lastTransactionId, ' - + 'b5.lastTransferId as lastTransferId, ' + + 'max(coalesce(b5.lasttransactionid, -1), coalesce(moneys2.lastTransactionId, -1)) as lastTransactionId, ' + + 'max(coalesce(b5.lasttransferid, -1), coalesce(moneys2.lastTransferId, -1)) as lastTransferId, ' + 'b5.amount as cachedAmount, ' + 'f.fine as fine, ' + 'f.fineSince as fineSince ' + 'from ( ' + 'SELECT user.id as id, ' + 'COALESCE(sum(moneys.totalValue), 0) as totalValue, ' - + 'count(moneys.totalValue) as count ' + + 'count(moneys.totalValue) as count, ' + + 'max(moneys.transactionId) as lastTransactionId, ' + + 'max(moneys.transferId) as lastTransferId ' + 'from user ' + 'left join ( ' - + 'select t.fromId as `id`, str.amount * pr.priceInclVat * -1 as `totalValue` ' + + 'select t.fromId as `id`, str.amount * pr.priceInclVat * -1 as `totalValue`, t.id as `transactionId`, null as `transferId` ' + 'from `transaction` as `t` ' + `left join ${balanceSubquery()} as b on t.fromId=b.userId ` + 'inner join sub_transaction st on t.id=st.transactionId ' @@ -226,7 +229,7 @@ export default class BalanceService { query = this.addWhereClauseForIds(query, parameters, 't.fromId', ids); query = this.addWhereClauseForDate(query, parameters, 't.createdAt', d); query += 'UNION ALL ' - + 'select st2.toId as `id`, str2.amount * pr2.priceInclVat as `totalValue` from sub_transaction st2 ' + + 'select st2.toId as `id`, str2.amount * pr2.priceInclVat as `totalValue`, t.id as `transactionId`, null as `transferId` from sub_transaction st2 ' + `left join ${balanceSubquery()} b on st2.toId=b.userId ` + 'inner join `transaction` t on t.id=st2.transactionId ' + 'inner join sub_transaction_row str2 on st2.id=str2.subTransactionId ' @@ -235,13 +238,13 @@ export default class BalanceService { query = this.addWhereClauseForIds(query, parameters, 'st2.toId', ids); query = this.addWhereClauseForDate(query, parameters, 't.createdAt', d); query += 'UNION ALL ' - + 'select t2.fromId as `id`, t2.amount*-1 as `totalValue` from transfer t2 ' + + 'select t2.fromId as `id`, t2.amount*-1 as `totalValue`, null as `transactionId`, t2.id as `transferId` from transfer t2 ' + `left join ${balanceSubquery()} b on t2.fromId=b.userId ` + 'where t2.createdAt > COALESCE(b.lastTransferDate, 0) '; query = this.addWhereClauseForIds(query, parameters, 't2.fromId', ids); query = this.addWhereClauseForDate(query, parameters, 't2.createdAt', d); query += 'UNION ALL ' - + 'select t3.toId as `id`, t3.amount as `totalValue` from transfer t3 ' + + 'select t3.toId as `id`, t3.amount as `totalValue`, null as `transactionId`, t3.id as `transferId` from transfer t3 ' + `left join ${balanceSubquery()} b on t3.toId=b.userId ` + 'where t3.createdAt > COALESCE(b.lastTransferDate, 0) '; query = this.addWhereClauseForIds(query, parameters, 't3.toId', ids); @@ -298,7 +301,7 @@ export default class BalanceService { const count = (await connection.query(query, parameters)).length; return { _pagination: { take, skip, count }, - records: balances.map((b: object) => this.asBalanceResponse(b)), + records: balances.map((b: object) => this.asBalanceResponse(b, date ?? new Date())), }; } diff --git a/src/service/debtor-service.ts b/src/service/debtor-service.ts index 5b2e3f70f..137392d17 100644 --- a/src/service/debtor-service.ts +++ b/src/service/debtor-service.ts @@ -44,11 +44,11 @@ import UserWillGetFined from '../mailer/templates/user-will-get-fined'; export interface CalculateFinesParams { userTypes?: UserType[]; userIds?: number[]; - referenceDate?: Date; + referenceDates: Date[]; } export interface HandOutFinesParams { - referenceDate?: Date; + referenceDate: Date; userIds: number[]; } @@ -144,41 +144,41 @@ export default class DebtorService { * For all these users, also return their fine based on the reference date. * @param userTypes List of all user types fines should be calculated for * @param userIds List of all user IDs fines should be calculated for - * @param referenceDate Date to base fines on. If undefined, use now. + * @param referenceDates Dates at which a user needs to have a negative balance. The first + * date will be used to determine the size of the fine */ - public static async calculateFinesOnDate({ userTypes, userIds, referenceDate }: CalculateFinesParams): Promise { - const debtorsOnReferenceDate = await BalanceService.getBalances({ - maxBalance: DineroTransformer.Instance.from(-500), - date: referenceDate, - userTypes, - ids: userIds, - }); + public static async calculateFinesOnDate({ userTypes, userIds, referenceDates }: CalculateFinesParams): Promise { + if (referenceDates.length === 0) throw new Error('No reference dates given.'); - const debtorsNow = await BalanceService.getBalances({ + const balances = await Promise.all(referenceDates.map((date) => BalanceService.getBalances({ maxBalance: DineroTransformer.Instance.from(-500), + date, userTypes, ids: userIds, - }); - const debtorsNowIds = debtorsNow.records.map((b) => b.id); + }))); - const userBalancesToFine = debtorsOnReferenceDate.records.filter((b) => debtorsNowIds.includes(b.id)); + const [debtorsOnReferenceDate, ...debtors] = balances; + const userBalancesToFine = debtorsOnReferenceDate.records + .filter((d1) => debtors.every((b) => b.records + .some((d2) => d1.id === d2.id))); return userBalancesToFine.map((u) => { const fine = calculateFine(u.amount); return { id: u.id, - amount: { + fineAmount: { amount: fine.getAmount(), currency: fine.getCurrency(), precision: fine.getPrecision(), }, + balances: balances.map((balance) => balance.records.find((b) => b.id === u.id)), }; }); } /** * Write fines in a single database transaction to database for all given user ids. - * @param referenceDate Date to base fines on. If undefined, the date of the previous fines will be used. If this is the first fine, use now. + * @param referenceDate Date to base fines on * @param userIds Ids of all users to fine * @param createdBy User handing out fines */ @@ -191,10 +191,8 @@ export default class DebtorService { take: 1, }))[0]; - const date = referenceDate || previousFineGroup?.createdAt || new Date(); - const balances = await BalanceService.getBalances({ - date, + date: referenceDate, ids: userIds, }); @@ -202,7 +200,7 @@ export default class DebtorService { const { fines: fines1, fineHandoutEvent: fineHandoutEvent1, emails: emails1 } = await getConnection().transaction(async (manager) => { // Create a new fine group to "connect" all these fines const fineHandoutEvent = Object.assign(new FineHandoutEvent(), { - referenceDate: date, + referenceDate, createdBy, }); await manager.save(fineHandoutEvent); @@ -232,7 +230,7 @@ export default class DebtorService { const transfer = await TransferService.createTransfer({ amount: amount.toObject(), fromId: user.id, - description: `Fine for balance of ${dinero({ amount: b.amount.amount }).toFormat()} on ${date.toLocaleDateString()}.`, + description: `Fine for balance of ${dinero({ amount: b.amount.amount }).toFormat()} on ${referenceDate.toLocaleDateString()}.`, toId: undefined, }, manager); @@ -240,7 +238,7 @@ export default class DebtorService { name: user.firstName, fine: amount, balance: DineroTransformer.Instance.from(b.amount.amount), - referenceDate: date, + referenceDate, totalFine: userFineGroup.fines.reduce((sum, f) => sum.add(f.amount), dinero({ amount :0 })).add(amount), }) }); @@ -325,29 +323,16 @@ export default class DebtorService { public static async sendFineWarnings({ referenceDate, userIds, }: HandOutFinesParams): Promise { - const previousFineGroup = (await FineHandoutEvent.find({ - order: { id: 'desc' }, - relations: ['fines', 'fines.userFineGroup'], - take: 1, - }))[0]; - - const date = referenceDate || previousFineGroup?.createdAt || new Date(); - - const balances = await BalanceService.getBalances({ - date, - ids: userIds, - }); - - const fines = await this.calculateFinesOnDate({ userIds, referenceDate }); + const fines = await this.calculateFinesOnDate({ userIds, referenceDates: [referenceDate] }); await Promise.all(fines.map(async (f) => { const user = await User.findOne({ where: { id: f.id } }); - const balance = balances.records.find((b) => b.id === f.id); + const balance = f.balances[0]; if (balance == null) throw new Error('Missing balance'); return Mailer.getInstance().send(user, new UserWillGetFined({ name: user.firstName, - referenceDate: date, - fine: dinero(f.amount as any), + referenceDate: referenceDate, + fine: dinero(f.fineAmount as any), balance: dinero(balance.amount as any), })); })); diff --git a/src/subscriber/index.ts b/src/subscriber/index.ts new file mode 100644 index 000000000..c15a3abc8 --- /dev/null +++ b/src/subscriber/index.ts @@ -0,0 +1,20 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +export { default as TransactionSubscriber } from './transaction-subscriber'; +export { default as TransferSubscriber } from './transfer-subscriber'; diff --git a/src/subscriber/transaction-subscriber.ts b/src/subscriber/transaction-subscriber.ts new file mode 100644 index 000000000..fbeee67c9 --- /dev/null +++ b/src/subscriber/transaction-subscriber.ts @@ -0,0 +1,70 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm'; +import Transaction from '../entity/transactions/transaction'; +import User from '../entity/user/user'; +import BalanceService from '../service/balance-service'; +import Mailer from '../mailer'; +import UserDebtNotification from '../mailer/templates/user-debt-notification'; +import DineroTransformer from '../entity/transformer/dinero-transformer'; +import { getLogger } from 'log4js'; + +@EventSubscriber() +export default class TransactionSubscriber implements EntitySubscriberInterface { + listenTo(): Function | string { + return Transaction; + } + + async afterInsert(event: InsertEvent): Promise { + if (process.env.NODE_ENV === 'test') return; + let { entity } = event; + if (entity.subTransactions == null + || (entity.subTransactions.length > 0 && entity.subTransactions[0].subTransactionRows == null) + || (entity.subTransactions.length > 0 && entity.subTransactions[0].subTransactionRows.length > 0 && entity.subTransactions[0].subTransactionRows[0].product == null)) { + entity = await event.manager.findOne(Transaction, { + where: { id: entity.id }, + relations: ['subTransactions', 'subTransactions.subTransactionRows', 'subTransactions.subTransactionRows.product'], + }); + } + + const user = await event.manager.findOne(User, { where: { id: entity.from.id } }); + const balance = await BalanceService.getBalance(user.id); + + let currentBalance = balance.amount.amount; + if (balance.lastTransactionId < event.entity.id) { + currentBalance -= entity.subTransactions[0].subTransactionRows[0].amount * entity.subTransactions[0].subTransactionRows[0].product.priceInclVat.getAmount(); + } + + if (currentBalance >= 0) return; + // User is now in debt + + const balanceBefore = await BalanceService.getBalance( + user.id, + new Date(entity.createdAt.getTime() - 1), + ); + + if (balanceBefore.amount.amount < 0) return; + // User was not in debt before this new transaction + + Mailer.getInstance().send(user, new UserDebtNotification({ + name: user.firstName, + balance: DineroTransformer.Instance.from(currentBalance), + url: '', + })).catch((e) => getLogger('Transaction').error(e)); + } +} diff --git a/src/subscriber/transfer-subscriber.ts b/src/subscriber/transfer-subscriber.ts new file mode 100644 index 000000000..59b926015 --- /dev/null +++ b/src/subscriber/transfer-subscriber.ts @@ -0,0 +1,49 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm'; +import Transfer from '../entity/transactions/transfer'; +import User from '../entity/user/user'; +import BalanceService from '../service/balance-service'; + +@EventSubscriber() +export default class TransferSubscriber implements EntitySubscriberInterface { + listenTo(): Function | string { + return Transfer; + } + + async afterInsert(event: InsertEvent) { + if (event.entity.toId == null) return; + + const user = await event.manager.findOne(User, { where: { id: event.entity.toId }, relations: ['currentFines'] }); + if (user.currentFines == null) return; + + const balance = await BalanceService.getBalance(user.id); + + // If the new transfer is not included in the balance calculation, add it manually + let currentBalance = balance.amount.amount; + if (balance.lastTransferId < event.entity.id) { + currentBalance += event.entity.amount.getAmount(); + } + + // Remove currently unpaid fines when new balance is positive. + if (currentBalance >= 0) { + user.currentFines = null; + await user.save(); + } + } +} diff --git a/test/seed.ts b/test/seed.ts index e3f8d8970..6041715fc 100644 --- a/test/seed.ts +++ b/test/seed.ts @@ -60,6 +60,7 @@ import Fine from '../src/entity/fine/fine'; import { calculateBalance } from './helpers/balance'; import GewisUser from '../src/gewis/entity/gewis-user'; import AssignedRole from '../src/entity/roles/assigned-role'; +import MemberAuthenticator from '../src/entity/authenticator/member-authenticator'; function getDate(startDate: Date, endDate: Date, i: number): Date { const diff = endDate.getTime() - startDate.getTime(); @@ -186,6 +187,27 @@ export async function seedRoles(users: User[]): Promise { }))).filter((r) => r != null); } +/** + * Seed some member authenticators + * @param users Users that can authenticate as organs + * @param authenticateAs + */ +export async function seedMemberAuthenticators(users: User[], authenticateAs: User[]): Promise { + const memberAuthenticators: MemberAuthenticator[] = []; + await Promise.all(authenticateAs.map(async (as, i) => { + return Promise.all(users.map(async (user, j) => { + if ((i + j) % 7 > 1) return; + const authenticator = Object.assign(new MemberAuthenticator(), { + userId: user.id, + authenticateAsId: as.id, + } as MemberAuthenticator); + await authenticator.save(); + memberAuthenticators.push(authenticator); + })); + })); + return memberAuthenticators; +} + export function defineInvoiceEntries(invoiceId: number, startEntryId: number, transactions: Transaction[]): { invoiceEntries: InvoiceEntry[], cost: number } { const invoiceEntries: InvoiceEntry[] = []; @@ -1361,6 +1383,10 @@ export interface DatabaseContent { export default async function seedDatabase(): Promise { const users = await seedUsers(); + await seedMemberAuthenticators( + users.filter((u) => u.type !== UserType.ORGAN), + [users.filter((u) => u.type === UserType.ORGAN)[0]], + ); const pinUsers = await seedHashAuthenticator(users, PinAuthenticator); const localUsers = await seedHashAuthenticator(users, LocalAuthenticator); const gewisUsers = await seedGEWISUsers(users); diff --git a/test/unit/controller/debtor-controller.ts b/test/unit/controller/debtor-controller.ts index 72e30d6cb..087d25aaf 100644 --- a/test/unit/controller/debtor-controller.ts +++ b/test/unit/controller/debtor-controller.ts @@ -266,62 +266,82 @@ describe('DebtorController', () => { }); }); - describe('DELETE /fines/{id}', () => { - it('should delete fine if admin', async () => { - const fine = ctx.fines[0]; + describe('GET /fines/eligible', () => { + it('should return list of fines', async () => { + const referenceDates = [new Date('2021-02-12')]; const res = await request(ctx.app) - .delete(`/fines/single/${fine.id}`) + .get('/fines/eligible') + .query({ + referenceDates, + }) .set('Authorization', `Bearer ${ctx.adminToken}`); - expect(res.status).to.equal(204); - expect(res.body).to.be.empty; - }); - it('should return 404 if fine does not exist', async () => { - const id = 9999999; - const fine = await Fine.findOne({ where: { id } }); - expect(fine).to.be.null; + expect(res.status).to.equal(200); - const res = await request(ctx.app) - .delete(`/fines/single/${id}`) - .set('Authorization', `Bearer ${ctx.adminToken}`); - expect(res.status).to.equal(404); - expect(res.body).to.be.empty; - }); - it('should return 403 if not admin', async () => { - const fine = ctx.fines[1]; - const res = await request(ctx.app) - .delete(`/fines/single/${fine.id}`) - .set('Authorization', `Bearer ${ctx.userToken}`); - expect(res.status).to.equal(403); - expect(res.body).to.be.empty; + const fines = res.body as UserToFineResponse[]; + fines.forEach((f) => { + const user = ctx.users.find((u) => u.id === f.id); + expect(user).to.not.be.undefined; + const balance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines, referenceDates[0]).amount.getAmount(); + expect(f.fineAmount.amount).to.equal(calculateFine(balance)); + expect(f.balances.length).to.equal(1); + expect(f.balances[0].amount.amount).to.equal(balance); + expect(f.balances[0].date).to.equal(referenceDates[0].toISOString()); + }); }); - }); - - describe('GET /fines/eligible', () => { - it('should correctly return list of possible fines', async () => { - const userTypes = [UserType.LOCAL_USER, UserType.MEMBER]; + it('should return list of fines having multiple reference dates', async () => { + const referenceDates = [new Date('2021-02-12'), new Date()]; const res = await request(ctx.app) .get('/fines/eligible') - .query({ userTypes: userTypes.map((t) => UserType[t]) }) + .query({ + referenceDates, + }) .set('Authorization', `Bearer ${ctx.adminToken}`); expect(res.status).to.equal(200); const fines = res.body as UserToFineResponse[]; - fines.forEach((fine) => { - const validation = ctx.specification.validateModel('UserToFineResponse', fine, false, true); - expect(validation.valid).to.be.true; + const actualUsers = ctx.users.filter((u) => referenceDates.every((date) => + calculateBalance(u, ctx.transactions, ctx.subTransactions, ctx.transfers, date).amount.getAmount() < -500)); + expect(fines.length).to.equal(actualUsers.length); + expect(fines.map((f) => f.id)).to.deep.equalInAnyOrder(actualUsers.map((u) => u.id)); + + fines.forEach((f) => { + const user = ctx.users.find((u) => u.id === f.id); + expect(user).to.not.be.undefined; + + referenceDates.forEach((date, i) => { + const balance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines, date).amount.getAmount(); + expect(f.balances[i].date).to.equal(date.toISOString()); + expect(f.balances[i].amount.amount).to.equal(balance); + + if (i === 0) { + expect(f.fineAmount.amount).to.equal(calculateFine(balance)); + } + }); }); }); - it('should correctly return list of possible fines for user types', async () => { + it('should return list of fines for users of given userType', async () => { const userTypes = [UserType.LOCAL_USER, UserType.MEMBER]; + const referenceDates = [new Date('2021-02-12')]; const res = await request(ctx.app) .get('/fines/eligible') - .query({ userTypes: userTypes.map((t) => UserType[t]) }) + .query({ + userTypes: userTypes.map((t) => UserType[t]), + referenceDates, + }) .set('Authorization', `Bearer ${ctx.adminToken}`); expect(res.status).to.equal(200); const fines = res.body as UserToFineResponse[]; + const actualUsers = ctx.users.filter((u) => userTypes.includes(u.type) && referenceDates.every((date) => + calculateBalance(u, ctx.transactions, ctx.subTransactions, ctx.transfers, date).amount.getAmount() < -500)); + expect(fines.length).to.equal(actualUsers.length); + expect(fines.map((f) => f.id)).to.deep.equalInAnyOrder(actualUsers.map((u) => u.id)); + fines.forEach((f) => { const user = ctx.users.find((u) => u.id === f.id); + expect(user).to.not.be.undefined; + const balance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines, referenceDates[0]).amount.getAmount(); + expect(f.fineAmount.amount).to.equal(calculateFine(balance)); expect(userTypes).to.include(user.type); }); }); @@ -339,26 +359,6 @@ describe('DebtorController', () => { .set('Authorization', `Bearer ${ctx.adminToken}`); expect(res.status).to.equal(400); }); - it('should return list of fines based on reference date', async () => { - const userTypes = [UserType.LOCAL_USER, UserType.MEMBER]; - const referenceDate = new Date('2021-02-12'); - const res = await request(ctx.app) - .get('/fines/eligible') - .query({ - userTypes: userTypes.map((t) => UserType[t]), - referenceDate: referenceDate.toISOString(), - }) - .set('Authorization', `Bearer ${ctx.adminToken}`); - expect(res.status).to.equal(200); - - const fines = res.body as UserToFineResponse[]; - fines.forEach((f) => { - const user = ctx.users.find((u) => u.id === f.id); - expect(user).to.not.be.undefined; - const balance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers, referenceDate).amount.getAmount(); - expect(f.amount.amount).to.equal(calculateFine(balance)); - }); - }); it('should return 400 when referenceDate is not a valid date', async () => { const userTypes = [UserType.LOCAL_USER, UserType.MEMBER]; const res = await request(ctx.app) @@ -380,12 +380,42 @@ describe('DebtorController', () => { }); }); + describe('DELETE /fines/{id}', () => { + it('should delete fine if admin', async () => { + const fine = ctx.fines[0]; + const res = await request(ctx.app) + .delete(`/fines/single/${fine.id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(204); + expect(res.body).to.be.empty; + }); + it('should return 404 if fine does not exist', async () => { + const id = 9999999; + const fine = await Fine.findOne({ where: { id } }); + expect(fine).to.be.null; + + const res = await request(ctx.app) + .delete(`/fines/single/${id}`) + .set('Authorization', `Bearer ${ctx.adminToken}`); + expect(res.status).to.equal(404); + expect(res.body).to.be.empty; + }); + it('should return 403 if not admin', async () => { + const fine = ctx.fines[1]; + const res = await request(ctx.app) + .delete(`/fines/single/${fine.id}`) + .set('Authorization', `Bearer ${ctx.userToken}`); + expect(res.status).to.equal(403); + expect(res.body).to.be.empty; + }); + }); + describe('POST /fines/handout', () => { it('should correctly hand out fines to given users', async () => { const res = await request(ctx.app) .post('/fines/handout') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send({ userIds: ctx.users.map((u) => u.id) }); + .send({ userIds: ctx.users.map((u) => u.id), referenceDate: new Date() }); expect(res.status).to.equal(200); const fineHandoutEventResponse = res.body as FineHandoutEventResponse; @@ -398,7 +428,7 @@ describe('DebtorController', () => { const res = await request(ctx.app) .post('/fines/handout') .set('Authorization', `Bearer ${ctx.userToken}`) - .send({ userIds: ctx.users.map((u) => u.id) }); + .send({ userIds: ctx.users.map((u) => u.id), referenceDate: new Date() }); expect(res.status).to.equal(403); }); it('should return 400 if userIds is not a list', async () => { @@ -426,6 +456,13 @@ describe('DebtorController', () => { const fineHandoutEventResponse = res.body as FineHandoutEventResponse; expect(fineHandoutEventResponse.referenceDate).to.equal(referenceDate.toISOString()); }); + it('should return 400 if no reference date', async () => { + const res = await request(ctx.app) + .post('/fines/handout') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ userIds: ctx.users.map((u) => u.id) }); + expect(res.status).to.equal(400); + }); it('should return 400 if invalid reference date', async () => { const res = await request(ctx.app) .post('/fines/handout') @@ -444,7 +481,7 @@ describe('DebtorController', () => { const res = await request(ctx.app) .post('/fines/handout') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send({ userIds: [] }); + .send({ userIds: [], referenceDate: new Date() }); expect(res.status).to. equal(200); const fineHandoutEventResponse = res.body as FineHandoutEventResponse; @@ -457,7 +494,7 @@ describe('DebtorController', () => { const res = await request(ctx.app) .post('/fines/notify') .set('Authorization', `Bearer ${ctx.adminToken}`) - .send({ userIds: ctx.users.map((u) => u.id) }); + .send({ userIds: ctx.users.map((u) => u.id), referenceDate: new Date() }); expect(res.status).to.equal(204); const body = res.body as FineHandoutEventResponse; @@ -468,7 +505,7 @@ describe('DebtorController', () => { const res = await request(ctx.app) .post('/fines/notify') .set('Authorization', `Bearer ${ctx.userToken}`) - .send({ userIds: ctx.users.map((u) => u.id) }); + .send({ userIds: ctx.users.map((u) => u.id), referenceDate: new Date() }); expect(res.status).to.equal(403); }); it('should return 400 if userIds is not a list', async () => { @@ -485,6 +522,13 @@ describe('DebtorController', () => { .send({ userIds: ['WieDitLeestTrektBak'] }); expect(res.status).to.equal(400); }); + it('should return 400 if no reference date', async () => { + const res = await request(ctx.app) + .post('/fines/notify') + .set('Authorization', `Bearer ${ctx.adminToken}`) + .send({ userIds: ctx.users.map((u) => u.id) }); + expect(res.status).to.equal(400); + }); it('should return 400 if invalid reference date', async () => { const res = await request(ctx.app) .post('/fines/notify') diff --git a/test/unit/controller/user-controller.ts b/test/unit/controller/user-controller.ts index 299c8a104..c95fee741 100644 --- a/test/unit/controller/user-controller.ts +++ b/test/unit/controller/user-controller.ts @@ -578,16 +578,18 @@ describe('UserController', (): void => { .get(`/users/${user.id}/members`) .set('Authorization', `Bearer ${ctx.adminToken}`); expect(res.status).to.equal(200); - expect(ctx.specification.validateModel( + const validation = ctx.specification.validateModel( 'PaginatedUserResponse', res.body, false, true, - ).valid).to.be.true; + ); + expect(validation.valid).to.be.true; + expect(res.body.records.length).to.be.greaterThan(0); }); it('should return an HTTP 200 and all the members of the organ', async () => { await inUserContext(await (await UserFactory()).clone(3), async (...users: User[]) => { - const organ = await User.findOne({ where: { type: UserType.ORGAN } }); + const organ = (await User.find({ where: { type: UserType.ORGAN } }))[2]; const promises: Promise[] = []; users.forEach((user) => { const auth = Object.assign(new MemberAuthenticator(), { @@ -1054,12 +1056,14 @@ describe('UserController', (): void => { .get('/users/1/pointsofsale') .set('Authorization', `Bearer ${ctx.userToken}`); expect(res.status).to.equal(200); - expect(ctx.specification.validateModel( - 'PaginatedContainerResponse', + const validation = ctx.specification.validateModel( + 'PaginatedPointOfSaleResponse', res.body, false, true, - ).valid).to.be.true; + ); + expect(validation.valid).to.be.true; + expect(res.body.records.length).to.be.greaterThan(0); }); it('should give an HTTP 200 when requesting own points of sale', async () => { const res = await request(ctx.app) diff --git a/test/unit/service/balance-service.ts b/test/unit/service/balance-service.ts index e94df5336..e853654ed 100644 --- a/test/unit/service/balance-service.ts +++ b/test/unit/service/balance-service.ts @@ -125,6 +125,7 @@ describe('BalanceService', (): void => { expect(user).to.not.be.undefined; const actualBalance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers); expect(balance.amount.amount).to.equal(actualBalance.amount.getAmount()); + expect(new Date().getTime() - new Date(balance.date).getTime()).to.be.at.most(1000); await checkFine(balance, user); })); }); @@ -137,6 +138,7 @@ describe('BalanceService', (): void => { expect(user).to.not.be.undefined; const actualBalance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers, date); expect(balance.amount.amount).to.equal(actualBalance.amount.getAmount()); + expect(balance.date).to.equal(date.toISOString()); await checkFine(balance, user); })); }); @@ -437,34 +439,44 @@ describe('BalanceService', (): void => { expect(balance.amount.amount).to.equal(0); expect(balance.fine).to.be.null; expect(balance.fineSince).to.be.null; + expect(balance.lastTransactionId).to.equal(-1); + expect(balance.lastTransferId).to.equal(-1); }); it('should return correct balance for new user with single outgoing transaction', async () => { const newUser = await (await UserFactory()).get(); - const { amount } = await addTransaction(newUser, ctx.pointOfSaleRevisions, false); + const { transaction, amount } = await addTransaction(newUser, ctx.pointOfSaleRevisions, false); const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(-amount.amount); + expect(balance.lastTransactionId).to.equal(transaction.id); + expect(balance.lastTransferId).to.equal(-1); }); it('should return correct balance for new user with single incoming transaction', async () => { const newUser = await (await UserFactory()).get(); - const { amount } = await addTransaction(newUser, ctx.pointOfSaleRevisions, true); + const { transaction, amount } = await addTransaction(newUser, ctx.pointOfSaleRevisions, true); const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(amount.amount); + expect(balance.lastTransactionId).to.equal(transaction.id); + expect(balance.lastTransferId).to.equal(-1); }); it('should correctly return balance for new user with single outgoing transfer', async () => { const newUser = await (await UserFactory()).get(); - const { amount } = await addTransfer(newUser, ctx.users, false); + const { transfer, amount } = await addTransfer(newUser, ctx.users, false); const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(-amount.amount); + expect(balance.lastTransactionId).to.equal(-1); + expect(balance.lastTransferId).to.equal(transfer.id); }); it('should correctly return balance for new user with single incoming transfer', async () => { const newUser = await (await UserFactory()).get(); - const { amount } = await addTransfer(newUser, ctx.users, true); + const { transfer, amount } = await addTransfer(newUser, ctx.users, true); const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(amount.amount); + expect(balance.lastTransactionId).to.equal(-1); + expect(balance.lastTransferId).to.equal(transfer.id); }); it('should return correct balance for new user with two outgoing transactions and balance cache', async () => { const newUser = await (await UserFactory()).get(); @@ -481,6 +493,8 @@ describe('BalanceService', (): void => { const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(-amount.amount - transaction2.amount.amount); + expect(balance.lastTransactionId).to.equal(transaction2.transaction.id); + expect(balance.lastTransferId).to.equal(-1); }); it('should return correct balance for new user with two incoming transactions and balance cache', async () => { const newUser = await (await UserFactory()).get(); @@ -498,6 +512,8 @@ describe('BalanceService', (): void => { const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(amount.amount + transaction2.amount.amount); + expect(balance.lastTransactionId).to.equal(transaction2.transaction.id); + expect(balance.lastTransferId).to.equal(-1); }); it('should correctly return balance for new user with two outgoing transfers with balance cache', async () => { const newUser = await (await UserFactory()).get(); @@ -514,6 +530,8 @@ describe('BalanceService', (): void => { const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(-amount.amount - transfer2.amount.amount); + expect(balance.lastTransactionId).to.equal(-1); + expect(balance.lastTransferId).to.equal(transfer2.transfer.id); }); it('should correctly return balance for new user with single incoming transfer', async () => { const newUser = await (await UserFactory()).get(); @@ -530,6 +548,8 @@ describe('BalanceService', (): void => { const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(amount.amount + transfer2.amount.amount); + expect(balance.lastTransactionId).to.equal(-1); + expect(balance.lastTransferId).to.equal(transfer2.transfer.id); }); it('should correctly return balance for new user with incoming and outgoing transactions and transfers', async () => { const newUser = await (await UserFactory()).get(); @@ -627,5 +647,22 @@ describe('BalanceService', (): void => { expect(actualBalance.amount.amount).to.equal(expectedBalance.amount.getAmount()); } }); + it('should use balance cache when determining lastTransactionId', async () => { + const newUser = await (await UserFactory()).get(); + const { + transaction, amount, + } = await addTransaction(newUser, ctx.pointOfSaleRevisions, false, new Date(new Date().getTime() - 5000)); + await Balance.save([{ + userId: newUser.id, + user: newUser, + lastTransaction: transaction, + amount: DineroTransformer.Instance.from(-amount.amount), + } as any]); + + const balance = await BalanceService.getBalance(newUser.id); + expect(balance.amount.amount).to.equal(-amount.amount); + expect(balance.lastTransactionId).to.equal(transaction.id); + expect(balance.lastTransferId).to.equal(-1); + }); }); }); diff --git a/test/unit/service/debtor-service.ts b/test/unit/service/debtor-service.ts index a9ee180c5..32bdf08ee 100644 --- a/test/unit/service/debtor-service.ts +++ b/test/unit/service/debtor-service.ts @@ -117,7 +117,10 @@ describe('DebtorService', (): void => { describe('calculateFinesOnDate', () => { it('should return everyone who should get fined', async () => { - const calculatedFines = await DebtorService.calculateFinesOnDate({}); + const now = new Date(); + const calculatedFines = await DebtorService.calculateFinesOnDate({ + referenceDates: [now], + }); const usersToFine = ctx.users .map((u) => calculateBalance(u, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines)) .filter((b) => b.amount.getAmount() <= -500); @@ -126,7 +129,10 @@ describe('DebtorService', (): void => { calculatedFines.forEach((f) => { const u = usersToFine.find((b) => b.user.id === f.id); expect(u).to.not.be.undefined; - expect(f.amount.amount).to.equal(calculateFine(u.amount.getAmount())); + expect(f.fineAmount.amount).to.equal(calculateFine(u.amount.getAmount())); + expect(f.balances.length).to.equal(1); + expect(f.balances[0].date).to.equal(now.toISOString()); + expect(f.balances[0].amount.amount).to.equal(u.amount.getAmount()); }); }); @@ -134,6 +140,7 @@ describe('DebtorService', (): void => { const userTypes = [UserType.LOCAL_USER, UserType.INVOICE]; const calculatedFines = await DebtorService.calculateFinesOnDate({ userTypes, + referenceDates: [new Date()], }); const usersToFine = ctx.users .filter((u) => userTypes.includes(u.type)) @@ -145,25 +152,36 @@ describe('DebtorService', (): void => { const u = usersToFine.find((b) => b.user.id === f.id); expect(u).to.not.be.undefined; expect(userTypes).to.include(u.user.type); - expect(f.amount.amount).to.equal(calculateFine(u.amount.getAmount())); + expect(f.fineAmount.amount).to.equal(calculateFine(u.amount.getAmount())); }); }); it('should only return users that have more than 5 euros debt now and on reference date', async () => { - const referenceDate = new Date('2021-02-12'); + const referenceDates = [new Date('2021-02-12'), new Date()]; const calculatedFines = await DebtorService.calculateFinesOnDate({ - referenceDate, + referenceDates, }); const usersToFine = ctx.users - .map((u) => calculateBalance(u, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines, referenceDate)) + .map((u) => calculateBalance(u, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines, referenceDates[0])) .filter((b) => b.amount.getAmount() <= -500) - .filter((b) => calculateBalance(b.user, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines).amount.getAmount() <= -500); + .filter((b) => calculateBalance(b.user, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines, referenceDates[1]).amount.getAmount() <= -500); expect(calculatedFines.length).to.equal(usersToFine.length); calculatedFines.forEach((f) => { const u = usersToFine.find((b) => b.user.id === f.id); expect(u).to.not.be.undefined; - expect(f.amount.amount).to.equal(calculateFine(u.amount.getAmount())); + expect(f.fineAmount.amount).to.equal(calculateFine(u.amount.getAmount())); + expect(f.balances.length).to.equal(referenceDates.length); + + const [firstBalance, ...balances] = f.balances; + expect(firstBalance.amount.amount).to.equal(u.amount.getAmount()); + expect(firstBalance.date).to.equal(referenceDates[0].toISOString()); + balances.forEach((b, index) => { + const actualBalance = calculateBalance(u.user, ctx.transactions, ctx.subTransactions, ctx.transfersInclFines, new Date(b.date)); + expect(b.amount.amount).to.equal(actualBalance.amount.getAmount()); + expect(b.date).to.equal(referenceDates[index + 1].toISOString()); + }); + }); }); it('should return 1 euro fine with a balance of exactly -5 euros', async () => { @@ -180,10 +198,12 @@ describe('DebtorService', (): void => { const balance = await BalanceService.getBalance(newUser.id); expect(balance.amount.amount).to.equal(-500); - const fines = await DebtorService.calculateFinesOnDate({}); + const fines = await DebtorService.calculateFinesOnDate({ + referenceDates: [new Date()], + }); const fineForUser = fines.find((f) => f.id === newUser.id); expect(fineForUser).to.not.be.undefined; - expect(fineForUser.amount.amount).to.equal(100); + expect(fineForUser.fineAmount.amount).to.equal(100); await Transfer.remove(transfer); await User.remove(newUser); @@ -365,33 +385,10 @@ describe('DebtorService', (): void => { expect(f.transfer.description).to.equal(`Fine for balance of ${balString} on ${date.toLocaleDateString()}.`); } - it('should correctly create first fines without reference date', async () => { - const usersToFine = await DebtorService.calculateFinesOnDate({}); - const fineHandoutEvent = await DebtorService.handOutFines({ - userIds: usersToFine.map((u) => u.id), - }, ctx.actor); - expect(fineHandoutEvent.fines.length).to.equal(usersToFine.length); - expect(new Date().getTime() - new Date(fineHandoutEvent.referenceDate).getTime()) - .to.be.at.most(1000); - expect(fineHandoutEvent.createdBy.id).to.equal(ctx.actor.id); - - const fines = await Promise.all(fineHandoutEvent.fines.map((f) => Fine - .findOne({ where: { id: f.id }, relations: ['transfer', 'transfer.from', 'fineHandoutEvent', 'userFineGroup', 'userFineGroup.user'] }))); - - await Promise.all(fines.map(async (f) => { - const preCalcedFine = usersToFine.find((u) => u.id === f.userFineGroup.userId); - expect(preCalcedFine).to.not.be.undefined; - expect(f.amount.getAmount()).to.equal(preCalcedFine.amount.amount); - expect(new Date().getTime() - new Date(fineHandoutEvent.referenceDate).getTime()).to.be.at.most(1000); - await checkFine(f, new Date(), fineHandoutEvent); - })); - - await checkCorrectNewBalance(fines); - }); it('should correctly create fines with reference date', async () => { const referenceDate = new Date('2021-01-30'); const usersToFine = await DebtorService.calculateFinesOnDate({ - referenceDate, + referenceDates: [referenceDate], }); const fineHandoutEvent = await DebtorService.handOutFines({ userIds: usersToFine.map((u) => u.id), @@ -407,46 +404,7 @@ describe('DebtorService', (): void => { await Promise.all(fines.map(async (f) => { const preCalcedFine = usersToFine.find((u) => u.id === f.userFineGroup.userId); expect(preCalcedFine).to.not.be.undefined; - expect(f.amount.getAmount()).to.equal(preCalcedFine.amount.amount); - expect(new Date(fineHandoutEvent.referenceDate).getTime()).to.equal(referenceDate.getTime()); - await checkFine(f, referenceDate, fineHandoutEvent); - })); - - await checkCorrectNewBalance(fines); - }); - it('should correctly calculate fines based on date of previous fines', async () => { - const oldRef = new Date('2020-08-01'); - const referenceDate = new Date('2021-01-30'); - - // Two finegroups, so we can check that the newest one is used - await Object.assign(new FineHandoutEvent(), { - createdAt: oldRef, - updatedAt: oldRef, - referenceDate: oldRef, - }).save(); - await Object.assign(new FineHandoutEvent(), { - createdAt: referenceDate, - updatedAt: referenceDate, - referenceDate, - }).save(); - - const usersToFine = await DebtorService.calculateFinesOnDate({ - referenceDate, - }); - const fineHandoutEvent = await DebtorService.handOutFines({ - userIds: usersToFine.map((u) => u.id), - }, ctx.actor); - expect(fineHandoutEvent.fines.length).to.equal(usersToFine.length); - expect(fineHandoutEvent.referenceDate).to.equal(referenceDate.toISOString()); - expect(fineHandoutEvent.createdBy.id).to.equal(ctx.actor.id); - - const fines = await Promise.all(fineHandoutEvent.fines.map((f) => Fine - .findOne({ where: { id: f.id }, relations: ['transfer', 'transfer.from', 'fineHandoutEvent', 'userFineGroup', 'userFineGroup.user'] }))); - - await Promise.all(fines.map(async (f) => { - const preCalcedFine = usersToFine.find((u) => u.id === f.userFineGroup.userId); - expect(preCalcedFine).to.not.be.undefined; - expect(f.amount.getAmount()).to.equal(preCalcedFine.amount.amount); + expect(f.amount.getAmount()).to.equal(preCalcedFine.fineAmount.amount); expect(new Date(fineHandoutEvent.referenceDate).getTime()).to.equal(referenceDate.getTime()); await checkFine(f, referenceDate, fineHandoutEvent); })); @@ -456,16 +414,18 @@ describe('DebtorService', (): void => { it('should correctly put two fines in same userFineGroup', async () => { const referenceDate = new Date('2021-01-30'); const usersToFine = await DebtorService.calculateFinesOnDate({ - referenceDate, + referenceDates: [referenceDate], }); const user = usersToFine[0]; expect(user).to.not.be.undefined; const fineHandoutEvent1 = await DebtorService.handOutFines({ userIds: [user.id], + referenceDate, }, ctx.actor); const fineHandoutEvent2 = await DebtorService.handOutFines({ userIds: [user.id], + referenceDate: new Date(), }, ctx.actor); expect(fineHandoutEvent1.fines.length).to.equal(1); @@ -485,7 +445,7 @@ describe('DebtorService', (): void => { expect(ids).to.include(fineHandoutEvent2.fines[0].id); }); it('should create no fines if empty list of userIds is given', async () => { - const fineHandoutEvent = await DebtorService.handOutFines({ userIds: [] }, ctx.actor); + const fineHandoutEvent = await DebtorService.handOutFines({ userIds: [], referenceDate: new Date() }, ctx.actor); expect(fineHandoutEvent.fines.length).to.equal(0); expect(await Fine.count()).to.equal(0); @@ -495,7 +455,7 @@ describe('DebtorService', (): void => { let dbUser = await User.findOne({ where: { id: user.id }, relations: ['currentFines'] }); expect(dbUser.currentFines).to.be.null; - const fineHandoutEvent = await DebtorService.handOutFines({ userIds: [user.id] }, ctx.actor); + const fineHandoutEvent = await DebtorService.handOutFines({ userIds: [user.id], referenceDate: new Date() }, ctx.actor); expect(fineHandoutEvent.fines.length).to.equal(1); const fine = fineHandoutEvent.fines[0]; expect(fine.user.id).to.equal(user.id); @@ -512,6 +472,7 @@ describe('DebtorService', (): void => { await DebtorService.handOutFines({ userIds: [user.id], + referenceDate: new Date(), }, ctx.actor); expect(sendMailFake).to.be.calledOnce; diff --git a/test/unit/entity/transactions/transaction.ts b/test/unit/subscribe/transaction-subscriber.ts similarity index 84% rename from test/unit/entity/transactions/transaction.ts rename to test/unit/subscribe/transaction-subscriber.ts index 30e855cd9..f395e5623 100644 --- a/test/unit/entity/transactions/transaction.ts +++ b/test/unit/subscribe/transaction-subscriber.ts @@ -16,11 +16,11 @@ * along with this program. If not, see . */ import { Connection } from 'typeorm'; -import User, { TermsOfServiceStatus, UserType } from '../../../../src/entity/user/user'; -import Transaction from '../../../../src/entity/transactions/transaction'; -import SubTransaction from '../../../../src/entity/transactions/sub-transaction'; -import Transfer from '../../../../src/entity/transactions/transfer'; -import Database from '../../../../src/database/database'; +import User, { TermsOfServiceStatus, UserType } from '../../../src/entity/user/user'; +import Transaction from '../../../src/entity/transactions/transaction'; +import SubTransaction from '../../../src/entity/transactions/sub-transaction'; +import Transfer from '../../../src/entity/transactions/transfer'; +import Database from '../../../src/database/database'; import { seedContainers, seedPointsOfSale, @@ -28,19 +28,19 @@ import { seedProducts, seedTransactions, seedTransfers, seedUsers, seedVatGroups, -} from '../../../seed'; -import { calculateBalance } from '../../../helpers/balance'; -import ProductRevision from '../../../../src/entity/product/product-revision'; -import ContainerRevision from '../../../../src/entity/container/container-revision'; -import PointOfSaleRevision from '../../../../src/entity/point-of-sale/point-of-sale-revision'; -import Mailer from '../../../../src/mailer'; +} from '../../seed'; +import { calculateBalance } from '../../helpers/balance'; +import ProductRevision from '../../../src/entity/product/product-revision'; +import ContainerRevision from '../../../src/entity/container/container-revision'; +import PointOfSaleRevision from '../../../src/entity/point-of-sale/point-of-sale-revision'; +import Mailer from '../../../src/mailer'; import sinon, { SinonSandbox, SinonSpy } from 'sinon'; import nodemailer, { Transporter } from 'nodemailer'; import { expect } from 'chai'; -import TransactionService from '../../../../src/service/transaction-service'; -import BalanceService from '../../../../src/service/balance-service'; +import TransactionService from '../../../src/service/transaction-service'; +import BalanceService from '../../../src/service/balance-service'; -describe('Transaction', () => { +describe('TransactionSubscriber', () => { let ctx: { connection: Connection, adminUser: User, @@ -121,16 +121,20 @@ describe('Transaction', () => { sendMailFake.resetHistory(); }); - describe('sendEmailNotificationIfNowInDebt', () => { + describe('afterInsert', () => { it('should send an email if someone gets into debt', async () => { - const user = ctx.usersNotInDebt[0]; + const user = ctx.usersNotInDebt[1]; const currentBalance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers).amount; expect(currentBalance.getAmount()).to.be.at.least(0); expect((await BalanceService.getBalance(user.id)).amount.amount).to.equal(currentBalance.getAmount()); - const pos = ctx.pointOfSales[0]; - const container = ctx.containers[0]; - const product = ctx.products[0]; + const pos = ctx.pointOfSales.find((p) => p.pointOfSale.owner.id !== user.id); + const container = ctx.containers.find((c) => c.container.owner.id !== user.id); + const product = ctx.products.find((p) => p.product.owner.id !== user.id); + + expect(pos).to.not.be.undefined; + expect(container).to.not.be.undefined; + expect(product).to.not.be.undefined; const amount = Math.ceil(currentBalance.getAmount() / product.priceInclVat.getAmount()) + 1 ; const totalPriceInclVat = product.priceInclVat.multiply(amount).toObject(); @@ -163,7 +167,7 @@ describe('Transaction', () => { expect(sendMailFake).to.be.calledOnce; }); it('should not send email if someone does not go into debt', async () => { - const user = ctx.usersNotInDebt[1]; + const user = ctx.usersNotInDebt[2]; const currentBalance = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers).amount; expect(currentBalance.getAmount()).to.be.at.least(0); expect((await BalanceService.getBalance(user.id)).amount.amount).to.equal(currentBalance.getAmount()); @@ -172,8 +176,8 @@ describe('Transaction', () => { const container = ctx.containers[0]; const product = ctx.products[0]; - const amount = Math.floor(currentBalance.getAmount() / product.priceInclVat.getAmount()) - 1; - expect(amount).to.be.at.least(0); + const amount = Math.floor(currentBalance.getAmount() / product.priceInclVat.getAmount()); + expect(amount).to.be.at.least(1); const totalPriceInclVat = product.priceInclVat.multiply(amount).toObject(); await TransactionService.createTransaction({ from: user.id, diff --git a/test/unit/entity/transactions/transfer.ts b/test/unit/subscribe/transfer-subscriber.ts similarity index 87% rename from test/unit/entity/transactions/transfer.ts rename to test/unit/subscribe/transfer-subscriber.ts index 4d0f12b32..64b016987 100644 --- a/test/unit/entity/transactions/transfer.ts +++ b/test/unit/subscribe/transfer-subscriber.ts @@ -16,10 +16,10 @@ * along with this program. If not, see . */ import { Connection } from 'typeorm'; -import User from '../../../../src/entity/user/user'; -import Transaction from '../../../../src/entity/transactions/transaction'; -import Transfer from '../../../../src/entity/transactions/transfer'; -import Database from '../../../../src/database/database'; +import User from '../../../src/entity/user/user'; +import Transaction from '../../../src/entity/transactions/transaction'; +import Transfer from '../../../src/entity/transactions/transfer'; +import Database from '../../../src/database/database'; import { seedContainers, seedPointsOfSale, @@ -27,17 +27,17 @@ import { seedProducts, seedTransactions, seedTransfers, seedUsers, seedVatGroups, -} from '../../../seed'; -import SubTransaction from '../../../../src/entity/transactions/sub-transaction'; -import { calculateBalance } from '../../../helpers/balance'; -import DebtorService from '../../../../src/service/debtor-service'; +} from '../../seed'; +import SubTransaction from '../../../src/entity/transactions/sub-transaction'; +import { calculateBalance } from '../../helpers/balance'; +import DebtorService from '../../../src/service/debtor-service'; import { expect } from 'chai'; -import { addTransfer } from '../../../helpers/transaction-helpers'; -import BalanceService from '../../../../src/service/balance-service'; +import { addTransfer } from '../../helpers/transaction-helpers'; +import BalanceService from '../../../src/service/balance-service'; import dinero from 'dinero.js'; -import Fine from '../../../../src/entity/fine/fine'; +import Fine from '../../../src/entity/fine/fine'; -describe('transfer', (): void => { +describe('TransferSubscriber', (): void => { let ctx: { connection: Connection, users: User[], @@ -76,15 +76,16 @@ describe('transfer', (): void => { await ctx.connection.destroy(); }); - describe('validateDebtPaid', () => { + describe('afterInsert', () => { it('should set currentFines to null when debt is paid', async () => { const user = ctx.usersInDebt[0]; expect(user).to.not.be.undefined; const debt = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers).amount; + expect(debt.getAmount()).to.be.lessThan(0); expect((await BalanceService.getBalance(user.id)).amount.amount).to.equal(debt.getAmount()); - const { fines } = await DebtorService.handOutFines({ userIds: [user.id] }, ctx.users[0]); + const { fines } = await DebtorService.handOutFines({ userIds: [user.id], referenceDate: new Date() }, ctx.users[0]); const fine = await Fine.findOne({ where: { id: fines[0].id }, relations: ['userFineGroup'], @@ -103,7 +104,7 @@ describe('transfer', (): void => { await addTransfer(user, [], true, undefined, transferAmount.getAmount()); const newBalance = await BalanceService.getBalance(user.id); - expect(newBalance.amount.amount).to.be.greaterThanOrEqual(0); + expect(newBalance.amount.amount).to.equal(0); dbUser = await User.findOne({ where: { id: user.id }, relations: ['currentFines'] }); expect(dbUser.currentFines).to.be.null; }); @@ -114,7 +115,7 @@ describe('transfer', (): void => { const debt = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers).amount; expect((await BalanceService.getBalance(user.id)).amount.amount).to.equal(debt.getAmount()); - const { fines } = await DebtorService.handOutFines({ userIds: [user.id] }, ctx.users[0]); + const { fines } = await DebtorService.handOutFines({ userIds: [user.id], referenceDate: new Date() }, ctx.users[0]); const fine = await Fine.findOne({ where: { id: fines[0].id }, relations: ['userFineGroup'], @@ -146,7 +147,7 @@ describe('transfer', (): void => { const debt = calculateBalance(user, ctx.transactions, ctx.subTransactions, ctx.transfers).amount; expect((await BalanceService.getBalance(user.id)).amount.amount).to.equal(debt.getAmount()); - const { fines } = await DebtorService.handOutFines({ userIds: [user.id] }, ctx.users[0]); + const { fines } = await DebtorService.handOutFines({ userIds: [user.id], referenceDate: new Date() }, ctx.users[0]); const fine = await Fine.findOne({ where: { id: fines[0].id }, relations: ['userFineGroup'], From 6d2415324962706a8bd9db69335fe9eeef5e890b Mon Sep 17 00:00:00 2001 From: Yoronex Date: Mon, 16 Oct 2023 07:55:24 +0200 Subject: [PATCH 07/13] Add invoice pagination to swagger spec --- src/controller/invoice-controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controller/invoice-controller.ts b/src/controller/invoice-controller.ts index 1a33c228b..867947b8a 100644 --- a/src/controller/invoice-controller.ts +++ b/src/controller/invoice-controller.ts @@ -93,6 +93,8 @@ export default class InvoiceController extends BaseController { * @param {boolean} returnEntries.query - Boolean if invoice entries should be returned * @param {string} fromDate.query - Start date for selected invoices (inclusive) * @param {string} tillDate.query - End date for selected invoices (exclusive) + * @param {integer} take.query - How many entries the endpoint should return + * @param {integer} skip.query - How many entries should be skipped (for pagination) * @returns {PaginatedInvoiceResponse.model} 200 - All existing invoices * @returns {string} 500 - Internal server error */ From 4863f7e7d07e75032944f572339b964268a30900 Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg Date: Fri, 20 Oct 2023 14:31:40 +0200 Subject: [PATCH 08/13] Fix OpenAPI docs for some event-related endpoints --- src/controller/event-controller.ts | 2 +- src/controller/event-shift-controller.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/controller/event-controller.ts b/src/controller/event-controller.ts index b41f8d8db..6a4a72e15 100644 --- a/src/controller/event-controller.ts +++ b/src/controller/event-controller.ts @@ -300,7 +300,7 @@ export default class EventController extends BaseController { /** * Synchronize an event, so that EventShiftAnswers are created/deleted * for users that are (no longer) part of a shift - * @route GET /events/{id} + * @route POST /events/{id}/sync * @group events - Operations of the event controller * @operationId syncEventShiftAnswers * @security JWT diff --git a/src/controller/event-shift-controller.ts b/src/controller/event-shift-controller.ts index 5449e7f6e..86cc14609 100644 --- a/src/controller/event-shift-controller.ts +++ b/src/controller/event-shift-controller.ts @@ -116,7 +116,7 @@ export default class EventShiftController extends BaseController { * @group events - Operations of the event controller * @operationId createEventShift * @security JWT - * @param {CreateEventShiftRequest.model} body.body.required + * @param {CreateShiftRequest.model} body.body.required * @returns {EventShiftResponse.model} 200 - Created event shift * @returns {string} 400 - Validation error * @returns {string} 500 - Internal server error @@ -156,7 +156,7 @@ export default class EventShiftController extends BaseController { * @operationId updateEventShift * @security JWT * @param {integer} id.path.required - The id of the event which should be returned - * @param {UpdateEventShiftRequest.model} body.body.required + * @param {UpdateShiftRequest.model} body.body.required * @returns {EventShiftResponse.model} 200 - Created event shift * @returns {string} 400 - Validation error * @returns {string} 500 - Internal server error @@ -237,12 +237,13 @@ export default class EventShiftController extends BaseController { * Get the number of times a user has been selected for the given shift * @route GET /eventshifts/{id}/counts * @group events - Operations of the event controller - * @operationId getAllEventShifts + * @operationId getShiftSelectedCount * @security JWT + * @param {integer} id.path.required - The id of the event which should be deleted * @param {string} eventType.query - Only include events of this type * @param {string} afterDate.query - Only include events after this date * @param {string} beforeDate.query - Only include events before this date - * @returns {PaginatedEventShiftResponse.model} 200 - All existing event shifts + * @returns {Array.} 200 - Users with how many times they did this shift * @returns {string} 400 - Validation error * @returns {string} 500 - Internal server error */ From 5aef5c20b6f31f2fbe66caa2e2737758dc16016d Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:28:41 +0200 Subject: [PATCH 09/13] Fix balance query not working due to SQLite/MariaDB Syntax difference (#99) --- src/service/balance-service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/service/balance-service.ts b/src/service/balance-service.ts index cf9c77d68..0e2b8655b 100644 --- a/src/service/balance-service.ts +++ b/src/service/balance-service.ts @@ -203,11 +203,13 @@ export default class BalanceService { return result; }; + const greatest = process.env.TYPEORM_CONNECTION === 'sqlite' ? 'max' : 'greatest'; + let query = 'SELECT moneys2.id as id, ' + 'moneys2.totalValue + COALESCE(b5.amount, 0) as amount, ' + 'moneys2.count as count, ' - + 'max(coalesce(b5.lasttransactionid, -1), coalesce(moneys2.lastTransactionId, -1)) as lastTransactionId, ' - + 'max(coalesce(b5.lasttransferid, -1), coalesce(moneys2.lastTransferId, -1)) as lastTransferId, ' + + `${greatest}(coalesce(b5.lasttransactionid, -1), coalesce(moneys2.lastTransactionId, -1)) as lastTransactionId, ` + + `${greatest}(coalesce(b5.lasttransferid, -1), coalesce(moneys2.lastTransferId, -1)) as lastTransferId, ` + 'b5.amount as cachedAmount, ' + 'f.fine as fine, ' + 'f.fineSince as fineSince ' From 1420cdd66eef2bb96bce0d38a9ccab8471a176d2 Mon Sep 17 00:00:00 2001 From: Robin van Dijke Date: Sat, 21 Oct 2023 16:22:13 +0200 Subject: [PATCH 10/13] Fix/user openapi docs (#101) * Fix OpenAPI docs for some event-related endpoints * Changed UserRequest to UpdateUserRequest in user-controller.ts --------- Co-authored-by: Roy Kakkenberg Co-authored-by: Samuel --- package-lock.json | 7327 +----------------- src/controller/user-controller.ts | 4 +- src/entity/transformer/dinero-transformer.ts | 6 +- 3 files changed, 11 insertions(+), 7326 deletions(-) diff --git a/package-lock.json b/package-lock.json index b892f619c..667e4e59a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "sudosos-back-end", "version": "0.1.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -874,6 +874,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.0.tgz", "integrity": "sha512-UR6D5f4KEGWJV6BGPH3Qb2EtgH+t+1XQ1Tt85c7qicN6cezzuHPdZwwAxqZr4JLtnQu0LZsTza/5gmNmSl8XLg==", + "deprecated": "This functionality has been moved to @npmcli/fs", "dev": true, "dependencies": { "mkdirp": "^1.0.4", @@ -4842,6 +4843,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-5.1.3.tgz", "integrity": "sha512-CpDFlBwz/6la78hZxyB9FECVKGYjIIl3Ms3KLqFj99W7IIb7D00/RDgc++IGB4BBALl0QRhh5m4q5WNSopvLtQ==", + "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", "dependencies": { "call-me-maybe": "^1.0.1", "debug": "^3.1.0", @@ -7500,6 +7502,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", "optional": true, "dependencies": { "mkdirp": "^1.0.4", @@ -7981,7 +7984,8 @@ "node_modules/swagger-methods": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/swagger-methods/-/swagger-methods-1.0.8.tgz", - "integrity": "sha512-G6baCwuHA+C5jf4FNOrosE4XlmGsdjbOjdBK4yuiDDj/ro9uR4Srj3OR84oQMT8F3qKp00tYNv0YN730oTHPZA==" + "integrity": "sha512-G6baCwuHA+C5jf4FNOrosE4XlmGsdjbOjdBK4yuiDDj/ro9uR4Srj3OR84oQMT8F3qKp00tYNv0YN730oTHPZA==", + "deprecated": "This package is no longer being maintained." }, "node_modules/swagger-model-validator": { "version": "3.0.21", @@ -9042,7324 +9046,5 @@ "node": ">= 0.10" } } - }, - "dependencies": { - "@babel/compat-data": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", - "integrity": "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==", - "dev": true - }, - "@babel/core": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.8.tgz", - "integrity": "sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.8", - "@babel/helper-compilation-targets": "^7.14.5", - "@babel/helper-module-transforms": "^7.14.8", - "@babel/helpers": "^7.14.8", - "@babel/parser": "^7.14.8", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.8", - "@babel/types": "^7.14.8", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz", - "integrity": "sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.8", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz", - "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.14.5", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", - "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", - "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz", - "integrity": "sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-simple-access": "^7.14.8", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.8", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.8", - "@babel/types": "^7.14.8" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz", - "integrity": "sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.8" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz", - "integrity": "sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", - "dev": true - }, - "@babel/helpers": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.8.tgz", - "integrity": "sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw==", - "dev": true, - "requires": { - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.8", - "@babel/types": "^7.14.8" - } - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.8.tgz", - "integrity": "sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==", - "dev": true - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - } - } - }, - "@babel/traverse": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.8.tgz", - "integrity": "sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.8", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.8", - "@babel/types": "^7.14.8", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.8.tgz", - "integrity": "sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.8", - "to-fast-properties": "^2.0.0" - } - }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - } - }, - "@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", - "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "@eslint/js": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", - "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", - "dev": true - }, - "@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "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" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", - "requires": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "dependencies": { - "are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "requires": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@npmcli/fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.1.tgz", - "integrity": "sha512-1Q0uzx6c/NVNGszePbr5Gc2riSU1zLpNlo/1YWntH+eaPmMgBssAW0qXofCVkpdj3ce4swZtlDYQu+NKiYcptg==", - "dev": true, - "requires": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - } - }, - "@npmcli/move-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.0.tgz", - "integrity": "sha512-UR6D5f4KEGWJV6BGPH3Qb2EtgH+t+1XQ1Tt85c7qicN6cezzuHPdZwwAxqZr4JLtnQu0LZsTza/5gmNmSl8XLg==", - "dev": true, - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } - } - }, - "@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "@sinonjs/samsam": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", - "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, - "@sqltools/formatter": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" - }, - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true - }, - "@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true - }, - "@types/asn1": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.0.tgz", - "integrity": "sha512-5TMxIpYbIA9c1J0hYQjQDX3wr+rTgQEAXaW2BI8ECM8FO53wSW4HFZplTalrKSHuZUc76NtXcePRhwuOHqGD5g==", - "requires": { - "@types/node": "*" - } - }, - "@types/bcrypt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", - "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", - "dev": true - }, - "@types/chai-as-promised": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", - "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, - "@types/chai-sorted": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@types/chai-sorted/-/chai-sorted-0.2.0.tgz", - "integrity": "sha512-YDeysIUTg6ig2Y949ehUsmOszcxagjp2GZfpk5aYhtIPB4Lud7ukJ66f6mXV+0tNSMyHFLIUmpzXFUMTW4mWQQ==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", - "dev": true - }, - "@types/deep-equal-in-any-order": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.1.tgz", - "integrity": "sha512-hUWUUE53WjKfcCncSmWmNXVNNT+0Iz7gYFnov3zdCXrX3Thxp1Cnmfd5LwWOeCVUV5LhpiFgS05vaAG72doo9w==", - "dev": true - }, - "@types/dinero.js": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@types/dinero.js/-/dinero.js-1.8.1.tgz", - "integrity": "sha512-3hEsGSPlKWiaR/DOw5zQXum7bkplnfUK9V1UYQYDzMsYGbWFS1OsqIMLIRutVIX4jGSAmLaBp4CkrtFYf/odUQ==", - "dev": true - }, - "@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-fileupload": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@types/express-fileupload/-/express-fileupload-1.4.1.tgz", - "integrity": "sha512-sbl865h1Sser6SF+efpw2F/+roGISj+PRIbMcGXbtzgJQCBAeeBmoSo7sPge/mBa22ymCHfFPtHFsag/wUxwfg==", - "dev": true, - "requires": { - "@types/busboy": "*", - "@types/express": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "@types/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true - }, - "@types/mime-types": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", - "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", - "dev": true - }, - "@types/mocha": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", - "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", - "dev": true - }, - "@types/node": { - "version": "18.15.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.2.tgz", - "integrity": "sha512-sDPHm2wfx2QhrMDK0pOt2J4KLJMAcerqWNvnED0itPRJWvI+bK+uNHzcH1dFsBlf7G3u8tqXmRF3wkvL9yUwMw==" - }, - "@types/node-cron": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.7.tgz", - "integrity": "sha512-9PuLtBboc/+JJ7FshmJWv769gDonTpItN0Ol5TMwclpSQNjVyB2SRxSKBcTtbSysSL5R7Oea06kTTFNciCoYwA==", - "dev": true - }, - "@types/nodemailer": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz", - "integrity": "sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true - }, - "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, - "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/sinon": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.4.tgz", - "integrity": "sha512-fOYjrxQv8zJsqOY6V6ecP4eZhQBxtY80X0er1VVnUIAIZo74jHm8e1vguG5Yt4Iv8W2Wr7TgibB8MfRe32k9pA==", - "dev": true, - "requires": { - "@sinonjs/fake-timers": "^7.1.0" - } - }, - "@types/sinon-chai": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.9.tgz", - "integrity": "sha512-/19t63pFYU0ikrdbXKBWj9PCdnKyTd0Qkz0X91Ta081cYsq90OxYdcWwK/dwEoDa6dtXgj2HJfmzgq+QZTHdmQ==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, - "@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", - "dev": true - }, - "@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "dev": true - }, - "@types/superagent": { - "version": "3.8.7", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", - "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", - "dev": true, - "requires": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, - "@types/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==" - }, - "@types/validator": { - "version": "13.7.13", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.13.tgz", - "integrity": "sha512-EMfHccxNKXaSxTK6DN0En9WsXa7uR4w3LQtx31f6Z2JjG5hJQeVX5zUYMZoatjZgnoQmRcT94WnNWwi0BzQW6Q==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz", - "integrity": "sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/type-utils": "5.55.0", - "@typescript-eslint/utils": "5.55.0", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/parser": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.55.0.tgz", - "integrity": "sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/typescript-estree": "5.55.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.55.0.tgz", - "integrity": "sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/visitor-keys": "5.55.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.55.0.tgz", - "integrity": "sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.55.0", - "@typescript-eslint/utils": "5.55.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.55.0.tgz", - "integrity": "sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.55.0.tgz", - "integrity": "sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/visitor-keys": "5.55.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/utils": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.55.0.tgz", - "integrity": "sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/typescript-estree": "5.55.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.55.0.tgz", - "integrity": "sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.55.0", - "eslint-visitor-keys": "^3.3.0" - } - }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "agentkeepalive": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", - "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", - "requires": { - "debug": "^4.1.0", - "depd": "^1.1.2", - "humanize-ms": "^1.2.1" - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==" - }, - "append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", - "dev": true, - "requires": { - "default-require-extensions": "^3.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "is-string": "^1.0.7" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - } - }, - "array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - } - }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bcrypt": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", - "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", - "requires": { - "@mapbox/node-pre-gyp": "^1.0.10", - "node-addon-api": "^5.0.0" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "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" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - } - }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "busboy": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", - "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", - "requires": { - "dicer": "0.3.0" - } - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "cacache": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.1.tgz", - "integrity": "sha512-VDKN+LHyCQXaaYZ7rA/qtkURU+/yYhviUdvqEv2LT6QPZU8jpyzEkEVAcKlKLt5dJ5BRp11ym8lo3NKLluEPLg==", - "dev": true, - "requires": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^1.1.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "lru-cache": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.2.tgz", - "integrity": "sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==", - "dev": true - }, - "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", - "dev": true, - "requires": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw==" - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001247", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001247.tgz", - "integrity": "sha512-4rS7co+7+AoOSPRPOPUt5/GdaqZc0EsUpWk66ofE3HJTAajUK2Ss2VwoNzVN69ghg8lYYlh0an0Iy4LIHHo9UQ==", - "dev": true - }, - "chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, - "chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chai-http": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", - "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", - "dev": true, - "requires": { - "@types/chai": "4", - "@types/superagent": "^3.8.3", - "cookiejar": "^2.1.1", - "is-ip": "^2.0.0", - "methods": "^1.1.2", - "qs": "^6.5.1", - "superagent": "^3.7.0" - } - }, - "chai-sorted": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chai-sorted/-/chai-sorted-0.2.0.tgz", - "integrity": "sha512-mSDz4v2CpSZcuR9dFbK6D0t+KGVGTFTlqo8mL4eesL7mMcIEeLYNTrjx2cs/5NtCnRPawDv3TB1fBuEsf+dGBw==", - "dev": true - }, - "chai-swag": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/chai-swag/-/chai-swag-0.1.9.tgz", - "integrity": "sha512-xMyUpfou0bl81lcL9Na2OTeyeK3QDsbsIPCbjYkukqm4Fc2oYppCwqXKYJLF0oxOqg39mAy1gJ+J4s334qiG9w==", - "dev": true, - "requires": { - "js-yaml": "^3.12.1", - "json-cycle": "^1.3.0", - "res-swag": "^0.1.14" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, - "cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "requires": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - } - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" - }, - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "optional": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true - }, - "core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true - }, - "date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" - }, - "date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal-in-any-order": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.5.tgz", - "integrity": "sha512-A3sZZHof34Eo8sJSdjcteC4srnfkn94xqdLlT1Ul677YfWEarRyoWGyDVElU+6TBj8S/C9a6R5jTlz+wHxrmxA==", - "dev": true, - "requires": { - "lodash.mapvalues": "^4.6.0", - "sort-any": "^2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "default-require-extensions": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", - "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", - "dev": true, - "requires": { - "strip-bom": "^4.0.0" - } - }, - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true - }, - "dicer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", - "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", - "requires": { - "streamsearch": "0.1.2" - } - }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "dinero.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/dinero.js/-/dinero.js-1.9.0.tgz", - "integrity": "sha512-gbDFhCCe/ba9pU2P232FS54LFPCLeGTb8vcrgsjmXHknu/VDHIgFecA8mxLYQsqVKkHoHSCUq95ojYOakzuweA==" - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "doctrine-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/doctrine-file/-/doctrine-file-1.0.3.tgz", - "integrity": "sha512-OK37HbZtNmIMn84riibVXRmcEGUIf6BNfYMcbXg20ejP+LEsf4tnk8QfYy3EmQs4KzZFhTl3zwoKqVwARxpBgA==", - "requires": { - "doctrine": "^2.0.0" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "requires": { - "esutils": "^2.0.2" - } - } - } - }, - "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" - }, - "dynamic-dedupe": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", - "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", - "dev": true, - "requires": { - "xtend": "^4.0.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "electron-to-chromium": { - "version": "1.3.786", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.786.tgz", - "integrity": "sha512-AmvbLBj3hepRk8v/DHrFF8gINxOFfDbrn6Ts3PcK46/FBdQb5OMmpamSpZQXSkfi77FfBzYtQtAk+00LCLYMVw==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" - }, - "err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", - "dev": true, - "requires": { - "array-buffer-byte-length": "^1.0.0", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" - } - }, - "es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - } - }, - "es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", - "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.1", - "@eslint/js": "8.36.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.5.0", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-config-airbnb-typescript": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.0.0.tgz", - "integrity": "sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^15.0.0" - } - }, - "eslint-import-resolver-node": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", - "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", - "dev": true, - "requires": { - "debug": "^3.2.7" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "eslint-plugin-chai-expect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-chai-expect/-/eslint-plugin-chai-expect-3.0.0.tgz", - "integrity": "sha512-NS0YBcToJl+BRKBSMCwRs/oHJIX67fG5Gvb4tGked+9Wnd1/PzKijd82B2QVKcSSOwRe+pp4RAJ2AULeck4eQw==", - "dev": true - }, - "eslint-plugin-chai-friendly": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.7.1.tgz", - "integrity": "sha512-0xhGiSQ+9oWtNc6IZPUR+6ChKbEvLXwT9oZZ5NcGlPzHVKGn1YKwQFj7a9yL3rnRKbWF7b3RkRYEP8kN6dPOwQ==", - "dev": true - }, - "eslint-plugin-header": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", - "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", - "dev": true - }, - "eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", - "dev": true, - "requires": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "espree": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", - "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "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.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "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" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - } - } - }, - "express-fileupload": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.2.1.tgz", - "integrity": "sha512-fWPNAkBj+Azt9Itmcz/Reqdg3LeBfaXptDEev2JM8bCC0yDptglCnlizhf0YZauyU5X/g6v7v4Xxqhg8tmEfEA==", - "requires": { - "busboy": "^0.3.1" - } - }, - "express-swagger-generator": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/express-swagger-generator/-/express-swagger-generator-1.1.17.tgz", - "integrity": "sha512-eKB2cR3TcvmSepkqjm9sFPqPAV7PQawyc3Df2p9/0vN4Q7LyBrLLpechH246YYJ1kIDPa8RresfhJeIHg5zS4A==", - "requires": { - "doctrine": "^2.0.0", - "doctrine-file": "^1.0.2", - "express-swaggerize-ui": "^1.0.3", - "glob": "^7.0.3", - "recursive-iterator": "^2.0.3", - "swagger-parser": "^5.0.5" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "requires": { - "esutils": "^2.0.2" - } - } - } - }, - "express-swaggerize-ui": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/express-swaggerize-ui/-/express-swaggerize-ui-1.1.0.tgz", - "integrity": "sha512-dDJuWV/GlISNYyKvFMa3EDr6sYzMgMrVRCt9o1kQxaIIKnmK1NJvaTzGbRIokIlGGHriIT6E2ztorRyRxLuOzA==", - "requires": { - "express": "^4.13.3" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - } - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - } - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "format-util": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", - "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==" - }, - "formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "dev": true - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fromentries": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", - "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", - "dev": true - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "requires": { - "is-property": "^1.0.2" - } - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" - }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, - "hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "requires": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - } - } - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "ignore-walk": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", - "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", - "integrity": "sha512-9MTn0dteHETtyUx8pxqMwg5hMBi3pvlyglJ+b79KOCca0po23337LbVV2Hl4xmMvfw++ljnO0/+5G6G+0Szh6g==", - "dev": true, - "requires": { - "ip-regex": "^2.0.0" - } - }, - "is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "requires": { - "append-transform": "^2.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^3.3.3" - }, - "dependencies": { - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jref/-/jref-1.0.2.tgz", - "integrity": "sha512-Bry+3PfzAOSyLIMCQiXj/HX7Uiq2bfscD2yCFfJxxRWPJlK0qCU/z9UmCIb0ett+a9kfsCY/LgSUvCe1kj7erA==", - "dev": true - }, - "js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-cycle": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/json-cycle/-/json-cycle-1.3.0.tgz", - "integrity": "sha512-FD/SedD78LCdSvJaOUQAXseT8oQBb5z6IVYaQaCrVUlu9zOAr1BDdKyVYQaSD/GDsAMrXpKcOyBD4LIl8nfjHw==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema-ref-parser": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-5.1.3.tgz", - "integrity": "sha512-CpDFlBwz/6la78hZxyB9FECVKGYjIIl3Ms3KLqFj99W7IIb7D00/RDgc++IGB4BBALl0QRhh5m4q5WNSopvLtQ==", - "requires": { - "call-me-maybe": "^1.0.1", - "debug": "^3.1.0", - "js-yaml": "^3.12.0", - "ono": "^4.0.6" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonschema": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", - "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" - }, - "jsonschema-draft4": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz", - "integrity": "sha512-sBV3UnQPRiyCTD6uzY/Oao2Yohv6KKgQq7zjPwjFHeR6scg/QSXnzDxdugsGaLQDmFUrUlTbMYdEE+72PizhGA==" - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "ldap-escape": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/ldap-escape/-/ldap-escape-2.0.6.tgz", - "integrity": "sha512-M0mZojh0QIDSDkA0+M5Zopqz3Ku5DNs1/Q8VqWO5l3Pjx1J2p71c9WksQIDQQKmd1XkV3N2NZFwcBFLJSm1l1w==" - }, - "ldapts": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-4.2.4.tgz", - "integrity": "sha512-crE50bd+UA7bJcbOy2IynWJHTQwrG2FeySo1AqokGtApG7xoCVxz/sO5JLe2Xso1zb/Z+Zh65ERHkpJaGRJaEA==", - "requires": { - "@types/asn1": ">=0.2.0", - "@types/node": ">=14", - "@types/uuid": ">=9", - "asn1": "~0.2.6", - "debug": "~4.3.4", - "strict-event-emitter-types": "~2.0.0", - "uuid": "~9.0.0" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "lodash.mapvalues": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", - "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "requires": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - } - }, - "long": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", - "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "make-fetch-happen": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.0.tgz", - "integrity": "sha512-OnEfCLofQVJ5zgKwGk55GaqosqKjaR6khQlJY3dBAA+hM25Bc5CmX5rKUfVut+rYA3uidA7zb7AvcglU87rPRg==", - "dev": true, - "requires": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.2.tgz", - "integrity": "sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==", - "dev": true - }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - } - } - }, - "md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" - }, - "mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "requires": { - "mime-db": "1.50.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "minipass-fetch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.0.tgz", - "integrity": "sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==", - "dev": true, - "requires": { - "encoding": "^0.1.13", - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "dependencies": { - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - } - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", - "dev": true, - "requires": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - } - } - }, - "mocha-junit-reporter": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.0.tgz", - "integrity": "sha512-W83Ddf94nfLiTBl24aS8IVyFvO8aRDLlCvb+cKb/VEaN5dEbcqu3CXiTe8MQK2DvzS7oKE1RsFTxzN302GGbDQ==", - "dev": true, - "requires": { - "debug": "^4.3.4", - "md5": "^2.3.0", - "mkdirp": "~1.0.4", - "strip-ansi": "^6.0.1", - "xml": "^1.0.1" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "mocha-multi-reporters": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz", - "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "lodash": "^4.17.15" - } - }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" - }, - "moment-timezone": { - "version": "0.5.33", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", - "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", - "requires": { - "moment": ">= 2.9.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mysql2": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.2.0.tgz", - "integrity": "sha512-0Vn6a9WSrq6fWwvPgrvIwnOCldiEcgbzapVRDAtDZ4cMTxN7pnGqCTx8EG32S/NYXl6AXkdO+9hV1tSIi/LigA==", - "requires": { - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", - "long": "^5.2.1", - "lru-cache": "^7.14.1", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" - } - } - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "named-placeholders": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", - "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", - "requires": { - "lru-cache": "^7.14.1" - }, - "dependencies": { - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" - } - } - }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "needle": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.8.0.tgz", - "integrity": "sha512-ZTq6WYkN/3782H1393me3utVYdq2XyqNUFBsprEE3VMAT0+hP/cItpnITpqsY6ep2yeFE4Tqtqwc74VqUlUYtw==", - "dev": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^10.0.2", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "requires": { - "isarray": "0.0.1" - } - } - } - }, - "node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" - }, - "node-cron": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz", - "integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==", - "requires": { - "uuid": "8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } - } - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-gyp": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", - "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", - "dev": true, - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "dependencies": { - "are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "dev": true, - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - } - }, - "nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "requires": { - "abbrev": "^1.0.0" - } - }, - "npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "dev": true, - "requires": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - } - } - }, - "node-pre-gyp": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.17.0.tgz", - "integrity": "sha512-abzZt1hmOjkZez29ppg+5gGqdPLUuJeAEwVPtHYEJgx0qzttCbcKFpxrCQn2HYbwCv2c+7JwH4BgEzFkUGpn4A==", - "dev": true, - "requires": { - "detect-libc": "^1.0.3", - "mkdirp": "^0.5.5", - "needle": "^2.5.2", - "nopt": "^4.0.3", - "npm-packlist": "^1.4.8", - "npmlog": "^4.1.2", - "rc": "^1.2.8", - "rimraf": "^2.7.1", - "semver": "^5.7.1", - "tar": "^4.4.13" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "tar": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.14.tgz", - "integrity": "sha512-ouN3XcSWYOAHmXZ+P4NEFJvqXL50To9OZBSQNNP30vBUFJFZZ0PLX15fnwupv6azfxMUfUDUr2fhYw4zGAEPcg==", - "dev": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "dev": true, - "requires": { - "process-on-spawn": "^1.0.0" - } - }, - "node-releases": { - "version": "1.1.73", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", - "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==", - "dev": true - }, - "nodemailer": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.1.tgz", - "integrity": "sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==" - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-bundled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", - "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", - "dev": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "dev": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" - }, - "nyc": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", - "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", - "dev": true, - "requires": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^2.0.0", - "get-package-type": "^0.1.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "dependencies": { - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "ono": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", - "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", - "requires": { - "format-util": "^1.0.3" - } - }, - "openapi-schema-validation": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz", - "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==", - "requires": { - "jsonschema": "1.2.4", - "jsonschema-draft4": "^1.0.0", - "swagger-schema-official": "2.0.0-bab6bed" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" - }, - "parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "requires": { - "parse5": "^6.0.1" - }, - "dependencies": { - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - } - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", - "integrity": "sha512-fjAPuiws93rm7mPUu21RdBnkeZNrbfCFCwfAhPWY+rR3zG0ubpe5cEReHOw5fIbfmsxEV/g2kSxGTATY3Bpnwg==", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", - "dev": true, - "requires": { - "fromentries": "^1.2.0" - } - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" - }, - "promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "requires": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - } - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true - } - } - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - } - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "recursive-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/recursive-iterator/-/recursive-iterator-2.0.3.tgz", - "integrity": "sha512-SqfNKjjTw7Lq3E2S6P8L5Ac7YFD91mbkzWRlxrWK4tWioJRKDhe1+PtWS0X0hkQNEzZDpSish0TTXAVM4cRUzQ==" - }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "res-swag": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/res-swag/-/res-swag-0.1.15.tgz", - "integrity": "sha512-zOZPH9Qfb1XL0T5Agd72VNCIePTKnrzdgo84kdLrjQa1YrZxjWuP7mDR8MGwvIP3agdYDdRk8m7R+rqHDxOvsQ==", - "dev": true, - "requires": { - "ajv": "^6.10.0", - "jref": "^1.0.1", - "md5": "^2.2.1", - "url-parse": "^1.4.4" - } - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "sinon": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.2.tgz", - "integrity": "sha512-PCVP63XZkg0/LOqQH5rEU4LILuvTFMb5tNxTHfs6VUMNnZz2XrnGSTZbAGITjzwQWbl/Bl/8hi4G3zZWjyBwHg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.0.2", - "@sinonjs/samsam": "^7.0.1", - "diff": "^5.1.0", - "nise": "^5.1.4", - "supports-color": "^7.2.0" - }, - "dependencies": { - "diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true - } - } - }, - "sinon-chai": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", - "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, - "socks": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", - "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", - "requires": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - } - }, - "socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - } - }, - "sort-any": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", - "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", - "dev": true, - "requires": { - "lodash": "^4.17.21" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "dev": true, - "requires": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - } - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", - "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "sqlite3": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.5.tgz", - "integrity": "sha512-7sP16i4wI+yKnGOO2q2ijze7EjQ9US+Vw7DYYwxfFtqNZDGgBcEw0oeDaDvUTq66uJOzVd/z6MkIg+c9erSJKg==", - "requires": { - "@mapbox/node-pre-gyp": "^1.0.0", - "node-addon-api": "^4.2.0", - "node-gyp": "8.x", - "tar": "^6.1.11" - }, - "dependencies": { - "@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "optional": true, - "requires": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "optional": true, - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "optional": true - }, - "are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "optional": true, - "requires": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "optional": true - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "optional": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "optional": true, - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - } - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "optional": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true - }, - "make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "optional": true, - "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - } - }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "optional": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "optional": true, - "requires": { - "encoding": "^0.1.12", - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true - }, - "node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" - }, - "node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "optional": true, - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - } - }, - "npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "optional": true, - "requires": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "optional": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "optional": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - } - }, - "ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "optional": true, - "requires": { - "minipass": "^3.1.1" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - } - } - }, - "sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" - }, - "ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "requires": { - "minipass": "^3.1.1" - }, - "dependencies": { - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" - }, - "streamroller": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", - "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", - "requires": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "dependencies": { - "date-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", - "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==" - } - } - }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==" - }, - "strict-event-emitter-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", - "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "stripe": { - "version": "10.17.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-10.17.0.tgz", - "integrity": "sha512-JHV2KoL+nMQRXu3m9ervCZZvi4DDCJfzHUE6CmtJxR9TmizyYfrVuhGvnsZLLnheby9Qrnf4Hq6iOEcejGwnGQ==", - "requires": { - "@types/node": ">=8.1.0", - "qs": "^6.11.0" - } - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - } - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "swagger-methods": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/swagger-methods/-/swagger-methods-1.0.8.tgz", - "integrity": "sha512-G6baCwuHA+C5jf4FNOrosE4XlmGsdjbOjdBK4yuiDDj/ro9uR4Srj3OR84oQMT8F3qKp00tYNv0YN730oTHPZA==" - }, - "swagger-model-validator": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/swagger-model-validator/-/swagger-model-validator-3.0.20.tgz", - "integrity": "sha512-lFwgcigjAS+ZDRUetx9JRbI00f0BsFJiPnAjFcEnLz/u1+Wk7rSNNCp31LJFDYqn1yrGoGqIS7hXBsafWZKpEA==", - "requires": { - "lodash.isequal": "^4.5.0" - } - }, - "swagger-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-5.0.6.tgz", - "integrity": "sha512-FdzCYFK11iGgrOpojlqUluU6SKThtzmu+5Get+6ValJR2TFwTnES1x4Fdfgy3C4/8VVXk4Va/WsqGlbyY/Os+A==", - "requires": { - "call-me-maybe": "^1.0.1", - "debug": "^3.1.0", - "json-schema-ref-parser": "^5.1.3", - "ono": "^4.0.6", - "openapi-schema-validation": "^0.4.2", - "swagger-methods": "^1.0.4", - "swagger-schema-official": "2.0.0-bab6bed", - "z-schema": "^3.23.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "swagger-schema-official": { - "version": "2.0.0-bab6bed", - "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", - "integrity": "sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==" - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, - "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "requires": { - "@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" - }, - "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - } - } - }, - "ts-node-dev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", - "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", - "dev": true, - "requires": { - "chokidar": "^3.5.1", - "dynamic-dedupe": "^0.3.0", - "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "resolve": "^1.0.0", - "rimraf": "^2.6.1", - "source-map-support": "^0.5.12", - "tree-kill": "^1.2.2", - "ts-node": "^10.4.0", - "tsconfig": "^7.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "dev": true, - "requires": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true - } - } - }, - "tsconfig-paths": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz", - "integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==", - "dev": true, - "requires": { - "json5": "^2.2.0", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typeorm": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.12.tgz", - "integrity": "sha512-sYSxBmCf1nJLLTcYtwqZ+lQIRtLPyUoO93rHTOKk9vJCyT4UfRtU7oRsJvfvKP3nnZTD1hzz2SEy2zwPEN6OyA==", - "requires": { - "@sqltools/formatter": "^1.2.5", - "app-root-path": "^3.1.0", - "buffer": "^6.0.3", - "chalk": "^4.1.2", - "cli-highlight": "^2.1.11", - "date-fns": "^2.29.3", - "debug": "^4.3.4", - "dotenv": "^16.0.3", - "glob": "^8.1.0", - "js-yaml": "^4.1.0", - "mkdirp": "^2.1.3", - "reflect-metadata": "^0.1.13", - "sha.js": "^2.4.11", - "tslib": "^2.5.0", - "uuid": "^9.0.0", - "xml2js": "^0.4.23", - "yargs": "^17.6.2" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "mkdirp": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.5.tgz", - "integrity": "sha512-jbjfql+shJtAPrFoKxHOXip4xS+kul9W3OzfzzrqueWK2QMGon2bFH2opl6W9EagBThjEz+iysyi/swOoVfB/w==" - }, - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "requires": { - "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" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - } - } - }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "devOptional": true - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url-parse": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", - "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validator": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", - "dev": true - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", - "dev": true - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", - "requires": { - "cliui": "^7.0.2", - "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.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - } - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" - }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true - } - } - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - }, - "z-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.25.1.tgz", - "integrity": "sha512-7tDlwhrBG+oYFdXNOjILSurpfQyuVgkRe3hB2q8TEssamDHB7BbLWYkYO98nTn0FibfdFroFKDjndbgufAgS/Q==", - "requires": { - "commander": "^2.7.1", - "core-js": "^2.5.7", - "lodash.get": "^4.0.0", - "lodash.isequal": "^4.0.0", - "validator": "^10.0.0" - }, - "dependencies": { - "validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" - } - } - } } } diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index 012641357..9a28c9289 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -736,10 +736,10 @@ export default class UserController extends BaseController { * @operationId updateUser * @group users - Operations of user controller * @param {integer} id.path.required - The id of the user - * @param {UserRequest.model} user.body.required - + * @param {UpdateUserRequest.model} user.body.required - * The user which should be updated * @security JWT - * @returns {UserRequest.model} 200 - New user + * @returns {UpdateUserRequest.model} 200 - New user * @returns {string} 400 - Bad request */ public async updateUser(req: RequestWithToken, res: Response): Promise { diff --git a/src/entity/transformer/dinero-transformer.ts b/src/entity/transformer/dinero-transformer.ts index 011324a90..d6d67570c 100644 --- a/src/entity/transformer/dinero-transformer.ts +++ b/src/entity/transformer/dinero-transformer.ts @@ -43,9 +43,9 @@ export default class DineroTransformer implements ValueTransformer { } /** - * Converts a monetary value to it's Dinero object representation. - * @param value - the monetary value represented as integer. - * @throws {TypeError} if value is non-integer. + * Converts a monetary value to its Dinero object representation. + * @param {Number} value - The monetary value represented as an integer. + * @throws {TypeError} If value is non-integer. */ public from(value: number | string | null): Dinero { if (value == null) return dinero({ amount: 0 }); From e720d09bda53b81f9cdd2f9a28d43cd3cf740f29 Mon Sep 17 00:00:00 2001 From: Rink Date: Sat, 21 Oct 2023 18:24:12 +0200 Subject: [PATCH 11/13] Use mariadb in CI (#103) --- .github/workflows/build.yml | 45 ++++++++- .github/workflows/docker-build.yml | 45 +++++++++ .../workflows/{docker.yml => docker-push.yml} | 6 +- .gitlab-ci.yml | 97 ------------------- 4 files changed, 87 insertions(+), 106 deletions(-) create mode 100644 .github/workflows/docker-build.yml rename .github/workflows/{docker.yml => docker-push.yml} (93%) delete mode 100644 .gitlab-ci.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16439e01e..a30a7a532 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,10 +58,34 @@ jobs: retention-days: 1 coverage: - needs: [lint] + name: "coverage-${{ matrix.typeorm-connection }}" + needs: [ lint ] runs-on: [ self-hosted, linux, docker ] container: image: node:18 + services: + mariadb: + image: mariadb:lts + env: + MARIADB_RANDOM_ROOT_PASSWORD: true + MARIADB_DATABASE: sudosos-ci + MARIADB_USER: sudosos-ci + MARIADB_PASSWORD: sudosos-ci + strategy: + matrix: + include: + - typeorm-connection: mariadb + typeorm-host: mariadb + typeorm-port: 3306 + typeorm-username: sudosos-ci + typeorm-password: sudosos-ci + typeorm-database: sudosos-ci + - typeorm-connection: sqlite + typeorm-host: '' + typeorm-port: '' + typeorm-username: '' + typeorm-password: '' + typeorm-database: local.sqlite env: NODE_ENV: development API_HOST: localhost:3000 @@ -71,10 +95,14 @@ jobs: GEWISWEB_JWT_SECRET: ChangeMe JWT_KEY_PATH: ./config/jwt.key HTTP_PORT: 3000 - TYPEORM_CONNECTION: sqlite - TYPEORM_DATABASE: local.sqlite + TYPEORM_CONNECTION: ${{ matrix.typeorm-connection }} + TYPEORM_HOST: ${{ matrix.typeorm-host }} + TYPEORM_PORT: ${{ matrix.typeorm-port }} + TYPEORM_USERNAME: ${{ matrix.typeorm-username }} + TYPEORM_PASSWORD: ${{ matrix.typeorm-password }} + TYPEORM_DATABASE: ${{ matrix.typeorm-database }} TYPEORM_SYNCHRONIZE: 0 - TYPEORM_LOGGING: 1 + TYPEORM_LOGGING: 0 LOG_LEVEL: INFO RESET_TOKEN_EXPIRES: 3600 FILE_STORAGE_METHOD: disk @@ -97,8 +125,14 @@ jobs: - run: npm run swagger - run: npm run coverage-ci # Separate command to limit the number of workers to prevent timeouts - run: git config --global --add safe.directory $GITHUB_WORKSPACE # To avoid dubious ownership + if: ${{ matrix.typeorm-connection == 'mariadb' }} + + - name: "Cannot commit code coverage cross-fork" + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork && matrix.typeorm-connection == 'mariadb' + run: | + echo "::warning Cannot comment code coverage cross-fork" - name: "Comment code coverage on PR" - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && matrix.typeorm-connection == 'mariadb' uses: sidx1024/report-nyc-coverage-github-action@v1.2.6 with: comment_template_file: .github/coverage-template.md @@ -106,6 +140,7 @@ jobs: base_coverage_file: '' sources_base_path: "/__w/sudosos-backend/sudosos-backend" #github.action_path does not work in Docker (https://github.com/actions/runner/issues/716) - name: "Upload code coverage report" + if: ${{ matrix.typeorm-connection == 'mariadb' }} uses: actions/upload-artifact@v3 with: name: coverage diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 000000000..c644b748f --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,45 @@ +name: Docker Build + +on: + pull_request: + branches: [ main, develop ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dockerize: + runs-on: [self-hosted, linux, docker] + container: + image: docker:dind + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Get Docker meta across forks + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ github.actor }}/${{ github.repository }} + tags: | + type=ref,event=pr + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # Build and push Docker image with Buildx (don't push on PR) + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64 #SudoSOS does not run on linux/arm64 + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker-push.yml similarity index 93% rename from .github/workflows/docker.yml rename to .github/workflows/docker-push.yml index cf33aad31..2602d1652 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker-push.yml @@ -1,12 +1,10 @@ -name: Docker +name: Docker Build & Push on: push: branches: [ main, develop ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] - pull_request: - branches: [ main, develop ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -55,7 +53,7 @@ jobs: with: context: . platforms: linux/amd64 #SudoSOS does not run on linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index a16f98da7..000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,97 +0,0 @@ -image: node:18 -stages: -- install -- build -- test - -cache: - paths: - - node_modules/ - -variables: - API_HOST: localhost:3000 - API_BASEPATH: /v1 - CURRENCY_CODE: EUR - CURRENCY_PRECISION: 2 - GEWISWEB_JWT_SECRET: ChangeMe - JWT_KEY_PATH: config/jwt.key - HTTP_PORT: 3000 - TYPEORM_CONNECTION: sqlite - TYPEORM_DATABASE: local.sqlite - TYPEORM_SYNCHRONIZE: 0 - TYPEORM_LOGGING: 1 - LOG_LEVEL: INFO - PAGINATION_DEFAULT: 20 - PAGINATION_MAX: 500 - FILE_STORAGE_METHOD: disk - SMTP_FORM: SudoSOS - TYPEORM_USERNAME: $TYPEORM_USERNAME - TYPEORM_PASSWORD: $TYPEORM_PASSWORD - TYPEORM_DATABASE: $TYPEORM_DATABASE - -install_dependencies: - stage: install - script: npm install - artifacts: - paths: - - node_modules/ - -build: - stage: build - script: npm run build - artifacts: - paths: - - out/ - -swagger: - stage: build - script: npm run swagger - artifacts: - paths: - - out/ - -dockerize-master: - image: docker:19.03 - stage: build - services: - - docker:19.03-dind - before_script: - - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - script: - - docker build --pull -t "$CI_REGISTRY_IMAGE" . - - docker push "$CI_REGISTRY_IMAGE" - only: - - master - -dockerize: - image: docker:19.03 - stage: build - services: - - docker:19.03-dind - before_script: - - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - script: - - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" . --build-arg TYPEORM_USERNAME=$TYPEORM_USERNAME --build-arg TYPEORM_PASSWORD=$TYPEORM_PASSWORD --build-arg TYPEORM_DATABASE=$TYPEORM_DATABASE - - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" - except: - - master - -test: - stage: test - before_script: - - cp "$ENV_FILE" .env - - openssl genrsa -out config/jwt.key 2048 - script: npm run coverage - artifacts: - when: always - reports: - junit: reports/junit.xml - coverage_report: - coverage_format: cobertura - path: reports/coverage/cobertura-coverage.xml - paths: - - reports/coverage/ - -lint: - stage: test - script: npm run lint From f506542b24f0906da90fd931239e44cf3af826e8 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 2 Jan 2024 14:44:19 +0100 Subject: [PATCH 12/13] Migration to `express-jsdoc-swagger` (#114) * Changed the swagger.ts to use the new express-jsdoc-swagger. Changing all the files will be a gigantic hassle, but running swagger:validate will show if the generated swagger is valid. It does not show if the swagger contains everything you expect it to. * Migrated: banner-response.ts base-response.ts pagination.ts root-controller.ts * Migrated: authentication-controller.ts authentication-response.ts all authentication requests user-response.ts * Migrated: authentication-secure-controller.ts * Migrated: all responses, balance-controller.ts and some entities * Migrated: all requests * Migrated: banner-controller.ts * Migrated: container-controller.ts * Migrated: debtor-controller.ts * Migrated: event-controller.ts * Migrated: event-shift-controller.ts * Migrated all controllers * Small remaining changes and mistakes * Rebased on dev * Added logging * Started working on fixing the test files / finding out whats going wrong. * TODO Fix code to use specification.component.schemas instead of specification.definitions * Fixed test cases and some of the spec errors that came up. * Small test to fix CI * Small test to fix CI * Update build.yml * Added swagger-cli as devtool * Continued CI/CD testsuite bug fixing. * Continued CI/CD testsuite bug fixing. * Cleaned swagger.ts and removed old dep. * Missed a dangling endpoint * Small changes to try and fix runner * Fixed the stupid mistake * Function signature changed. * Small fix in test model * Small fix in test model path * Small typo * Small error fix * Changed generateSpecification params to include file patterns. * Removed all entity swagger doc to only leave Responses * Undid all the changes to the entities swaggerdoc since these are unused. * Applied requested pull changes. * Applied requested pull changes. --- .github/workflows/build.yml | 2 +- .run/Template Mocha.run.xml | 2 +- package-lock.json | 725 ++++++++---------- package.json | 5 +- src/controller/authentication-controller.ts | 116 +-- .../authentication-secure-controller.ts | 20 +- src/controller/balance-controller.ts | 50 +- src/controller/banner-controller.ts | 88 +-- src/controller/base-controller.ts | 6 +- src/controller/container-controller.ts | 78 +- src/controller/debtor-controller.ts | 72 +- src/controller/event-controller.ts | 102 +-- src/controller/event-shift-controller.ts | 68 +- src/controller/invoice-controller.ts | 62 +- src/controller/payout-request-controller.ts | 48 +- src/controller/point-of-sale-controller.ts | 92 ++- src/controller/product-category-controller.ts | 52 +- src/controller/product-controller.ts | 70 +- src/controller/rbac-controller.ts | 10 +- src/controller/request/accept-tos-request.ts | 2 +- .../request/authentication-ean-request.ts | 2 +- .../request/authentication-key-request.ts | 2 +- .../request/authentication-ldap-request.ts | 2 +- .../request/authentication-local-request.ts | 2 +- .../request/authentication-mock-request.ts | 2 +- .../request/authentication-nfc-request.ts | 2 +- .../request/authentication-pin-request.ts | 2 +- .../authentication-reset-token-request.ts | 2 +- src/controller/request/banner-request.ts | 2 +- src/controller/request/container-request.ts | 8 +- src/controller/request/debtor-request.ts | 2 +- src/controller/request/dinero-request.ts | 4 +- src/controller/request/event-request.ts | 12 +- src/controller/request/file-request.ts | 22 + .../request/invoice-entry-request.ts | 4 +- src/controller/request/invoice-request.ts | 11 +- .../request/payout-request-request.ts | 4 +- .../request/payout-request-status-request.ts | 5 +- .../request/point-of-sale-request.ts | 8 +- .../request/product-category-request.ts | 2 +- src/controller/request/product-request.ts | 8 +- src/controller/request/reset-local-request.ts | 2 +- src/controller/request/revision-request.ts | 6 +- src/controller/request/simple-file-request.ts | 2 +- src/controller/request/stripe-request.ts | 4 +- src/controller/request/transaction-request.ts | 22 +- src/controller/request/transfer-request.ts | 4 +- .../request/update-local-request.ts | 2 +- src/controller/request/update-nfc-request.ts | 2 +- src/controller/request/update-pin-request.ts | 2 +- src/controller/request/user-request.ts | 4 +- src/controller/request/vat-group-request.ts | 4 +- .../request/voucher-group-request.ts | 4 +- .../response/authentication-response.ts | 8 +- src/controller/response/balance-response.ts | 10 +- src/controller/response/banner-response.ts | 8 +- src/controller/response/base-response.ts | 2 +- src/controller/response/container-response.ts | 26 +- src/controller/response/debtor-response.ts | 30 +- src/controller/response/dinero-response.ts | 2 +- src/controller/response/dinero.ts | 2 +- src/controller/response/event-response.ts | 26 +- .../response/financial-mutation-response.ts | 12 +- src/controller/response/invoice-response.ts | 33 +- src/controller/response/message-response.ts | 2 +- .../response/payout-request-response.ts | 22 +- .../response/point-of-sale-response.ts | 16 +- .../response/product-category-response.ts | 8 +- src/controller/response/product-response.ts | 20 +- .../response/rbac/action-response.ts | 6 +- .../response/rbac/entity-response.ts | 6 +- .../response/rbac/relation-response.ts | 6 +- src/controller/response/rbac/role-response.ts | 4 +- .../response/simple-file-response.ts | 4 +- src/controller/response/stripe-response.ts | 17 +- .../response/transaction-report-response.ts | 44 +- .../response/transaction-response.ts | 48 +- src/controller/response/transfer-response.ts | 24 +- .../response/update-key-response.ts | 2 +- src/controller/response/user-response.ts | 10 +- src/controller/response/vat-group-response.ts | 22 +- .../response/voucher-group-response.ts | 12 +- src/controller/root-controller.ts | 34 +- src/controller/simple-file-controller.ts | 46 +- src/controller/stripe-controller.ts | 12 +- src/controller/stripe-webhook-controller.ts | 6 +- src/controller/test-controller.ts | 10 +- src/controller/transaction-controller.ts | 78 +- src/controller/transfer-controller.ts | 38 +- src/controller/user-controller.ts | 296 ++++--- src/controller/vat-group-controller.ts | 58 +- src/controller/voucher-group-controller.ts | 52 +- .../swagger-model-validator/index.d.ts | 5 +- .../gewis-authentication-controller.ts | 42 +- .../gewis-authentication-pin-request.ts | 2 +- .../gewisweb-authentication-request.ts | 2 +- .../response}/gewis-user-response.ts | 4 +- src/gewis/gewis.ts | 2 +- src/helpers/pagination.ts | 2 +- src/index.ts | 1 - .../request-validator-middleware.ts | 6 +- src/service/container-service.ts | 2 +- src/service/product-service.ts | 2 +- src/service/transfer-service.ts | 4 +- src/start/swagger.ts | 110 +-- test/unit/controller/container-controller.ts | 10 +- .../unit/controller/transaction-controller.ts | 16 +- test/unit/controller/user-controller.ts | 8 +- test/unit/entity/transformer/test-model.ts | 5 +- test/unit/gewis/gewis.ts | 2 +- test/unit/swagger.ts | 4 +- 111 files changed, 1546 insertions(+), 1581 deletions(-) create mode 100644 src/controller/request/file-request.ts rename src/gewis/{entity => controller/response}/gewis-user-response.ts (87%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a30a7a532..dcf854bfe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -122,7 +122,7 @@ jobs: - run: npm install if: steps.cache-node.outputs.cache-hit != 'true' - run: openssl genrsa -out ./config/jwt.key 2048 && chmod 0777 ./config/jwt.key - - run: npm run swagger + - run: npm run swagger:validate - run: npm run coverage-ci # Separate command to limit the number of workers to prevent timeouts - run: git config --global --add safe.directory $GITHUB_WORKSPACE # To avoid dubious ownership if: ${{ matrix.typeorm-connection == 'mariadb' }} diff --git a/.run/Template Mocha.run.xml b/.run/Template Mocha.run.xml index 12a9868ac..f1f8f6d84 100644 --- a/.run/Template Mocha.run.xml +++ b/.run/Template Mocha.run.xml @@ -5,7 +5,7 @@ true - -r ts-node/register --timeout 50000 --file ./test/setup.ts --reporter mocha-multi-reporters --reporter-options configFile=mocha.json + -r ts-node/register --timeout 50000 --require ./test/setup.ts --reporter mocha-multi-reporters --reporter-options configFile=mocha.json DIRECTORY false diff --git a/package-lock.json b/package-lock.json index 667e4e59a..19336be8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-fileupload": "^1.4.0", - "express-swagger-generator": "^1.1.17", + "express-jsdoc-swagger": "^1.8.0", "express-swaggerize-ui": "^1.1.0", "jsonwebtoken": "^9.0.0", "ldap-escape": "^2.0.6", @@ -35,6 +35,7 @@ "validator": "^13.9.0" }, "devDependencies": { + "@apidevtools/swagger-cli": "^4.0.4", "@types/bcrypt": "^5.0.0", "@types/body-parser": "^1.19.2", "@types/chai": "^4.3.4", @@ -52,6 +53,7 @@ "@types/nodemailer": "^6.4.7", "@types/sinon": "^10.0.13", "@types/sinon-chai": "^3.2.9", + "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.1", "@types/validator": "^13.7.13", "@typescript-eslint/eslint-plugin": "^5.55.0", @@ -93,6 +95,171 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", + "integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-cli": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-cli/-/swagger-cli-4.0.4.tgz", + "integrity": "sha512-hdDT3B6GLVovCsRZYDi3+wMcB1HfetTU20l2DC8zD3iFRNMC6QNAZG5fo/6PYeHWBEv7ri4MvnlKodhNB0nt7g==", + "deprecated": "This package has been abandoned. Please switch to using the actively maintained @redocly/cli", + "dev": true, + "dependencies": { + "@apidevtools/swagger-parser": "^10.0.1", + "chalk": "^4.1.0", + "js-yaml": "^3.14.0", + "yargs": "^15.4.1" + }, + "bin": { + "swagger-cli": "bin/swagger-cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-cli/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/@apidevtools/swagger-cli/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@apidevtools/swagger-cli/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/@apidevtools/swagger-cli/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@apidevtools/swagger-cli/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz", + "integrity": "sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==", + "dev": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "9.0.6", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.6.3", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@babel/compat-data": { "version": "7.18.8", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz", @@ -668,76 +835,6 @@ "node": ">=8" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -803,6 +900,12 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -1241,6 +1344,16 @@ "@types/node": "*" } }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", @@ -1649,6 +1762,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -2080,7 +2194,8 @@ "node_modules/call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw==" + "integrity": "sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw==", + "dev": true }, "node_modules/callsites": { "version": "3.1.0", @@ -2340,12 +2455,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "optional": true - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -2427,13 +2536,6 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, - "node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2659,7 +2761,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -2667,25 +2768,6 @@ "node": ">=6.0.0" } }, - "node_modules/doctrine-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/doctrine-file/-/doctrine-file-1.0.3.tgz", - "integrity": "sha512-OK37HbZtNmIMn84riibVXRmcEGUIf6BNfYMcbXg20ejP+LEsf4tnk8QfYy3EmQs4KzZFhTl3zwoKqVwARxpBgA==", - "dependencies": { - "doctrine": "^2.0.0" - } - }, - "node_modules/doctrine-file/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -3258,15 +3340,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/espree": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", @@ -3288,6 +3361,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -3397,34 +3471,27 @@ "node": ">=12.0.0" } }, - "node_modules/express-swagger-generator": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/express-swagger-generator/-/express-swagger-generator-1.1.17.tgz", - "integrity": "sha512-eKB2cR3TcvmSepkqjm9sFPqPAV7PQawyc3Df2p9/0vN4Q7LyBrLLpechH246YYJ1kIDPa8RresfhJeIHg5zS4A==", - "dependencies": { - "doctrine": "^2.0.0", - "doctrine-file": "^1.0.2", - "express-swaggerize-ui": "^1.0.3", - "glob": "^7.0.3", - "recursive-iterator": "^2.0.3", - "swagger-parser": "^5.0.5" - } - }, - "node_modules/express-swagger-generator/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/express-jsdoc-swagger": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/express-jsdoc-swagger/-/express-jsdoc-swagger-1.8.0.tgz", + "integrity": "sha512-1Ij+2tSRldJzduxi56Dm3zS87UKQl83VXanhy7GPmXHxChCxafPcbf0SCJTIu+NdO0kq0seSPE1fQdwaEq+Vrg==", "dependencies": { - "esutils": "^2.0.2" + "chalk": "^4.1.0", + "doctrine": "^3.0.0", + "express": "^4.17.1", + "glob": "^7.1.6", + "merge": "^2.1.1", + "swagger-ui-express": "^4.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.0.0" } }, "node_modules/express-swaggerize-ui": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/express-swaggerize-ui/-/express-swaggerize-ui-1.1.0.tgz", "integrity": "sha512-dDJuWV/GlISNYyKvFMa3EDr6sYzMgMrVRCt9o1kQxaIIKnmK1NJvaTzGbRIokIlGGHriIT6E2ztorRyRxLuOzA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dependencies": { "express": "^4.13.3" } @@ -3599,6 +3666,19 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -3662,11 +3742,6 @@ "node": ">= 0.12" } }, - "node_modules/format-util": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", - "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==" - }, "node_modules/formidable": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", @@ -3742,20 +3817,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -4810,6 +4871,7 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4839,26 +4901,6 @@ "node": ">= 4" } }, - "node_modules/json-schema-ref-parser": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-5.1.3.tgz", - "integrity": "sha512-CpDFlBwz/6la78hZxyB9FECVKGYjIIl3Ms3KLqFj99W7IIb7D00/RDgc++IGB4BBALl0QRhh5m4q5WNSopvLtQ==", - "deprecated": "Please switch to @apidevtools/json-schema-ref-parser", - "dependencies": { - "call-me-maybe": "^1.0.1", - "debug": "^3.1.0", - "js-yaml": "^3.12.0", - "ono": "^4.0.6" - } - }, - "node_modules/json-schema-ref-parser/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4891,19 +4933,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonschema": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", - "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==", - "engines": { - "node": "*" - } - }, - "node_modules/jsonschema-draft4": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz", - "integrity": "sha512-sBV3UnQPRiyCTD6uzY/Oao2Yohv6KKgQq7zjPwjFHeR6scg/QSXnzDxdugsGaLQDmFUrUlTbMYdEE+72PizhGA==" - }, "node_modules/jsonwebtoken": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", @@ -4979,6 +5008,18 @@ "node": ">= 0.8.0" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4993,7 +5034,8 @@ "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -5052,6 +5094,7 @@ "version": "2.3.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "deprecated": "Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5", "dev": true, "dependencies": { "get-func-name": "^2.0.0" @@ -5151,6 +5194,11 @@ "node": ">= 0.6" } }, + "node_modules/merge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", + "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -5565,15 +5613,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -6284,58 +6323,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/nyc/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nyc/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/nyc/node_modules/p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -6348,24 +6335,6 @@ "node": ">=8" } }, - "node_modules/nyc/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/nyc/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -6523,23 +6492,12 @@ "wrappy": "1" } }, - "node_modules/ono": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", - "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", - "dependencies": { - "format-util": "^1.0.3" - } - }, - "node_modules/openapi-schema-validation": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz", - "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==", - "dependencies": { - "jsonschema": "1.2.4", - "jsonschema-draft4": "^1.0.0", - "swagger-schema-official": "2.0.0-bab6bed" - } + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "peer": true }, "node_modules/optionator": { "version": "0.9.1", @@ -6586,6 +6544,33 @@ "os-tmpdir": "^1.0.0" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -6601,6 +6586,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/package-hash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", @@ -6654,6 +6648,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -6730,76 +6733,6 @@ "node": ">=8" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6987,11 +6920,6 @@ "node": ">=8.10.0" } }, - "node_modules/recursive-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/recursive-iterator/-/recursive-iterator-2.0.3.tgz", - "integrity": "sha512-SqfNKjjTw7Lq3E2S6P8L5Ac7YFD91mbkzWRlxrWK4tWioJRKDhe1+PtWS0X0hkQNEzZDpSish0TTXAVM4cRUzQ==" - }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -7034,6 +6962,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -7336,6 +7273,7 @@ "version": "15.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.2.tgz", "integrity": "sha512-PCVP63XZkg0/LOqQH5rEU4LILuvTFMb5tNxTHfs6VUMNnZz2XrnGSTZbAGITjzwQWbl/Bl/8hi4G3zZWjyBwHg==", + "deprecated": "16.1.1", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0", @@ -7464,7 +7402,8 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, "node_modules/sqlite3": { "version": "5.1.5", @@ -7981,12 +7920,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger-methods": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/swagger-methods/-/swagger-methods-1.0.8.tgz", - "integrity": "sha512-G6baCwuHA+C5jf4FNOrosE4XlmGsdjbOjdBK4yuiDDj/ro9uR4Srj3OR84oQMT8F3qKp00tYNv0YN730oTHPZA==", - "deprecated": "This package is no longer being maintained." - }, "node_modules/swagger-model-validator": { "version": "3.0.21", "resolved": "https://registry.npmjs.org/swagger-model-validator/-/swagger-model-validator-3.0.21.tgz", @@ -7995,34 +7928,25 @@ "lodash.isequal": "^4.5.0" } }, - "node_modules/swagger-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-5.0.6.tgz", - "integrity": "sha512-FdzCYFK11iGgrOpojlqUluU6SKThtzmu+5Get+6ValJR2TFwTnES1x4Fdfgy3C4/8VVXk4Va/WsqGlbyY/Os+A==", - "dependencies": { - "call-me-maybe": "^1.0.1", - "debug": "^3.1.0", - "json-schema-ref-parser": "^5.1.3", - "ono": "^4.0.6", - "openapi-schema-validation": "^0.4.2", - "swagger-methods": "^1.0.4", - "swagger-schema-official": "2.0.0-bab6bed", - "z-schema": "^3.23.0" - } + "node_modules/swagger-ui-dist": { + "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==" }, - "node_modules/swagger-parser/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/swagger-ui-express": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz", + "integrity": "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==", "dependencies": { - "ms": "^2.1.1" + "swagger-ui-dist": ">=4.11.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" } }, - "node_modules/swagger-schema-official": { - "version": "2.0.0-bab6bed", - "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", - "integrity": "sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==" - }, "node_modules/tar": { "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", @@ -9020,31 +8944,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/z-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.25.1.tgz", - "integrity": "sha512-7tDlwhrBG+oYFdXNOjILSurpfQyuVgkRe3hB2q8TEssamDHB7BbLWYkYO98nTn0FibfdFroFKDjndbgufAgS/Q==", - "dependencies": { - "core-js": "^2.5.7", - "lodash.get": "^4.0.0", - "lodash.isequal": "^4.0.0", - "validator": "^10.0.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "optionalDependencies": { - "commander": "^2.7.1" - } - }, - "node_modules/z-schema/node_modules/validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==", - "engines": { - "node": ">= 0.10" - } } } } diff --git a/package.json b/package.json index ae90d4f44..87ab2501b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "schema": "node out/src/database/schema.js", "seed": "ts-node-dev --poll src/database/seed.ts", "swagger": "ts-node-dev --poll src/start/swagger.ts", + "swagger:validate": "ts-node-dev --poll src/start/swagger.ts && swagger-cli validate ./out/swagger.json", "test": "mocha -r ts-node/register --parallel --timeout 50000 --require ./test/setup.ts 'test/**/*.ts' --exit", "test-ci": "mocha -r ts-node/register --parallel --jobs 4 --timeout 50000 --require ./test/setup.ts 'test/**/*.ts' --exit", "test-file": "mocha -r ts-node/register --timeout 10000 --require ./test/setup.ts", @@ -30,7 +31,7 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-fileupload": "^1.4.0", - "express-swagger-generator": "^1.1.17", + "express-jsdoc-swagger": "^1.8.0", "express-swaggerize-ui": "^1.1.0", "jsonwebtoken": "^9.0.0", "ldap-escape": "^2.0.6", @@ -49,6 +50,7 @@ "validator": "^13.9.0" }, "devDependencies": { + "@apidevtools/swagger-cli": "^4.0.4", "@types/bcrypt": "^5.0.0", "@types/body-parser": "^1.19.2", "@types/chai": "^4.3.4", @@ -66,6 +68,7 @@ "@types/nodemailer": "^6.4.7", "@types/sinon": "^10.0.13", "@types/sinon-chai": "^3.2.9", + "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.1", "@types/validator": "^13.7.13", "@typescript-eslint/eslint-plugin": "^5.55.0", diff --git a/src/controller/authentication-controller.ts b/src/controller/authentication-controller.ts index 7624d1543..b5cc7e049 100644 --- a/src/controller/authentication-controller.ts +++ b/src/controller/authentication-controller.ts @@ -159,14 +159,14 @@ export default class AuthenticationController extends BaseController { } /** - * PIN login and hand out token - * @route POST /authentication/pin + * POST /authentication/pin + * @summary PIN login and hand out token * @operationId pinAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationPinRequest.model} req.body.required - The PIN login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationPinRequest} request.body.required - The PIN login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. + * @return {string} 403 - Authentication error. */ public async PINLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationPinRequest; @@ -230,15 +230,15 @@ export default class AuthenticationController extends BaseController { } /** - * LDAP login and hand out token + * POST /authentication/LDAP + * @summary LDAP login and hand out token * If user has never signed in before this also creates an account. - * @route POST /authentication/LDAP * @operationId ldapAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationLDAPRequest.model} req.body.required - The LDAP login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationLDAPRequest} request.body.required - The LDAP login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. + * @return {string} 403 - Authentication error. */ public async LDAPLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationLDAPRequest; @@ -285,14 +285,14 @@ export default class AuthenticationController extends BaseController { } /** - * Local login and hand out token - * @route POST /authentication/local + * POST /authentication/local + * @summary Local login and hand out token * @operationId localAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationLocalRequest.model} req.body.required - The local login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationLocalRequest} request.body.required - The local login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. + * @return {string} 403 - Authentication error. */ public async LocalLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationLocalRequest; @@ -340,13 +340,13 @@ export default class AuthenticationController extends BaseController { } /** - * Reset local authentication using the provided token - * @route PUT /authentication/local + * PUT /authentication/local + * @summary Reset local authentication using the provided token * @operationId resetLocalWithToken - * @group authenticate - Operations of authentication controller - * @param {AuthenticationResetTokenRequest.model} req.body.required - The reset token. - * @returns {string} 204 - Successfully reset - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationResetTokenRequest} request.body.required - The reset token. + * @return {string} 204 - Successfully reset + * @return {string} 403 - Authentication error. */ public async resetLocalUsingToken(req: Request, res: Response): Promise { const body = req.body as AuthenticationResetTokenRequest; @@ -379,12 +379,12 @@ export default class AuthenticationController extends BaseController { } /** - * Creates a reset token for the local authentication - * @route POST /authentication/local/reset + * POST /authentication/local/reset + * @summary Creates a reset token for the local authentication * @operationId resetLocal - * @group authenticate - Operations of authentication controller - * @param {ResetLocalRequest.model} req.body.required - The reset info. - * @returns {string} 204 - Creation success + * @tags authenticate - Operations of authentication controller + * @param {ResetLocalRequest} request.body.required - The reset info. + * @return {string} 204 - Creation success */ public async createResetToken(req: Request, res: Response): Promise { const body = req.body as ResetLocalRequest; @@ -413,13 +413,13 @@ export default class AuthenticationController extends BaseController { } /** - * NFC login and hand out token - * @route POST /authentication/nfc + * POST /authentication/nfc + * @summary NFC login and hand out token * @operationId nfcAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationNfcRequest.model} req.body.required - The NFC login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationNfcRequest} request.body.required - The NFC login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 403 - Authentication error. */ public async nfcLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationNfcRequest; @@ -427,7 +427,7 @@ export default class AuthenticationController extends BaseController { try { const { nfcCode } = body; - const authenticator = await NfcAuthenticator.findOne({ where: { nfcCode: nfcCode } }); + const authenticator = await NfcAuthenticator.findOne({ where: { nfcCode: nfcCode } }); if (authenticator == null || authenticator.user == null) { res.status(403).json({ message: 'Invalid credentials.', @@ -451,13 +451,13 @@ export default class AuthenticationController extends BaseController { } /** - * EAN login and hand out token - * @route POST /authentication/ean + * POST /authentication/ean + * @summary EAN login and hand out token * @operationId eanAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationEanRequest.model} req.body.required - The EAN login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationEanRequest} request.body.required - The EAN login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 403 - Authentication error. */ public async eanLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationEanRequest; @@ -488,14 +488,14 @@ export default class AuthenticationController extends BaseController { /** - * Key login and hand out token. - * @route POST /authentication/key + * POST /authentication/key + * @summary Key login and hand out token. * @operationId keyAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationKeyRequest.model} req.body.required - The key login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationKeyRequest} request.body.required - The key login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. + * @return {string} 403 - Authentication error. */ public async keyLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationKeyRequest; @@ -543,13 +543,13 @@ export default class AuthenticationController extends BaseController { } /** - * Mock login and hand out token. - * @route POST /authentication/mock + * POST /authentication/mock + * @summary Mock login and hand out token. * @operationId mockAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationMockRequest.model} req.body.required - The mock login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationMockRequest} request.body.required - The mock login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. */ public async mockLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationMockRequest; diff --git a/src/controller/authentication-secure-controller.ts b/src/controller/authentication-secure-controller.ts index e4b9eda09..8ec1fcb43 100644 --- a/src/controller/authentication-secure-controller.ts +++ b/src/controller/authentication-secure-controller.ts @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -import { Response } from 'express'; -import log4js, { Logger } from 'log4js'; -import BaseController, { BaseControllerOptions } from './base-controller'; +import {Response} from 'express'; +import log4js, {Logger} from 'log4js'; +import BaseController, {BaseControllerOptions} from './base-controller'; import Policy from './policy'; -import { RequestWithToken } from '../middleware/token-middleware'; +import {RequestWithToken} from '../middleware/token-middleware'; import AuthenticationService from '../service/authentication-service'; import TokenHandler from '../authentication/token-handler'; import User from '../entity/user/user'; @@ -53,25 +53,25 @@ export default class AuthenticationSecureController extends BaseController { GET: { policy: async () => Promise.resolve(true), handler: this.refreshToken.bind(this), - restrictions: { lesser: true, acceptedTOS: false }, + restrictions: {lesser: true, acceptedTOS: false}, }, }, }; } /** - * Get a new JWT token, lesser if the existing token is also lesser - * @route get /authentication/refreshToken + * GET /authentication/refreshToken + * @summary Get a new JWT token, lesser if the existing token is also lesser * @operationId refreshToken - * @group authenticate - Operations of the authentication controller + * @tags authenticate - Operations of the authentication controller * @security JWT - * @returns {AuthenticationResponse.model} 200 - The created json web token. + * @return {AuthenticationResponse} 200 - The created json web token. */ private async refreshToken(req: RequestWithToken, res: Response): Promise { this.logger.trace('Refresh token for user', req.token.user.id); try { - const user = await User.findOne({ where: { id: req.token.user.id } }); + const user = await User.findOne({where: {id: req.token.user.id}}); const token = await AuthenticationService.getSaltedToken(user, { roleManager: this.roleManager, tokenHandler: this.tokenHandler, diff --git a/src/controller/balance-controller.ts b/src/controller/balance-controller.ts index ba4287272..262d17286 100644 --- a/src/controller/balance-controller.ts +++ b/src/controller/balance-controller.ts @@ -40,8 +40,8 @@ export default class BalanceController extends BaseController { } /** - * @inheritdoc - */ + * @inheritdoc + */ public getPolicy(): Policy { return { '/': { @@ -66,15 +66,15 @@ export default class BalanceController extends BaseController { } /** - * Get balance of the current user - * @route get /balances + * GET /balances + * @summary Get balance of the current user * @operationId getBalances - * @group balance - Operations of balance controller + * @tags balance - Operations of balance controller * @security JWT - * @returns {BalanceResponse.model} 200 - The requested user's balance - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {BalanceResponse} 200 - The requested user's balance + * @return {string} 400 - Validation error + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ // eslint-disable-next-line class-methods-use-this private async getOwnBalance(req: RequestWithToken, res: Response): Promise { @@ -87,10 +87,10 @@ export default class BalanceController extends BaseController { } /** - * Get balance of the current user - * @route GET /balances/all + * GET /balances/all + * @summary Get balance of the current user * @operationId getAllBalance - * @group balance - Operations of balance controller + * @tags balance - Operations of balance controller * @security JWT * @param {string} date.query - Timestamp to get balances for * @param {integer} minBalance.query - Minimum balance @@ -98,14 +98,14 @@ export default class BalanceController extends BaseController { * @param {boolean} hasFine.query - Only users with(out) fines * @param {integer} minFine.query - Minimum fine * @param {integer} maxFine.query - Maximum fine - * @param {string} userType[].query.enum{MEMBER,ORGAN,VOUCHER,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE} - Filter based on user type. - * @param {enum} orderBy.query - Column to order balance by - eg: id,amount - * @param {enum} orderDirection.query - Order direction - eg: ASC,DESC + * @param {string} userType.query - enum:MEMBER,ORGAN,VOUCHER,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE - Filter based on user type. + * @param {string} orderBy.query - Column to order balance by - eg: id,amount + * @param {string} orderDirection.query - enum:ASC,DESC - Order direction * @param {integer} take.query - How many transactions the endpoint should return * @param {integer} skip.query - How many transactions should be skipped (for pagination) - * @returns {Array} 200 - The requested user's balance - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {Array} 200 - The requested user's balance + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ private async getAllBalances(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all balances by', req.token.user); @@ -143,16 +143,16 @@ export default class BalanceController extends BaseController { } /** - * Retrieves the requested balance - * @route get /balances/{id} + * GET /balances/{id} + * @summary Retrieves the requested balance * @operationId getBalanceId - * @group balance - Operations of balance controller + * @tags balance - Operations of balance controller * @param {integer} id.path.required - The id of the user for which the saldo is requested * @security JWT - * @returns {BalanceResponse.model} 200 - The requested user's balance - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {BalanceResponse} 200 - The requested user's balance + * @return {string} 400 - Validation error + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ private async getBalance(req: RequestWithToken, res: Response): Promise { try { diff --git a/src/controller/banner-controller.ts b/src/controller/banner-controller.ts index 32bfeb16d..3270141bb 100644 --- a/src/controller/banner-controller.ts +++ b/src/controller/banner-controller.ts @@ -90,16 +90,16 @@ export default class BannerController extends BaseController { } /** - * Returns all existing banners - * @route GET /banners + * GET /banners + * @summary Returns all existing banners * @operationId getAllBanners - * @group banners - Operations of banner controller + * @tags banners - Operations of banner controller * @security JWT * @param {integer} take.query - How many banners the endpoint should return * @param {integer} skip.query - How many banners should be skipped (for pagination) - * @returns {PaginatedBannerResponse.model} 200 - All existing banners - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {PaginatedBannerResponse} 200 - All existing banners + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async returnAllBanners(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all banners by', req.token.user); @@ -125,15 +125,15 @@ export default class BannerController extends BaseController { } /** - * Saves a banner to the database - * @route POST /banners + * POST /banners + * @summary Saves a banner to the database * @operationId create - * @group banners - Operations of banner controller - * @param {BannerRequest.model} banner.body.required - The banner which should be created + * @tags banners - Operations of banner controller + * @param {BannerRequest} request.body.required - The banner which should be created * @security JWT - * @returns {BannerResponse.model} 200 - The created banner entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {BannerResponse} 200 - The created banner entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createBanner(req: RequestWithToken, res: Response): Promise { const body = req.body as BannerRequest; @@ -153,16 +153,16 @@ export default class BannerController extends BaseController { } /** - * Uploads a banner image to the given banner - * @route POST /banners/{id}/image + * POST /banners/{id}/image + * @summary Uploads a banner image to the given banner * @operationId updateImage - * @group banners - Operations of banner controller + * @tags banners - Operations of banner controller * @param {integer} id.path.required - The id of the banner - * @param {file} file.formData + * @param {FileRequest} request.body.required - banner image - multipart/form-data * @security JWT - * @returns 204 - Success - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return 204 - Success + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async uploadBannerImage(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -207,15 +207,15 @@ export default class BannerController extends BaseController { } /** - * Returns the requested banner - * @route GET /banners/{id} + * GET /banners/{id} + * @summary Returns the requested banner * @operationId getBanner - * @group banners - Operations of banner controller + * @tags banners - Operations of banner controller * @param {integer} id.path.required - The id of the banner which should be returned * @security JWT - * @returns {BannerResponse.model} 200 - The requested banner entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {BannerResponse} 200 - The requested banner entity + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async returnSingleBanner(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -237,17 +237,17 @@ export default class BannerController extends BaseController { } /** - * Updates the requested banner - * @route PATCH /banners/{id} + * PATCH /banners/{id} + * @summary Updates the requested banner * @operationId update - * @group banners - Operations of banner controller + * @tags banners - Operations of banner controller * @param {integer} id.path.required - The id of the banner which should be updated - * @param {BannerRequest.model} banner.body.required - The updated banner + * @param {BannerRequest} request.body.required - The updated banner * @security JWT - * @returns {BannerResponse.model} 200 - The requested banner entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {BannerResponse} 200 - The requested banner entity + * @return {string} 400 - Validation error + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async updateBanner(req: RequestWithToken, res: Response): Promise { const body = req.body as BannerRequest; @@ -274,14 +274,14 @@ export default class BannerController extends BaseController { } /** - * Deletes the requested banner - * @route DELETE /banners/{id} + * DELETE /banners/{id} + * @summary Deletes the requested banner * @operationId delete - * @group banners - Operations of banner controller + * @tags banners - Operations of banner controller * @param {integer} id.path.required - The id of the banner which should be deleted * @security JWT - * @returns {BannerResponse.model} 200 - The deleted banner entity - * @returns {string} 404 - Not found error + * @return {BannerResponse} 200 - The deleted banner entity + * @return {string} 404 - Not found error */ public async removeBanner(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -303,15 +303,15 @@ export default class BannerController extends BaseController { } /** - * Returns all active banners - * @route GET /banners/active + * GET /banners/active + * @summary Returns all active banners * @operationId getActive - * @group banners - Operations of banner controller + * @tags banners - Operations of banner controller * @security JWT * @param {integer} take.query - How many banners the endpoint should return * @param {integer} skip.query - How many banners should be skipped (for pagination) - * @returns {PaginatedBannerResponse.model} 200 - All active banners - * @returns {string} 400 - Validation error + * @return {PaginatedBannerResponse} 200 - All active banners + * @return {string} 400 - Validation error */ public async returnActiveBanners(req: RequestWithToken, res: Response): Promise { const { body } = req; diff --git a/src/controller/base-controller.ts b/src/controller/base-controller.ts index 3d727186c..a79b3aa85 100644 --- a/src/controller/base-controller.ts +++ b/src/controller/base-controller.ts @@ -46,7 +46,7 @@ export default abstract class BaseController { /** * A reference to the swagger specification passed in the base controller options. */ - protected specification: SwaggerSpecification; + public specification: SwaggerSpecification; /** * A reference to the role manager passed in the base controller options. @@ -122,12 +122,12 @@ export default abstract class BaseController { /** * Gets the policy defined by child classes. This policy includes all routes that the controller * accepts, the authorization middleware, and the final handler function for every route. - * @returns The policy of this controller. + * @return The policy of this controller. */ public abstract getPolicy(): Policy; /** - * @returns the router used by this controller. + * @return the router used by this controller. */ public getRouter(): Router { return this.router; diff --git a/src/controller/container-controller.ts b/src/controller/container-controller.ts index 26f2f892a..a11d6e1f9 100644 --- a/src/controller/container-controller.ts +++ b/src/controller/container-controller.ts @@ -92,15 +92,15 @@ export default class ContainerController extends BaseController { } /** - * Returns all existing containers - * @route GET /containers + * GET /containers + * @summary Returns all existing containers * @operationId getAllContainers - * @group containers - Operations of container controller + * @tags containers - Operations of container controller * @security JWT * @param {integer} take.query - How many containers the endpoint should return * @param {integer} skip.query - How many containers should be skipped (for pagination) - * @returns {PaginatedContainerResponse.model} 200 - All existing containers - * @returns {string} 500 - Internal server error + * @return {PaginatedContainerResponse} 200 - All existing containers + * @return {string} 500 - Internal server error */ public async getAllContainers(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -121,16 +121,16 @@ export default class ContainerController extends BaseController { } /** - * Returns the requested container - * @route GET /containers/{id} + * GET /containers/{id} + * @summary Returns the requested container * @operationId getSingleContainer - * @group containers - Operations of container controller + * @tags containers - Operations of container controller * @param {integer} id.path.required - The id of the container which should be returned * @security JWT - * @returns {ContainerWithProductsResponse.model} 200 - The requested container - * @returns {string} 404 - Not found error - * @returns {string} 403 - Incorrect permissions - * @returns {string} 500 - Internal server error + * @return {ContainerWithProductsResponse} 200 - The requested container + * @return {string} 404 - Not found error + * @return {string} 403 - Incorrect permissions + * @return {string} 500 - Internal server error */ public async getSingleContainer(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -157,17 +157,17 @@ export default class ContainerController extends BaseController { } /** - * Returns all the products in the container - * @route GET /containers/{id}/products + * GET /containers/{id}/products + * @summary Returns all the products in the container * @operationId getProductsContainer - * @group containers - Operations of container controller + * @tags containers - Operations of container controller * @param {integer} id.path.required - The id of the container which should be returned * @security JWT * @param {integer} take.query - How many products the endpoint should return * @param {integer} skip.query - How many products should be skipped (for pagination) - * @returns {PaginatedProductResponse.model} 200 - All products in the container - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {PaginatedProductResponse} 200 - All products in the container + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async getProductsContainer(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -193,16 +193,16 @@ export default class ContainerController extends BaseController { } /** - * Create a new container. - * @route POST /containers + * POST /containers + * @summary Create a new container. * @operationId createContainer - * @group containers - Operations of container controller - * @param {CreateContainerRequest.model} container.body.required - + * @tags containers - Operations of container controller + * @param {CreateContainerRequest} request.body.required - * The container which should be created * @security JWT - * @returns {ContainerWithProductsResponse.model} 200 - The created container entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {ContainerWithProductsResponse} 200 - The created container entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createContainer(req: RequestWithToken, res: Response): Promise { const body = req.body as CreateContainerRequest; @@ -229,15 +229,15 @@ export default class ContainerController extends BaseController { } /** - * Returns all public container - * @route GET /containers/public + * GET /containers/public + * @summary Returns all public container * @operationId getPublicContainers - * @group containers - Operations of container controller + * @tags containers - Operations of container controller * @security JWT * @param {integer} take.query - How many containers the endpoint should return * @param {integer} skip.query - How many containers should be skipped (for pagination) - * @returns {PaginatedContainerResponse.model} 200 - All existing public containers - * @returns {string} 500 - Internal server error + * @return {PaginatedContainerResponse} 200 - All existing public containers + * @return {string} 500 - Internal server error */ public async getPublicContainers(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -258,18 +258,18 @@ export default class ContainerController extends BaseController { } /** - * Update an existing container. - * @route PATCH /containers/{id} + * PATCH /containers/{id} + * @summary Update an existing container. * @operationId updateContainer - * @group containers - Operations of container controller + * @tags containers - Operations of container controller * @param {integer} id.path.required - The id of the container which should be updated - * @param {UpdateContainerRequest.model} container.body.required - + * @param {UpdateContainerRequest} request.body.required - * The container which should be updated * @security JWT - * @returns {ContainerWithProductsResponse.model} 200 - The created container entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Product not found error - * @returns {string} 500 - Internal server error + * @return {ContainerWithProductsResponse} 200 - The created container entity + * @return {string} 400 - Validation error + * @return {string} 404 - Product not found error + * @return {string} 500 - Internal server error */ public async updateContainer(req: RequestWithToken, res: Response): Promise { const body = req.body as UpdateContainerRequest; @@ -309,7 +309,7 @@ export default class ContainerController extends BaseController { * 'organ' if user is not connected to container via organ * 'own' if user is connected to container * @param req - * @returns whether container is connected to used token + * @return whether container is connected to used token */ static async getRelation(req: RequestWithToken): Promise { const containerId = asNumber(req.params.id); diff --git a/src/controller/debtor-controller.ts b/src/controller/debtor-controller.ts index d1f6f6846..f2cd05a2c 100644 --- a/src/controller/debtor-controller.ts +++ b/src/controller/debtor-controller.ts @@ -80,16 +80,16 @@ export default class DebtorController extends BaseController { } /** - * Get all fine handout events - * @route GET /fines - * @group debtors - Operations of the debtor controller + * GET /fines + * @summary Get all fine handout events + * @tags debtors - Operations of the debtor controller * @operationId returnAllFineHandoutEvents * @security JWT * @param {integer} take.query - How many entries the endpoint should return * @param {integer} skip.query - How many entries should be skipped (for pagination) - * @returns {PaginatedFineHandoutEventResponse.model} 200 - All existing fine handout events - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {PaginatedFineHandoutEventResponse} 200 - All existing fine handout events + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async returnAllFineHandoutEvents(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all fine handout events by ', req.token.user); @@ -114,15 +114,15 @@ export default class DebtorController extends BaseController { } /** - * Get all fine handout events - * @route GET /fines/{id} - * @group debtors - Operations of the debtor controller + * GET /fines/{id} + * @summary Get all fine handout events + * @tags debtors - Operations of the debtor controller * @operationId returnSingleFineHandoutEvent * @security JWT * @param {integer} id.path.required - The id of the fine handout event which should be returned - * @returns {FineHandoutEventResponse.model} 200 - Requested fine handout event with corresponding fines - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {FineHandoutEventResponse} 200 - Requested fine handout event with corresponding fines + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async returnSingleFineHandoutEvent(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -137,15 +137,15 @@ export default class DebtorController extends BaseController { } /** - * Delete a fine - * @route DELETE /fines/single/{id} - * @group debtors - Operations of the debtor controller + * DELETE /fines/single/{id} + * @summary Delete a fine + * @tags debtors - Operations of the debtor controller * @operationId deleteFine * @security JWT * @param {integer} id.path.required - The id of the fine which should be deleted - * @returns {string} 204 - Success - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {string} 204 - Success + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async deleteFine(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -171,15 +171,15 @@ export default class DebtorController extends BaseController { * Return all users that had at most -5 euros balance both now and on the reference date * For all these users, also return their fine based on the reference date. * @route GET /fines/eligible - * @group debtors - Operations of the debtor controller + * @tags debtors - Operations of the debtor controller * @operationId calculateFines * @security JWT * @param {Array.} userTypes[].query - List of all user types fines should be calculated for * @param {Array.} referenceDates[].query.required - Dates to base the fines on. Every returned user has at * least five euros debt on every reference date. The height of the fine is based on the first date in the array. - * @returns {Array} 200 - List of eligible fines - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {Array} 200 - List of eligible fines + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async calculateFines(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all possible fines by ', req.token.user); @@ -208,15 +208,15 @@ export default class DebtorController extends BaseController { } /** - * Handout fines to all given users. Fines will be handed out "now" to prevent rewriting history. - * @route POST /fines/handout - * @group debtors - Operations of the debtor controller + * POST /fines/handout + * @summary Handout fines to all given users. Fines will be handed out "now" to prevent rewriting history. + * @tags debtors - Operations of the debtor controller * @operationId handoutFines * @security JWT - * @param {HandoutFinesRequest.model} body.body.required - * @returns {FineHandoutEventResponse.model} 200 - Created fine handout event with corresponding fines - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {HandoutFinesRequest} request.body.required + * @return {FineHandoutEventResponse} 200 - Created fine handout event with corresponding fines + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async handoutFines(req: RequestWithToken, res: Response): Promise { const body = req.body as HandoutFinesRequest; @@ -247,15 +247,15 @@ export default class DebtorController extends BaseController { } /** - * Send an email to all given users about their possible future fine. - * @route POST /fines/notify - * @group debtors - Operations of the debtor controller + * POST /fines/notify + * @summary Send an email to all given users about their possible future fine. + * @tags debtors - Operations of the debtor controller * @operationId notifyAboutFutureFines * @security JWT - * @param {HandoutFinesRequest.model} body.body.required - * @returns {string} 204 - Success - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {HandoutFinesRequest} request.body.required + * @return {string} 204 - Success + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async notifyAboutFutureFines(req: RequestWithToken, res: Response): Promise { const body = req.body as HandoutFinesRequest; diff --git a/src/controller/event-controller.ts b/src/controller/event-controller.ts index 6a4a72e15..7a26dc9cf 100644 --- a/src/controller/event-controller.ts +++ b/src/controller/event-controller.ts @@ -106,9 +106,9 @@ export default class EventController extends BaseController { } /** - * Get all events - * @route GET /events - * @group events - Operations of the event controller + * GET /events + * @summary Get all events + * @tags events - Operations of the event controller * @operationId getAllEvents * @security JWT * @param {string} name.query - Name of the event @@ -118,9 +118,9 @@ export default class EventController extends BaseController { * @param {string} type.query - Get only events that are this type * @param {integer} take.query - How many entries the endpoint should return * @param {integer} skip.query - How many entries should be skipped (for pagination) - * @returns {PaginatedBaseEventResponse.model} 200 - All existing events - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {PaginatedBaseEventResponse} 200 - All existing events + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async getAllEvents(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all events by user', req.token.user); @@ -155,15 +155,15 @@ export default class EventController extends BaseController { } /** - * Get a single event with its answers and shifts - * @route GET /events/{id} - * @group events - Operations of the event controller + * GET /events/{id} + * @summary Get a single event with its answers and shifts + * @tags events - Operations of the event controller * @operationId getSingleEvent * @security JWT * @param {integer} id.path.required - The id of the event which should be returned - * @returns {EventResponse.model} 200 - All existing events - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {EventResponse} 200 - All existing events + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async getSingleEvent(req: RequestWithToken, res: Response) { const { id } = req.params; @@ -184,15 +184,15 @@ export default class EventController extends BaseController { } /** - * Create an event with its corresponding answers objects - * @route POST /events - * @group events - Operations of the event controller + * POST /events + * @summary Create an event with its corresponding answers objects + * @tags events - Operations of the event controller * @operationId createEvent * @security JWT - * @param {CreateEventRequest.model} body.body.required - * @returns {EventResponse.model} 200 - Created event - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {CreateEventRequest} request.body.required + * @return {EventResponse} 200 - Created event + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createEvent(req: RequestWithToken, res: Response) { const body = req.body as EventRequest; @@ -219,16 +219,16 @@ export default class EventController extends BaseController { } /** - * Update an event with its corresponding answers objects - * @route PATCH /events/{id} - * @group events - Operations of the event controller + * PATCH /events/{id} + * @summary Update an event with its corresponding answers objects + * @tags events - Operations of the event controller * @operationId updateEvent * @security JWT * @param {integer} id.path.required - The id of the event which should be returned - * @param {UpdateEventRequest.model} body.body.required - * @returns {EventResponse.model} 200 - Created event - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {UpdateEventRequest} request.body.required + * @return {EventResponse} 200 - Created event + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async updateEvent(req: RequestWithToken, res: Response) { const { id } = req.params; @@ -267,15 +267,15 @@ export default class EventController extends BaseController { } /** - * Delete an event with its answers - * @route DELETE /events/{id} - * @group events - Operations of the event controller + * DELETE /events/{id} + * @summary Delete an event with its answers + * @tags events - Operations of the event controller * @operationId deleteEvent * @security JWT * @param {integer} id.path.required - The id of the event which should be deleted - * @returns {string} 204 - Success - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {string} 204 - Success + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async deleteEvent(req: RequestWithToken, res: Response) { const { id } = req.params; @@ -300,14 +300,14 @@ export default class EventController extends BaseController { /** * Synchronize an event, so that EventShiftAnswers are created/deleted * for users that are (no longer) part of a shift - * @route POST /events/{id}/sync - * @group events - Operations of the event controller + * @route GET /events/{id}/sync + * @tags events - Operations of the event controller * @operationId syncEventShiftAnswers * @security JWT * @param {integer} id.path.required - The id of the event which should be returned - * @returns {EventResponse.model} 200 - All existing events - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {EventResponse} 200 - All existing events + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async syncEventShiftAnswers(req: RequestWithToken, res: Response) { const { id } = req.params; @@ -330,18 +330,18 @@ export default class EventController extends BaseController { } /** - * Change the assignment of users to shifts on an event - * @route PUT /events/{eventId}/shift/{shiftId}/user/{userId}/assign - * @group events - Operations of the event controller + * PUT /events/{eventId}/shift/{shiftId}/user/{userId}/assign + * @summary Change the assignment of users to shifts on an event + * @tags events - Operations of the event controller * @operationId assignEventShift * @security JWT * @param {integer} eventId.path.required - The id of the event * @param {integer} shiftId.path.required - The id of the shift * @param {integer} userId.path.required - The id of the user - * @param {EventAnswerAssignmentRequest.model} body.body.required - * @returns {BaseEventAnswerResponse.model} 200 - Created event - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {EventAnswerAssignmentRequest} request.body.required + * @return {BaseEventAnswerResponse} 200 - Created event + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async assignEventShift(req: RequestWithToken, res: Response) { const { eventId: rawEventId, shiftId: rawShiftId, userId: rawUserId } = req.params; @@ -382,18 +382,18 @@ export default class EventController extends BaseController { } /** - * Update the availability of a user for a shift in an event - * @route POST /events/{eventId}/shift/{shiftId}/user/{userId}/availability - * @group events - Operations of the event controller + * POST /events/{eventId}/shift/{shiftId}/user/{userId}/availability + * @summary Update the availability of a user for a shift in an event + * @tags events - Operations of the event controller * @operationId updateEventShiftAvailability * @security JWT * @param {integer} eventId.path.required - The id of the event * @param {integer} shiftId.path.required - The id of the shift * @param {integer} userId.path.required - The id of the user - * @param {EventAnswerAvailabilityRequest.model} body.body.required - * @returns {BaseEventAnswerResponse.model} 200 - Created event - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {EventAnswerAvailabilityRequest} request.body.required + * @return {BaseEventAnswerResponse} 200 - Created event + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async updateShiftAvailability(req: RequestWithToken, res: Response) { const { userId: rawUserId, shiftId: rawShiftId, eventId: rawEventId } = req.params; diff --git a/src/controller/event-shift-controller.ts b/src/controller/event-shift-controller.ts index 86cc14609..0ea36c68e 100644 --- a/src/controller/event-shift-controller.ts +++ b/src/controller/event-shift-controller.ts @@ -76,16 +76,16 @@ export default class EventShiftController extends BaseController { } /** - * Get all event shifts - * @route GET /eventshifts - * @group events - Operations of the event controller + * GET /eventshifts + * @summary Get all event shifts + * @tags events - Operations of the event controller * @operationId getAllEventShifts * @security JWT * @param {integer} take.query - How many entries the endpoint should return * @param {integer} skip.query - How many entries should be skipped (for pagination) - * @returns {PaginatedEventShiftResponse.model} 200 - All existing event shifts - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {PaginatedEventShiftResponse} 200 - All existing event shifts + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async getAllShifts(req: RequestWithToken, res: Response) { this.logger.trace('Get all shifts by user', req.token.user); @@ -111,15 +111,15 @@ export default class EventShiftController extends BaseController { } /** - * Create an event shift - * @route POST /eventshifts - * @group events - Operations of the event controller + * POST /eventshifts + * @summary Create an event shift + * @tags events - Operations of the event controller * @operationId createEventShift * @security JWT - * @param {CreateShiftRequest.model} body.body.required - * @returns {EventShiftResponse.model} 200 - Created event shift - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {CreateShiftRequest} request.body.required + * @return {EventShiftResponse} 200 - Created event shift + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createShift(req: RequestWithToken, res: Response) { const body = req.body as EventShiftRequest; @@ -150,16 +150,16 @@ export default class EventShiftController extends BaseController { } /** - * Update an event shift - * @route PATCH /eventshifts/{id} - * @group events - Operations of the event controller + * PATCH /eventshifts/{id} + * @summary Update an event shift + * @tags events - Operations of the event controller * @operationId updateEventShift * @security JWT * @param {integer} id.path.required - The id of the event which should be returned - * @param {UpdateShiftRequest.model} body.body.required - * @returns {EventShiftResponse.model} 200 - Created event shift - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @param {UpdateShiftRequest} request.body.required + * @return {EventShiftResponse} 200 - Created event shift + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async updateShift(req: RequestWithToken, res: Response) { const { id: rawId } = req.params; @@ -203,15 +203,15 @@ export default class EventShiftController extends BaseController { } /** - * Delete an event shift with its answers - * @route DELETE /eventshifts/{id} - * @group events - Operations of the event controller + * DELETE /eventshifts/{id} + * @summary Delete an event shift with its answers + * @tags events - Operations of the event controller * @operationId deleteEventShift * @security JWT * @param {integer} id.path.required - The id of the event which should be deleted - * @returns {string} 204 - Success - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {string} 204 - Success + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async deleteShift(req: RequestWithToken, res: Response) { const { id: rawId } = req.params; @@ -234,18 +234,18 @@ export default class EventShiftController extends BaseController { } /** - * Get the number of times a user has been selected for the given shift - * @route GET /eventshifts/{id}/counts - * @group events - Operations of the event controller - * @operationId getShiftSelectedCount + * GET /eventshifts/{id}/counts + * @summary Get the number of times a user has been selected for the given shift + * @tags events - Operations of the event controller + * @operationId getEventShiftCount * @security JWT - * @param {integer} id.path.required - The id of the event which should be deleted + * @param {integer} id.path.required - The id of the event shift * @param {string} eventType.query - Only include events of this type * @param {string} afterDate.query - Only include events after this date * @param {string} beforeDate.query - Only include events before this date - * @returns {Array.} 200 - Users with how many times they did this shift - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {Array} 200 - All existing event shifts + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async getShiftSelectedCount(req: RequestWithToken, res: Response) { const { id: rawId } = req.params; diff --git a/src/controller/invoice-controller.ts b/src/controller/invoice-controller.ts index 867947b8a..aa95807e5 100644 --- a/src/controller/invoice-controller.ts +++ b/src/controller/invoice-controller.ts @@ -81,10 +81,10 @@ export default class InvoiceController extends BaseController { } /** - * Returns all invoices in the system. - * @route GET /invoices + * GET /invoices + * @summary Returns all invoices in the system. * @operationId getAllInvoices - * @group invoices - Operations of the invoices controller + * @tags invoices - Operations of the invoices controller * @security JWT * @param {integer} toId.query - Filter on Id of the debtor * @param {number} invoiceId.query - Filter on invoice ID @@ -95,8 +95,8 @@ export default class InvoiceController extends BaseController { * @param {string} tillDate.query - End date for selected invoices (exclusive) * @param {integer} take.query - How many entries the endpoint should return * @param {integer} skip.query - How many entries should be skipped (for pagination) - * @returns {PaginatedInvoiceResponse.model} 200 - All existing invoices - * @returns {string} 500 - Internal server error + * @return {PaginatedInvoiceResponse} 200 - All existing invoices + * @return {string} 500 - Internal server error */ public async getAllInvoices(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -128,17 +128,17 @@ export default class InvoiceController extends BaseController { } /** - * Returns a single invoice in the system. - * @route GET /invoices/{id} + * GET /invoices/{id} + * @summary Returns a single invoice in the system. * @operationId getSingleInvoice * @param {integer} id.path.required - The id of the requested invoice - * @group invoices - Operations of the invoices controller + * @tags invoices - Operations of the invoices controller * @security JWT * @param {boolean} returnEntries.query - * Boolean if invoice entries should be returned, defaults to true. - * @returns {InvoiceResponse.model} 200 - All existing invoices - * @returns {string} 404 - Invoice not found - * @returns {string} 500 - Internal server error + * @return {InvoiceResponse} 200 - All existing invoices + * @return {string} 404 - Invoice not found + * @return {string} 500 - Internal server error */ public async getSingleInvoice(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -166,16 +166,16 @@ export default class InvoiceController extends BaseController { } /** - * Adds an invoice to the system. - * @route POST /invoices + * POST /invoices + * @summary Adds an invoice to the system. * @operationId createInvoice - * @group invoices - Operations of the invoices controller + * @tags invoices - Operations of the invoices controller * @security JWT - * @param {CreateInvoiceRequest.model} invoice.body.required - + * @param {CreateInvoiceRequest} request.body.required - * The invoice which should be created - * @returns {InvoiceResponse.model} 200 - The created invoice entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {InvoiceResponse} 200 - The created invoice entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createInvoice(req: RequestWithToken, res: Response): Promise { const body = req.body as CreateInvoiceRequest; @@ -203,17 +203,17 @@ export default class InvoiceController extends BaseController { } /** - * Adds an invoice to the system. - * @route PATCH /invoices/{id} + * PATCH /invoices/{id} + * @summary Adds an invoice to the system. * @operationId updateInvoice - * @group invoices - Operations of the invoices controller + * @tags invoices - Operations of the invoices controller * @security JWT * @param {integer} id.path.required - The id of the invoice which should be updated - * @param {UpdateInvoiceRequest.model} invoice.body.required - + * @param {UpdateInvoiceRequest} request.body.required - * The invoice update to process - * @returns {BaseInvoiceResponse.model} 200 - The updated invoice entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {BaseInvoiceResponse} 200 - The updated invoice entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async updateInvoice(req: RequestWithToken, res: Response): Promise { const body = req.body as UpdateInvoiceRequest; @@ -244,15 +244,15 @@ export default class InvoiceController extends BaseController { } /** - * Deletes an invoice. - * @route DELETE /invoices/{id} + * DELETE /invoices/{id} + * @summary Deletes an invoice. * @operationId deleteInvoice - * @group invoices - Operations of the invoices controller + * @tags invoices - Operations of the invoices controller * @security JWT * @param {integer} id.path.required - The id of the invoice which should be deleted * @return {string} 404 - Invoice not found - * @return {BaseInvoiceResponse.model} 200 - The deleted invoice. - * @returns {string} 500 - Internal server error + * @return {BaseInvoiceResponse} 200 - The deleted invoice. + * @return {string} 500 - Internal server error */ // TODO Deleting of invoices that are not of state CREATED? public async deleteInvoice(req: RequestWithToken, res: Response): Promise { @@ -278,7 +278,7 @@ export default class InvoiceController extends BaseController { * all if user is not connected to invoice * own if user is connected to invoice * @param req - * @returns whether invoice is connected to used token + * @return whether invoice is connected to used token */ static async getRelation(req: RequestWithToken): Promise { const invoice: Invoice = await Invoice.findOne({ where: { id: parseInt(req.params.id, 10) }, relations: ['to'] }); diff --git a/src/controller/payout-request-controller.ts b/src/controller/payout-request-controller.ts index a25856ed7..c391c9e65 100644 --- a/src/controller/payout-request-controller.ts +++ b/src/controller/payout-request-controller.ts @@ -69,10 +69,10 @@ export default class PayoutRequestController extends BaseController { } /** - * Returns all payout requests given the filter parameters - * @route GET /payoutrequests + * GET /payoutrequests + * @summary Returns all payout requests given the filter parameters * @operationId getAllPayoutRequests - * @group payoutRequests - Operations of the payout request controller + * @tags payoutRequests - Operations of the payout request controller * @security JWT * @param {integer | Array} requestedById.query - ID of user(s) who requested a payout * @param {integer | Array} approvedById.query - ID of user(s) who approved a payout @@ -83,9 +83,9 @@ export default class PayoutRequestController extends BaseController { * @items.type {string} * @param {integer} take.query - How many payout requests the endpoint should return * @param {integer} skip.query - How many payout requests should be skipped (for pagination) - * @returns {PaginatedBasePayoutRequestResponse.model} 200 - All existing payout requests - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {PaginatedBasePayoutRequestResponse} 200 - All existing payout requests + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async returnAllPayoutRequests(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all payout requests by user', req.token.user); @@ -110,14 +110,14 @@ export default class PayoutRequestController extends BaseController { } /** - * Get a single payout request - * @route GET /payoutrequests/{id} + * GET /payoutrequests/{id} + * @summary Get a single payout request * @operationId getSinglePayoutRequest - * @group payoutRequests - Operations of the payout request controller + * @tags payoutRequests - Operations of the payout request controller * @param {integer} id.path.required - The ID of the payout request object that should be returned * @security JWT - * @returns {PayoutRequestResponse.model} 200 - Single payout request with given id - * @returns {string} 404 - Nonexistent payout request id + * @return {PayoutRequestResponse} 200 - Single payout request with given id + * @return {string} 404 - Nonexistent payout request id */ public async returnSinglePayoutRequest(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -142,14 +142,14 @@ export default class PayoutRequestController extends BaseController { } /** - * Create a new payout request - * @route POST /payoutrequests + * POST /payoutrequests + * @summary Create a new payout request * @operationId createPayoutRequest - * @group payoutRequests - Operations of the payout request controller - * @param {PayoutRequestRequest.model} payoutRequest.body.required - New payout request + * @tags payoutRequests - Operations of the payout request controller + * @param {PayoutRequestRequest} request.body.required - New payout request * @security JWT - * @returns {PayoutRequestResponse.model} 200 - * @returns {string} 400 - Validation error + * @return {PayoutRequestResponse} 200 - The created payout request. + * @return {string} 400 - Validation error */ public async createPayoutRequest(req: RequestWithToken, res: Response): Promise { const body = req.body as PayoutRequestRequest; @@ -165,16 +165,16 @@ export default class PayoutRequestController extends BaseController { } /** - * Create a new status for a payout request - * @route POST /payoutrequests/{id}/status + * POST /payoutrequests/{id}/status + * @summary Create a new status for a payout request * @operationId setPayoutRequestStatus - * @group payoutRequests - Operations of the payout request controller + * @tags payoutRequests - Operations of the payout request controller * @param {integer} id.path.required - The ID of the payout request object that should be returned - * @param {PayoutRequestStatusRequest.model} state.body.required - New state of payout request + * @param {PayoutRequestStatusRequest} request.body.required - New state of payout request * @security JWT - * @returns {PayoutRequestResponse.model} 200 - * @returns {string} 400 - Validation error - * @returns {string} 404 - Nonexistent payout request id + * @return {PayoutRequestResponse} 200 - The updated payout request + * @return {string} 400 - Validation error + * @return {string} 404 - Nonexistent payout request id */ public async updatePayoutRequestStatus(req: RequestWithToken, res: Response): Promise { const parameters = req.params; diff --git a/src/controller/point-of-sale-controller.ts b/src/controller/point-of-sale-controller.ts index a8fcaee74..51da42b69 100644 --- a/src/controller/point-of-sale-controller.ts +++ b/src/controller/point-of-sale-controller.ts @@ -100,16 +100,16 @@ export default class PointOfSaleController extends BaseController { } /** - * Create a new Point of Sale. - * @route POST /pointsofsale + * POST /pointsofsale + * @summary Create a new Point of Sale. * @operationId createPointOfSale - * @group pointofsale - Operations of the point of sale controller - * @param {CreatePointOfSaleRequest.model} pointofsale.body.required - + * @tags pointofsale - Operations of the point of sale controller + * @param {CreatePointOfSaleRequest} request.body.required - * The point of sale which should be created * @security JWT - * @returns {PointOfSaleWithContainersResponse.model} 200 - The created point of sale entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {PointOfSaleWithContainersResponse} 200 - The created point of sale entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createPointOfSale(req: RequestWithToken, res: Response): Promise { const body = req.body as CreatePointOfSaleRequest; @@ -137,15 +137,15 @@ export default class PointOfSaleController extends BaseController { } /** - * Returns all existing Point of Sales - * @route GET /pointsofsale + * GET /pointsofsale + * @summary Returns all existing Point of Sales * @operationId getAllPointsOfSale - * @group pointofsale - Operations of the point of sale controller + * @tags pointofsale - Operations of the point of sale controller * @security JWT * @param {integer} take.query - How many points of sale the endpoint should return * @param {integer} skip.query - How many points of sale should be skipped (for pagination) - * @returns {PaginatedPointOfSaleResponse.model} 200 - All existing point of sales - * @returns {string} 500 - Internal server error + * @return {PaginatedPointOfSaleResponse} 200 - All existing point of sales + * @return {string} 500 - Internal server error */ public async returnAllPointsOfSale(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -173,15 +173,15 @@ export default class PointOfSaleController extends BaseController { } /** - * Returns the requested Point of Sale - * @route GET /pointsofsale/{id} + * GET /pointsofsale/{id} + * @summary Returns the requested Point of Sale * @operationId getSinglePointOfSale - * @group pointofsale - Operations of the point of sale controller + * @tags pointofsale - Operations of the point of sale controller * @param {integer} id.path.required - The id of the Point of Sale which should be returned * @security JWT - * @returns {PointOfSaleWithContainersResponse.model} 200 - The requested point of sale entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {PointOfSaleWithContainersResponse} 200 - The requested point of sale entity + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async returnSinglePointOfSale(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -208,18 +208,18 @@ export default class PointOfSaleController extends BaseController { } /** - * Update an existing Point of Sale. - * @route PATCH /pointsofsale/{id} + * PATCH /pointsofsale/{id} + * @summary Update an existing Point of Sale. * @operationId updatePointOfSale - * @group pointofsale - Operations of the point of sale controller + * @tags pointofsale - Operations of the point of sale controller * @param {integer} id.path.required - The id of the Point of Sale which should be updated - * @param {UpdatePointOfSaleRequest.model} pointofsale.body.required - + * @param {UpdatePointOfSaleRequest} request.body.required - * The Point of Sale which should be updated * @security JWT - * @returns {PointOfSaleWithContainersResponse.model} 200 - The updated Point of Sale entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Product not found error - * @returns {string} 500 - Internal server error + * @return {PointOfSaleWithContainersResponse} 200 - The updated Point of Sale entity + * @return {string} 400 - Validation error + * @return {string} 404 - Product not found error + * @return {string} 500 - Internal server error */ public async updatePointOfSale(req: RequestWithToken, res: Response): Promise { const body = req.body as UpdatePointOfSaleRequest; @@ -254,16 +254,16 @@ export default class PointOfSaleController extends BaseController { } /** - * Returns the containers of the requested Point of Sale, empty list if POS does not exist - * @route GET /pointsofsale/{id}/containers + * GET /pointsofsale/{id}/containers + * @summary Returns the containers of the requested Point of Sale, empty list if POS does not exist * @operationId getAllPointOfSaleContainers - * @group pointofsale - Operations of the point of sale controller + * @tags pointofsale - Operations of the point of sale controller * @security JWT * @param {integer} id.path.required - The id of the point of sale * @param {integer} take.query - How many containers the endpoint should return * @param {integer} skip.query - How many containers should be skipped (for pagination) - * @returns {PaginatedContainerResponse.model} 200 - All containers of the requested Point of Sale - * @returns {string} 500 - Internal server error + * @return {PaginatedContainerResponse} 200 - All containers of the requested Point of Sale + * @return {string} 500 - Internal server error */ public async returnAllPointOfSaleContainers(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -284,16 +284,16 @@ export default class PointOfSaleController extends BaseController { } /** - * Returns the products of the requested Point of Sale, empty list if POS does not exist - * @route GET /pointsofsale/{id}/products + * GET /pointsofsale/{id}/products + * @summary Returns the products of the requested Point of Sale, empty list if POS does not exist * @operationId getAllPointOfSaleProducts - * @group pointofsale - Operations of the point of sale controller + * @tags pointofsale - Operations of the point of sale controller * @security JWT * @param {integer} id.path.required - The id of the point of sale * @param {integer} take.query - How many products the endpoint should return * @param {integer} skip.query - How many products should be skipped (for pagination) - * @returns {PaginatedProductResponse.model} 200 - All products of the requested Point of Sale - * @returns {string} 500 - Internal server error + * @return {PaginatedProductResponse} 200 - All products of the requested Point of Sale + * @return {string} 500 - Internal server error */ public async returnAllPointOfSaleProducts(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -314,19 +314,17 @@ export default class PointOfSaleController extends BaseController { } /** - * Returns a Point of Sale transactions - * @route GET /pointsofsale/{id}/transactions + * GET /pointsofsale/{id}/transactions + * @summary Returns a Point of Sale transactions * @operationId getTransactions - * @group pointofsale - Operations of the point of sale controller - * @param {integer} id.path.required - - * The id of the Point of Sale of which to get the transactions. + * @tags pointofsale - Operations of the point of sale controller + * @param {integer} id.path.required - The id of the Point of Sale of which to get the transactions. * @param {integer} take.query - How many transactions the endpoint should return * @param {integer} skip.query - How many transactions should be skipped (for pagination) * @security JWT - * @returns {PaginatedBaseTransactionResponse.model} 200 - - * The requested Point of Sale transactions - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {PaginatedBaseTransactionResponse} 200 - The requested Point of Sale transactions + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async returnPointOfSaleTransactions(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -368,7 +366,7 @@ export default class PointOfSaleController extends BaseController { * 'organ' if user is connected to POS via organ * 'own' if user is connected to POS * @param req - Request with CreatePointOfSaleRequest as body - * @returns whether POS is connected to user token + * @return whether POS is connected to user token */ static postRelation(req: RequestWithToken): string { const request = req.body as CreatePointOfSaleRequest; @@ -383,7 +381,7 @@ export default class PointOfSaleController extends BaseController { * 'organ' if user is connected to POS via organ * 'own' if user is connected to POS * @param req - * @returns whether POS is connected to used token + * @return whether POS is connected to used token */ static async getRelation(req: RequestWithToken): Promise { const pointOfSaleId = asNumber(req.params.id); diff --git a/src/controller/product-category-controller.ts b/src/controller/product-category-controller.ts index 79538b05d..7ffbca0a1 100644 --- a/src/controller/product-category-controller.ts +++ b/src/controller/product-category-controller.ts @@ -68,15 +68,15 @@ export default class ProductCategoryController extends BaseController { } /** - * Returns all existing productcategories - * @route GET /productcategories + * GET /productcategories + * @summary Returns all existing productcategories * @operationId getAllProductCategories - * @group productCategories - Operations of productcategory controller + * @tags productCategories - Operations of productcategory controller * @security JWT * @param {integer} take.query - How many product categories the endpoint should return * @param {integer} skip.query - How many product categories should be skipped (for pagination) - * @returns {PaginatedProductCategoryResponse.model} 200 - All existing productcategories - * @returns {string} 500 - Internal server error + * @return {PaginatedProductCategoryResponse} 200 - All existing productcategories + * @return {string} 500 - Internal server error */ public async returnAllProductCategories(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -105,16 +105,16 @@ export default class ProductCategoryController extends BaseController { } /** - * Post a new productCategory. - * @route POST /productcategories + * POST /productcategories + * @summary Post a new productCategory. * @operationId createProductCategory - * @group productCategories - Operations of productcategory controller - * @param {ProductCategoryRequest.model} productCategory.body.required + * @tags productCategories - Operations of productcategory controller + * @param {ProductCategoryRequest} request.body.required * - The productCategory which should be created * @security JWT - * @returns {ProductCategoryResponse.model} 200 - The created productcategory entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {ProductCategoryResponse} 200 - The created productcategory entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async postProductCategory(req: RequestWithToken, res: Response): Promise { const body = req.body as ProductCategoryRequest; @@ -132,15 +132,15 @@ export default class ProductCategoryController extends BaseController { } /** - * Returns the requested productcategory - * @route GET /productcategories/{id} + * GET /productcategories/{id} + * @summary Returns the requested productcategory * @operationId getSingleProductCategory - * @group productCategories - Operations of productcategory controller + * @tags productCategories - Operations of productcategory controller * @param {integer} id.path.required - The id of the productcategory which should be returned * @security JWT - * @returns {ProductCategoryResponse.model} 200 - The requested productcategory entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {ProductCategoryResponse} 200 - The requested productcategory entity + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async returnSingleProductCategory(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -164,18 +164,18 @@ export default class ProductCategoryController extends BaseController { } /** - * Update an existing productcategory. - * @route PATCH /productcategories/{id} + * PATCH /productcategories/{id} + * @summary Update an existing productcategory. * @operationId updateProductCategory - * @group productCategories - Operations of productcategory controller + * @tags productCategories - Operations of productcategory controller * @param {integer} id.path.required - The id of the productcategory which should be returned - * @param {ProductCategoryRequest.model} productCategory.body.required + * @param {ProductCategoryRequest} request.body.required * - The productcategory which should be created * @security JWT - * @returns {ProductCategoryResponse.model} 200 - The patched productcategory entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {ProductCategoryResponse} 200 - The patched productcategory entity + * @return {string} 400 - Validation error + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async updateProductCategory(req: RequestWithToken, res: Response): Promise { const body = req.body as ProductCategoryRequest; diff --git a/src/controller/product-controller.ts b/src/controller/product-controller.ts index 1e7d14f0e..8d442d0ab 100644 --- a/src/controller/product-controller.ts +++ b/src/controller/product-controller.ts @@ -88,15 +88,15 @@ export default class ProductController extends BaseController { } /** - * Returns all existing products - * @route GET /products + * GET /products + * @summary Returns all existing products * @operationId getAllProducts - * @group products - Operations of product controller + * @tags products - Operations of product controller * @security JWT * @param {integer} take.query - How many products the endpoint should return * @param {integer} skip.query - How many products should be skipped (for pagination) - * @returns {PaginatedProductResponse.model} 200 - All existing products - * @returns {string} 500 - Internal server error + * @return {PaginatedProductResponse} 200 - All existing products + * @return {string} 500 - Internal server error */ public async getAllProducts(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -124,15 +124,15 @@ export default class ProductController extends BaseController { } /** - * Create a new product. - * @route POST /products + * POST /products + * @summary Create a new product. * @operationId createProduct - * @group products - Operations of product controller - * @param {CreateProductRequest.model} product.body.required - The product which should be created + * @tags products - Operations of product controller + * @param {CreateProductRequest} request.body.required - The product which should be created * @security JWT - * @returns {ProductResponse.model} 200 - The created product entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {ProductResponse} 200 - The created product entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createProduct(req: RequestWithToken, res: Response): Promise { const body = req.body as CreateProductRequest; @@ -159,17 +159,17 @@ export default class ProductController extends BaseController { } /** - * Update an existing product. - * @route PATCH /products/{id} + * PATCH /products/{id} + * @summary Update an existing product. * @operationId updateProduct - * @group products - Operations of product controller + * @tags products - Operations of product controller * @param {integer} id.path.required - The id of the product which should be updated - * @param {UpdateProductRequest.model} product.body.required - The product which should be updated + * @param {UpdateProductRequest} request.body.required - The product which should be updated * @security JWT - * @returns {ProductResponse.model} 200 - The created product entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Product not found error - * @returns {string} 500 - Internal server error + * @return {ProductResponse} 200 - The created product entity + * @return {string} 400 - Validation error + * @return {string} 404 - Product not found error + * @return {string} 500 - Internal server error */ public async updateProduct(req: RequestWithToken, res: Response): Promise { const body = req.body as UpdateProductRequest; @@ -204,15 +204,15 @@ export default class ProductController extends BaseController { } /** - * Returns the requested product - * @route GET /products/{id} + * GET /products/{id} + * @summary Returns the requested product * @operationId getSingleProduct - * @group products - Operations of products controller + * @tags products - Operations of products controller * @param {integer} id.path.required - The id of the product which should be returned * @security JWT - * @returns {ProductResponse.model} 200 - The requested product entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {ProductResponse} 200 - The requested product entity + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async getSingleProduct(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -235,17 +235,17 @@ export default class ProductController extends BaseController { } /** - * Upload a new image for a product - * @route POST /products/{id}/image + * POST /products/{id}/image + * @summary Upload a new image for a product * @operationId updateProductImage - * @group products - Operations of products controller + * @tags products - Operations of products controller * @consumes multipart/form-data * @param {integer} id.path.required - The id of the product which should be returned - * @param {file} file.formData + * @param {FileRequest} request.body.required - product image - multipart/form-data * @security JWT - * @returns 204 - Success - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return 204 - Success + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async updateProductImage(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -296,7 +296,7 @@ export default class ProductController extends BaseController { * 'organ' if user is not connected to product via organ * 'own' if user is connected to product * @param req - Request with CreateProductRequest as body - * @returns whether product is connected to user token + * @return whether product is connected to user token */ static postRelation(req: RequestWithToken): string { const request = req.body as CreateProductRequest; @@ -310,7 +310,7 @@ export default class ProductController extends BaseController { * 'all' if user is not connected to product * 'own' if user is connected to product * @param req - Request with product id as param - * @returns whether product is connected to user token + * @return whether product is connected to user token */ static async getRelation(req: RequestWithToken): Promise { const productId = asNumber(req.params.id); diff --git a/src/controller/rbac-controller.ts b/src/controller/rbac-controller.ts index abce75e75..8b373b60b 100644 --- a/src/controller/rbac-controller.ts +++ b/src/controller/rbac-controller.ts @@ -48,13 +48,13 @@ export default class RbacController extends BaseController { } /** - * Returns all existing roles - * @route GET /rbac/roles + * GET /rbac/roles + * @summary Returns all existing roles * @operationId getAllRoles - * @group rbac - Operations of rbac controller + * @tags rbac - Operations of rbac controller * @security JWT - * @returns {Array.} 200 - All existing roles - * @returns {string} 500 - Internal server error + * @return {Array.} 200 - All existing roles + * @return {string} 500 - Internal server error */ public async returnAllRoles(req: Request, res: Response): Promise { const { body } = req; diff --git a/src/controller/request/accept-tos-request.ts b/src/controller/request/accept-tos-request.ts index dc04df52d..5002b80f7 100644 --- a/src/controller/request/accept-tos-request.ts +++ b/src/controller/request/accept-tos-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AcceptTosRequest + * @typedef {object} AcceptTosRequest * @property {boolean} extensiveDataProcessing.required - Whether data about this * user can be used (non-anonymously) for more data science! */ diff --git a/src/controller/request/authentication-ean-request.ts b/src/controller/request/authentication-ean-request.ts index 6016cb796..4ea51a490 100644 --- a/src/controller/request/authentication-ean-request.ts +++ b/src/controller/request/authentication-ean-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationEanRequest + * @typedef {object} AuthenticationEanRequest * @property {string} eanCode.required */ export default interface AuthenticationEanRequest { diff --git a/src/controller/request/authentication-key-request.ts b/src/controller/request/authentication-key-request.ts index 35eb6a997..e74153a68 100644 --- a/src/controller/request/authentication-key-request.ts +++ b/src/controller/request/authentication-key-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationKeyRequest + * @typedef {object} AuthenticationKeyRequest * @property {number} userId.required * @property {string} key.required - The password * diff --git a/src/controller/request/authentication-ldap-request.ts b/src/controller/request/authentication-ldap-request.ts index b5adeb0e4..861c2976f 100644 --- a/src/controller/request/authentication-ldap-request.ts +++ b/src/controller/request/authentication-ldap-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationLDAPRequest + * @typedef {object} AuthenticationLDAPRequest * @property {string} accountName.required - The AD account name to authenticate * @property {string} password.required - The password */ diff --git a/src/controller/request/authentication-local-request.ts b/src/controller/request/authentication-local-request.ts index 561485a12..2cc86efd7 100644 --- a/src/controller/request/authentication-local-request.ts +++ b/src/controller/request/authentication-local-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationLocalRequest + * @typedef {object} AuthenticationLocalRequest * @property {string} accountMail.required - The users mail to authenticate * @property {string} password.required - The password */ diff --git a/src/controller/request/authentication-mock-request.ts b/src/controller/request/authentication-mock-request.ts index 310647081..88a3b228d 100644 --- a/src/controller/request/authentication-mock-request.ts +++ b/src/controller/request/authentication-mock-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationMockRequest + * @typedef {object} AuthenticationMockRequest * @property {number} userId.required * @property {string} nonce.required */ diff --git a/src/controller/request/authentication-nfc-request.ts b/src/controller/request/authentication-nfc-request.ts index bb0c32c0f..f0f5686fd 100644 --- a/src/controller/request/authentication-nfc-request.ts +++ b/src/controller/request/authentication-nfc-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationNfcRequest + * @typedef {object} AuthenticationNfcRequest * @property {string} nfcCode.required */ export default interface AuthenticationNfcRequest { diff --git a/src/controller/request/authentication-pin-request.ts b/src/controller/request/authentication-pin-request.ts index e260f1518..c1c61191b 100644 --- a/src/controller/request/authentication-pin-request.ts +++ b/src/controller/request/authentication-pin-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationPinRequest + * @typedef {object} AuthenticationPinRequest * @property {number} userId.required * @property {string} pin.required */ diff --git a/src/controller/request/authentication-reset-token-request.ts b/src/controller/request/authentication-reset-token-request.ts index c7b80a71b..5ea3e7b7f 100644 --- a/src/controller/request/authentication-reset-token-request.ts +++ b/src/controller/request/authentication-reset-token-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef AuthenticationResetTokenRequest + * @typedef {object} AuthenticationResetTokenRequest * @property {string} accountMail.required - The mail of the user * @property {string} token.required - The reset token passcode * @property {string} password.required - The new password to set diff --git a/src/controller/request/banner-request.ts b/src/controller/request/banner-request.ts index 7f17bd45a..71f35e222 100644 --- a/src/controller/request/banner-request.ts +++ b/src/controller/request/banner-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef BannerRequest + * @typedef {object} BannerRequest * @property {string} name - Name/label of the banner * @property {number} duration - How long the banner should be shown (in seconds) * @property {boolean} active - Whether the banner is active. Overrides start and end date diff --git a/src/controller/request/container-request.ts b/src/controller/request/container-request.ts index ed42a4be7..e30c09b1c 100644 --- a/src/controller/request/container-request.ts +++ b/src/controller/request/container-request.ts @@ -37,9 +37,9 @@ export type ContainerParams = UpdateContainerParams | CreateContainerParams; // strict type checker later down the line. /** - * @typedef CreateContainerRequest + * @typedef {object} CreateContainerRequest * @property {string} name.required - Name of the container - * @property {Array.} products.required - + * @property {Array} products.required - * IDs or requests of the products to add to the container * @property {boolean} public.required - Whether the container is public or not * @property {integer} ownerId - Id of the user who will own the container, if undefined it will @@ -50,9 +50,9 @@ export interface CreateContainerRequest extends BaseContainerParams { } /** - * @typedef UpdateContainerRequest + * @typedef {object} UpdateContainerRequest * @property {string} name.required - Name of the container - * @property {Array.} products.required - + * @property {Array} products.required - * IDs or requests of the products to add to the container * @property {boolean} public.required - Whether the container is public or not */ diff --git a/src/controller/request/debtor-request.ts b/src/controller/request/debtor-request.ts index f6b1b347b..133256bd3 100644 --- a/src/controller/request/debtor-request.ts +++ b/src/controller/request/debtor-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef HandoutFinesRequest + * @typedef {object} HandoutFinesRequest * @property {Array} userIds.required - Users to fine. If a user is not eligible for a fine, a fine of 0,00 will be handed out. * @property {string} referenceDate.required - Reference date to calculate the balance and thus the height of the fine for. */ diff --git a/src/controller/request/dinero-request.ts b/src/controller/request/dinero-request.ts index e58d98a99..e94555b8c 100644 --- a/src/controller/request/dinero-request.ts +++ b/src/controller/request/dinero-request.ts @@ -17,14 +17,14 @@ */ /** - * @typedef DineroObject + * @typedef {object} DineroObject * @property {integer} amount.required - amount * @property {string} currency.required - currency * @property {integer} precision.required - precision */ /** -* @typedef DineroObjectRequest +* @typedef {object} DineroObjectRequest * @property {integer} amount.required - amount * @property {string} currency.required - currency * @property {integer} precision.required - precision diff --git a/src/controller/request/event-request.ts b/src/controller/request/event-request.ts index 7dc861292..cfa0cbcc5 100644 --- a/src/controller/request/event-request.ts +++ b/src/controller/request/event-request.ts @@ -18,7 +18,7 @@ import { Availability } from '../../entity/event/event-shift-answer'; /** - * @typedef CreateEventRequest + * @typedef {object} CreateEventRequest * @property {string} name.required - Name of the event. * @property {string} startDate.required - The starting date of the event. * @property {string} endDate.required - The end date of the event. @@ -28,7 +28,7 @@ import { Availability } from '../../entity/event/event-shift-answer'; */ /** - * @typedef UpdateEventRequest + * @typedef {object} UpdateEventRequest * @property {string} name - Name of the event. * @property {string} startDate - The starting date of the event. * @property {string} endDate - The end date of the event. @@ -45,13 +45,13 @@ export interface EventRequest { } /** - * @typedef CreateShiftRequest + * @typedef {object} CreateShiftRequest * @property {string} name.required - Name of the event * @property {Array} roles.required - Roles that (can) have this shift */ /** - * @typedef UpdateShiftRequest + * @typedef {object} UpdateShiftRequest * @property {string} name - Name of the event * @property {Array} roles - Roles that (can) have this shift */ @@ -61,7 +61,7 @@ export interface EventShiftRequest { } /** - * @typedef EventAnswerAssignmentRequest + * @typedef {object} EventAnswerAssignmentRequest * @property {boolean} selected.required - Whether this user is selected for the given shift at the given event */ export interface EventAnswerAssignmentRequest { @@ -69,7 +69,7 @@ export interface EventAnswerAssignmentRequest { } /** - * @typedef EventAnswerAvailabilityRequest + * @typedef {object} EventAnswerAvailabilityRequest * @property {string} availability.required - New availability of the given user for the given event (YES, NO, LATER, NA) */ export interface EventAnswerAvailabilityRequest { diff --git a/src/controller/request/file-request.ts b/src/controller/request/file-request.ts new file mode 100644 index 000000000..5954a6f1f --- /dev/null +++ b/src/controller/request/file-request.ts @@ -0,0 +1,22 @@ +/** + * SudoSOS back-end API service. + * Copyright (C) 2020 Study association GEWIS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +// TODO This is most likely not the way to go, but im not sure how to deal with file uploading in express-jsdoc-swagger. +/** + * @typedef {object} FileRequest + * @property {string} file - file - binary + */ diff --git a/src/controller/request/invoice-entry-request.ts b/src/controller/request/invoice-entry-request.ts index 596a8cf92..f338f91bb 100644 --- a/src/controller/request/invoice-entry-request.ts +++ b/src/controller/request/invoice-entry-request.ts @@ -18,10 +18,10 @@ import { DineroObjectRequest } from './dinero-request'; /** - * @typedef InvoiceEntryRequest + * @typedef {object} InvoiceEntryRequest * @property {string} description.required - The description of the entry * @property {integer} amount.required - Amount of item sold. - * @property {DineroObjectRequest.model} priceInclVat.required - The price per item. + * @property {DineroObjectRequest} priceInclVat.required - The price per item. * @property {number} vatPercentage.required - The percentage of VAT applied to this item */ export default interface InvoiceEntryRequest { diff --git a/src/controller/request/invoice-request.ts b/src/controller/request/invoice-request.ts index 3d64947d2..5af004eba 100644 --- a/src/controller/request/invoice-request.ts +++ b/src/controller/request/invoice-request.ts @@ -31,12 +31,11 @@ export interface UpdateInvoiceParams extends BaseUpdateInvoice { } /** - * @typedef UpdateInvoiceRequest + * @typedef{object} UpdateInvoiceRequest * @property {integer} byId - The user who updates the Invoice, defaults to the ID of the requester. * @property {string} addressee.required - Name of the addressed. * @property {string} description.required - The description of the invoice. - * @property {string} state - The state to set of the invoice, - * can be either CREATED, SENT, PAID or DELETED. + * @property {string} state - enum:CREATED,SENT,PAID,DELETED - The state to set of the invoice, */ export interface UpdateInvoiceRequest extends BaseUpdateInvoice { byId?: number, @@ -58,13 +57,13 @@ export interface CreateInvoiceParams extends BaseInvoice { } /** - * @typedef CreateInvoiceRequest + * @typedef {object} CreateInvoiceRequest * @property {integer} forId.required - The recipient of the Invoice. * @property {integer} byId - The creator of the Invoice, defaults to the ID of the requester. * @property {string} addressee.required - Name of the addressed. * @property {string} description.required - The description of the invoice. - * @property {Array.} customEntries - Custom entries to be added to the invoice - * @property {Array.} transactionIDs - IDs of the transactions to add to the Invoice. + * @property {Array} customEntries - Custom entries to be added to the invoice + * @property {Array} transactionIDs - IDs of the transactions to add to the Invoice. * @property {string} fromDate - For creating an Invoice for all transactions from a specific date. * @property {boolean} isCreditInvoice.required - If the invoice is an credit Invoice * If an invoice is a credit invoice the relevant subtransactions are defined as all the sub transactions which have `subTransaction.toId == forId`. diff --git a/src/controller/request/payout-request-request.ts b/src/controller/request/payout-request-request.ts index 21de26228..5dc9df788 100644 --- a/src/controller/request/payout-request-request.ts +++ b/src/controller/request/payout-request-request.ts @@ -18,8 +18,8 @@ import { DineroObjectRequest } from './dinero-request'; /** - * @typedef PayoutRequestRequest - * @property {DineroObjectRequest.model} amount.required - The requested amount to be paid out + * @typedef {object} PayoutRequestRequest + * @property {DineroObjectRequest} amount.required - The requested amount to be paid out * @property {string} bankAccountNumber.required - The bank account number to transfer the money to * @property {string} bankAccountName.required - The name of the owner of the bank account */ diff --git a/src/controller/request/payout-request-status-request.ts b/src/controller/request/payout-request-status-request.ts index e4f6cfa2e..e08d81728 100644 --- a/src/controller/request/payout-request-status-request.ts +++ b/src/controller/request/payout-request-status-request.ts @@ -18,9 +18,8 @@ import { PayoutRequestState } from '../../entity/transactions/payout-request-status'; /** - * @typedef PayoutRequestStatusRequest - * @property {string} state - PayoutRequestState to change to, - * should be one of CREATED, APPROVED, DENIED, CANCELLED + * @typedef {object} PayoutRequestStatusRequest + * @property {string} state - enum:CREATED,APPROVED,DENIED,CANCELLED - PayoutRequestState to change to. */ export interface PayoutRequestStatusRequest { state: PayoutRequestState; diff --git a/src/controller/request/point-of-sale-request.ts b/src/controller/request/point-of-sale-request.ts index 6e9689223..aaad9c02e 100644 --- a/src/controller/request/point-of-sale-request.ts +++ b/src/controller/request/point-of-sale-request.ts @@ -31,11 +31,11 @@ export interface UpdatePointOfSaleParams extends BasePointOfSaleParams { } /** - * @typedef CreatePointOfSaleRequest + * @typedef {object} CreatePointOfSaleRequest * @property {string} name.required - Name of the POS * @property {boolean} useAuthentication.required - Whether this POS requires users to * authenticate themselves before making a transaction - * @property {Array.} containers - + * @property {Array} containers - * IDs or Requests of the containers to add to the POS * @property {integer} ownerId - ID of the user who will own the POS, if undefined it will * default to the token ID. @@ -45,11 +45,11 @@ export interface CreatePointOfSaleRequest extends BasePointOfSaleParams { } /** - * @typedef UpdatePointOfSaleRequest + * @typedef {object} UpdatePointOfSaleRequest * @property {string} name.required - Name of the POS * @property {boolean} useAuthentication.required - Whether this POS requires users to * authenticate themselves before making a transaction - * @property {Array.} containers - + * @property {Array} containers - * IDs or Requests of the containers to add to the POS * @property {integer} id.required - ID of the POS to update. */ diff --git a/src/controller/request/product-category-request.ts b/src/controller/request/product-category-request.ts index 61a876ea2..f1c8e4659 100644 --- a/src/controller/request/product-category-request.ts +++ b/src/controller/request/product-category-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef ProductCategoryRequest + * @typedef {object} ProductCategoryRequest * @property {string} name - Name/label of the productCategory */ export default interface ProductCategoryRequest { diff --git a/src/controller/request/product-request.ts b/src/controller/request/product-request.ts index 9861fc793..bf41ce560 100644 --- a/src/controller/request/product-request.ts +++ b/src/controller/request/product-request.ts @@ -37,9 +37,9 @@ export interface UpdateProductParams extends BaseProductParams { export type ProductRequest = UpdateProductParams | CreateProductParams; /** - * @typedef CreateProductRequest + * @typedef {object} CreateProductRequest * @property {string} name.required - Name of the product - * @property {DineroObjectRequest.model} priceInclVat.required - Price of the product + * @property {DineroObjectRequest} priceInclVat.required - Price of the product * @property {number} vat.required - VAT group ID of the product * @property {number} category.required - Category of the product * @property {number} alcoholPercentage.required - Alcohol percentage of the product in 2 decimals @@ -50,9 +50,9 @@ export interface CreateProductRequest extends BaseProductParams { } /** - * @typedef UpdateProductRequest + * @typedef {object} UpdateProductRequest * @property {string} name.required - Name of the product - * @property {DineroObjectRequest.model} priceInclVat.required - Price of the product + * @property {DineroObjectRequest} priceInclVat.required - Price of the product * @property {number} vat.required - VAT group ID of the product * @property {number} category.required - Category of the product * @property {number} alcoholPercentage.required - Alcohol percentage of the product in 2 decimals diff --git a/src/controller/request/reset-local-request.ts b/src/controller/request/reset-local-request.ts index eb791f8d2..35b5f375d 100644 --- a/src/controller/request/reset-local-request.ts +++ b/src/controller/request/reset-local-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef ResetLocalRequest + * @typedef {object} ResetLocalRequest * @property {string} accountMail.required - The mail of the user */ export default interface ResetLocalRequest { diff --git a/src/controller/request/revision-request.ts b/src/controller/request/revision-request.ts index e194071ad..02f090445 100644 --- a/src/controller/request/revision-request.ts +++ b/src/controller/request/revision-request.ts @@ -17,9 +17,9 @@ */ /** - * @typedef RevisionRequest - * @property {integer} id - revision id - * @property {integer} revision - revision number + * @typedef {object} RevisionRequest + * @property {integer} id.required - revision id + * @property {integer} revision.required - revision number */ export default interface RevisionRequest { id: number, diff --git a/src/controller/request/simple-file-request.ts b/src/controller/request/simple-file-request.ts index 2ddf921ac..da4fafcd9 100644 --- a/src/controller/request/simple-file-request.ts +++ b/src/controller/request/simple-file-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef SimpleFileRequest + * @typedef {object} SimpleFileRequest * @property {string} name - Name of the file */ export default interface SimpleFileRequest { diff --git a/src/controller/request/stripe-request.ts b/src/controller/request/stripe-request.ts index 6e654e194..3549445e8 100644 --- a/src/controller/request/stripe-request.ts +++ b/src/controller/request/stripe-request.ts @@ -18,8 +18,8 @@ import { DineroObjectRequest } from './dinero-request'; /** - * @typedef StripeRequest - * @property {Dinero.model} amount - Amount of money being deposited + * @typedef {object} StripeRequest + * @property {DineroObjectRequest} amount - Amount of money being deposited */ export interface StripeRequest { amount: DineroObjectRequest; diff --git a/src/controller/request/transaction-request.ts b/src/controller/request/transaction-request.ts index 8dfa04141..9fa386667 100644 --- a/src/controller/request/transaction-request.ts +++ b/src/controller/request/transaction-request.ts @@ -20,12 +20,12 @@ import { DineroObjectRequest } from './dinero-request'; import RevisionRequest from './revision-request'; /** - * @typedef TransactionRequest + * @typedef {object} TransactionRequest * @property {integer} from.required - from user id * @property {integer} createdBy - createdBy user id - * @property {Array.} subTransactions.required - subtransactions - * @property {RevisionRequest.model} pointOfSale.required - point of sale - * @property {DineroObjectRequest.model} totalPriceInclVat.required - total price of the transaction + * @property {Array} subTransactions.required - subtransactions + * @property {RevisionRequest} pointOfSale.required - point of sale + * @property {DineroObjectRequest} totalPriceInclVat.required - total price of the transaction */ export interface TransactionRequest { from: number, @@ -36,11 +36,11 @@ export interface TransactionRequest { } /** - * @typedef SubTransactionRequest + * @typedef {object} SubTransactionRequest * @property {integer} to.required - to user id - * @property {RevisionRequest.model} container.required - container - * @property {Array.} subTransactionRows.required - subtransaction rows - * @property {DineroObjectRequest.model} totalPriceInclVat.required - total price + * @property {RevisionRequest} container.required - container + * @property {Array} subTransactionRows.required - subtransaction rows + * @property {DineroObjectRequest} totalPriceInclVat.required - total price * of the subtransaction */ export interface SubTransactionRequest { @@ -51,10 +51,10 @@ export interface SubTransactionRequest { } /** - * @typedef SubTransactionRowRequest - * @property {RevisionRequest.model} product - product + * @typedef {object} SubTransactionRowRequest + * @property {RevisionRequest} product - product * @property {integer} amount - amount of this product in subtransaction - * @property {DineroObjectRequest.model} totalPriceInclVat.required - total price + * @property {DineroObjectRequest} totalPriceInclVat.required - total price * of the subtransaction row */ export interface SubTransactionRowRequest { diff --git a/src/controller/request/transfer-request.ts b/src/controller/request/transfer-request.ts index 958b3a48a..405af2eba 100644 --- a/src/controller/request/transfer-request.ts +++ b/src/controller/request/transfer-request.ts @@ -19,9 +19,9 @@ import { DineroObjectRequest } from './dinero-request'; /** - * @typedef TransferRequest + * @typedef {object} TransferRequest * @property {string} description - Description of the transfer - * @property {DineroObjectRequest.model} amount - Amount of money being transferred + * @property {DineroObjectRequest} amount - Amount of money being transferred * @property {integer} type - Type of transfer * @property {integer} fromId - from which user the money is being transferred * @property {integer} toId - to which user the money is being transferred. diff --git a/src/controller/request/update-local-request.ts b/src/controller/request/update-local-request.ts index 5072ec475..3719089ac 100644 --- a/src/controller/request/update-local-request.ts +++ b/src/controller/request/update-local-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef UpdateLocalRequest + * @typedef {object} UpdateLocalRequest * @property {string} password.required - The password to set */ export default interface UpdateLocalRequest { diff --git a/src/controller/request/update-nfc-request.ts b/src/controller/request/update-nfc-request.ts index 5612ece8d..4ab8fbb66 100644 --- a/src/controller/request/update-nfc-request.ts +++ b/src/controller/request/update-nfc-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef UpdateNfcRequest + * @typedef {object} UpdateNfcRequest * @property {string} nfcCode.required - The NFC code to set */ export default interface UpdateNfcRequest { diff --git a/src/controller/request/update-pin-request.ts b/src/controller/request/update-pin-request.ts index 5b1dae67e..cc9faaa00 100644 --- a/src/controller/request/update-pin-request.ts +++ b/src/controller/request/update-pin-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef UpdatePinRequest + * @typedef {object} UpdatePinRequest * @property {string} pin.required - The PIN code to set */ export default interface UpdatePinRequest { diff --git a/src/controller/request/user-request.ts b/src/controller/request/user-request.ts index 4af4b6dd1..90c81f7ed 100644 --- a/src/controller/request/user-request.ts +++ b/src/controller/request/user-request.ts @@ -28,7 +28,7 @@ export default interface BaseUserRequest { } /** - * @typedef CreateUserRequest + * @typedef {object} CreateUserRequest * @property {string} firstName.required * @property {string} lastName * @property {string} nickname @@ -42,7 +42,7 @@ export interface CreateUserRequest extends BaseUserRequest { } /** - * @typedef UpdateUserRequest + * @typedef {object} UpdateUserRequest * @property {string} firstName * @property {string} lastName * @property {string} nickname diff --git a/src/controller/request/vat-group-request.ts b/src/controller/request/vat-group-request.ts index fbb83399b..3c80ef5a3 100644 --- a/src/controller/request/vat-group-request.ts +++ b/src/controller/request/vat-group-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef UpdateVatGroupRequest + * @typedef {object} UpdateVatGroupRequest * @property {string} name.required - Name of the VAT group * @property {boolean} deleted.required - Whether this group should be hidden * in the financial overviews when its value is zero @@ -31,7 +31,7 @@ export interface UpdateVatGroupRequest { } /** - * @typedef {UpdateVatGroupRequest} VatGroupRequest + * @typedef {allOf|UpdateVatGroupRequest} VatGroupRequest * @property {number} percentage.required - VAT percentage */ export interface VatGroupRequest extends UpdateVatGroupRequest { diff --git a/src/controller/request/voucher-group-request.ts b/src/controller/request/voucher-group-request.ts index 59f3f4cd4..540ffafa1 100644 --- a/src/controller/request/voucher-group-request.ts +++ b/src/controller/request/voucher-group-request.ts @@ -20,11 +20,11 @@ import DineroFactory from 'dinero.js'; import { DineroObjectRequest } from './dinero-request'; /** - * @typedef VoucherGroupRequest + * @typedef {object} VoucherGroupRequest * @property {string} name.required - Name of the group * @property {string} activeStartDate.required - Date from which the included cards are active * @property {string} activeEndDate.required - Date from which cards are no longer active - * @property {DineroObjectRequest.model} balance.required - Start balance to be assigned + * @property {DineroObjectRequest} balance.required - Start balance to be assigned * to the voucher users * @property {number} amount.required - Amount of users to be assigned to the voucher group */ diff --git a/src/controller/response/authentication-response.ts b/src/controller/response/authentication-response.ts index e6044a75b..70469feb5 100644 --- a/src/controller/response/authentication-response.ts +++ b/src/controller/response/authentication-response.ts @@ -20,10 +20,10 @@ import { UserResponse } from './user-response'; import { TermsOfServiceStatus } from '../../entity/user/user'; /** - * @typedef AuthenticationResponse - * @property {UserResponse.model} user.required - The user that has authenticated. - * @property {Array.} roles.required - The RBAC roles that the user has. - * @property {Array.} organs.required - The organs that the user is a member of. + * @typedef {object} AuthenticationResponse + * @property {UserResponse} user.required - The user that has authenticated. + * @property {Array} roles.required - The RBAC roles that the user has. + * @property {Array} organs.required - The organs that the user is a member of. * @property {string} token.required - The JWT token that can be used as Bearer token for authentication. * @property {string} acceptedToS.required - Whether the related user has accepted the Terms of Service * or is not required to. diff --git a/src/controller/response/balance-response.ts b/src/controller/response/balance-response.ts index f15861e28..6ad1aa325 100644 --- a/src/controller/response/balance-response.ts +++ b/src/controller/response/balance-response.ts @@ -19,11 +19,11 @@ import { DineroObjectResponse } from './dinero-response'; import { PaginationResult } from '../../helpers/pagination'; /** - * @typedef BalanceResponse + * @typedef {object} BalanceResponse * @property {number} id.required - ID of the user this balance belongs to * @property {string} date.required - Date at which this user had this balance - * @property {DineroObjectResponse.model} amount.required - The amount of balance this user has - * @property {DineroObjectResponse.model} fine - The amount of fines this user has at the current point in time, + * @property {DineroObjectResponse} amount.required - The amount of balance this user has + * @property {DineroObjectResponse} fine - The amount of fines this user has at the current point in time, * aka "now" (if any). Should be ignored if date is not now. * @property {string} fineSince - Timestamp of the first fine * @property {number} lastTransactionId - The ID of the last transaction that was @@ -42,8 +42,8 @@ export default interface BalanceResponse { } /** - * @typedef PaginatedBalanceResponse - * @property {PaginationResult.model} _pagination - Pagination metadata + * @typedef {object} PaginatedBalanceResponse + * @property {PaginationResult} _pagination - Pagination metadata * @property {Array} records - Returned balance responses */ export interface PaginatedBalanceResponse { diff --git a/src/controller/response/banner-response.ts b/src/controller/response/banner-response.ts index 0127e07cb..aaa8a13c2 100644 --- a/src/controller/response/banner-response.ts +++ b/src/controller/response/banner-response.ts @@ -20,7 +20,7 @@ import BaseResponse from './base-response'; import { PaginationResult } from '../../helpers/pagination'; /** - * @typedef {BaseResponse} BannerResponse + * @typedef {allOf|BaseResponse} BannerResponse * @property {string} name.required - Name/label of the banner * @property {string} image - Location of the image * @property {number} duration.required - How long the banner should be shown (in seconds) @@ -38,9 +38,9 @@ export interface BannerResponse extends BaseResponse { } /** - * @typedef PaginatedBannerResponse - * @property {PaginationResult.model} _pagination - Pagination metadata - * @property {Array.} records - Returned banners + * @typedef {object} PaginatedBannerResponse + * @property {PaginationResult} _pagination - Pagination metadata + * @property {Array} records - Returned banners */ export interface PaginatedBannerResponse { _pagination: PaginationResult, diff --git a/src/controller/response/base-response.ts b/src/controller/response/base-response.ts index c5fda2034..9c074858a 100644 --- a/src/controller/response/base-response.ts +++ b/src/controller/response/base-response.ts @@ -17,7 +17,7 @@ */ /** - * @typedef BaseResponse + * @typedef {object} BaseResponse * @property {integer} id.required - The unique id of the entity. * @property {string} createdAt - The creation Date of the entity. * @property {string} updatedAt - The last update Date of the entity. diff --git a/src/controller/response/container-response.ts b/src/controller/response/container-response.ts index 4dce3c8a7..cd9bad9d9 100644 --- a/src/controller/response/container-response.ts +++ b/src/controller/response/container-response.ts @@ -21,29 +21,29 @@ import { BaseUserResponse } from './user-response'; import { PaginationResult } from '../../helpers/pagination'; /** - * @typedef {BaseResponse} BaseContainerResponse + * @typedef {allOf|BaseResponse} BaseContainerResponse * @property {string} name.required - The name of the container. - * @property {boolean} public.required - Public status of the container. + * @property {boolean} public - Public status of the container. * @property {integer} revision - The container revision. */ export interface BaseContainerResponse extends BaseResponse { name: string, - public: boolean, + public?: boolean, revision?: number, } /** - * @typedef {BaseContainerResponse} ContainerResponse - * @property {BaseUserResponse.model} owner.required - The owner of the container. + * @typedef {allOf|BaseContainerResponse} ContainerResponse + * @property {BaseUserResponse} owner.required - The owner of the container. */ export interface ContainerResponse extends BaseContainerResponse { owner: BaseUserResponse, } /** - * @typedef PaginatedContainerResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned containers + * @typedef {object} PaginatedContainerResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned containers */ export interface PaginatedContainerResponse { _pagination: PaginationResult, @@ -51,9 +51,9 @@ export interface PaginatedContainerResponse { } /** - * @typedef PaginatedContainerWithProductResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned containers + * @typedef {object} PaginatedContainerWithProductResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned containers */ export interface PaginatedContainerWithProductResponse { _pagination: PaginationResult, @@ -61,8 +61,8 @@ export interface PaginatedContainerWithProductResponse { } /** - * @typedef {ContainerResponse} ContainerWithProductsResponse - * @property {Array.} products.required - The products in the container. + * @typedef {allOf|ContainerResponse} ContainerWithProductsResponse + * @property {Array} products.required - The products in the container. */ export interface ContainerWithProductsResponse extends ContainerResponse { products: ProductResponse[], diff --git a/src/controller/response/debtor-response.ts b/src/controller/response/debtor-response.ts index 4b6fe8555..d5bae7e8e 100644 --- a/src/controller/response/debtor-response.ts +++ b/src/controller/response/debtor-response.ts @@ -22,10 +22,10 @@ import { BaseUserResponse } from './user-response'; import BalanceResponse from './balance-response'; /** - * @typedef UserToFineResponse + * @typedef {object} UserToFineResponse * @property {integer} id.required - User ID - * @property {DineroObjectResponse.model} fineAmount.required - Amount to fine - * @property {Array.} balances.required - Balances at the given reference dates + * @property {DineroObjectResponse} fineAmount.required - Amount to fine + * @property {Array} balances.required - Balances at the given reference dates */ export interface UserToFineResponse { id: number; @@ -34,9 +34,9 @@ export interface UserToFineResponse { } /** - * @typedef {BaseResponse} FineResponse - * @property {DineroObjectResponse.model} amount.required - Fine amount - * @property {BaseUserResponse.model} user.required - User that got the fine + * @typedef {allOf|BaseResponse} FineResponse + * @property {DineroObjectResponse} amount.required - Fine amount + * @property {BaseUserResponse} user.required - User that got the fine */ export interface FineResponse extends BaseResponse { amount: DineroObjectResponse; @@ -44,9 +44,9 @@ export interface FineResponse extends BaseResponse { } /** - * @typedef {BaseResponse} BaseFineHandoutEventResponse + * @typedef {allOf|BaseResponse} BaseFineHandoutEventResponse * @property {string} referenceDate.required - Reference date of fines - * @property {BaseUserResponse.model} createdBy.required - User that handed out the fines + * @property {BaseUserResponse} createdBy.required - User that handed out the fines */ export interface BaseFineHandoutEventResponse extends BaseResponse { referenceDate: string; @@ -54,17 +54,17 @@ export interface BaseFineHandoutEventResponse extends BaseResponse { } /** - * @typedef {BaseFineHandoutEventResponse} FineHandoutEventResponse - * @property {Array.} fines.required - Fines that have been handed out + * @typedef {allOf|BaseFineHandoutEventResponse} FineHandoutEventResponse + * @property {Array} fines.required - Fines that have been handed out */ export interface FineHandoutEventResponse extends BaseFineHandoutEventResponse { fines: FineResponse[]; } /** - * @typedef PaginatedFineHandoutEventResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned fine handout events + * @typedef {object} PaginatedFineHandoutEventResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned fine handout events */ export interface PaginatedFineHandoutEventResponse { _pagination: PaginationResult, @@ -72,8 +72,8 @@ export interface PaginatedFineHandoutEventResponse { } /** - * @typedef UserFineGroupResponse - * @property {Array.} fines.required - Fines that have been handed out + * @typedef {object} UserFineGroupResponse + * @property {Array} fines.required - Fines that have been handed out */ export interface UserFineGroupResponse { fines: FineResponse[]; diff --git a/src/controller/response/dinero-response.ts b/src/controller/response/dinero-response.ts index 48c7096d2..2c4d536c1 100644 --- a/src/controller/response/dinero-response.ts +++ b/src/controller/response/dinero-response.ts @@ -17,7 +17,7 @@ */ /** -* @typedef DineroObjectResponse +* @typedef {object} DineroObjectResponse * @property {integer} amount.required - amount * @property {string} currency.required - currency * @property {integer} precision.required - precision diff --git a/src/controller/response/dinero.ts b/src/controller/response/dinero.ts index d526af574..559ce207c 100644 --- a/src/controller/response/dinero.ts +++ b/src/controller/response/dinero.ts @@ -17,7 +17,7 @@ */ /** - * @typedef Dinero + * @typedef {object} Dinero * @property {integer} amount.required - The amount of money as integer in the given precision. * @property {integer} precision.required - The precision of the amount, in decimal places. * @property {string} currency.required - The ISO 4217 currency code. diff --git a/src/controller/response/event-response.ts b/src/controller/response/event-response.ts index 4f346781e..9847aa62c 100644 --- a/src/controller/response/event-response.ts +++ b/src/controller/response/event-response.ts @@ -21,9 +21,9 @@ import { BaseUserResponse } from './user-response'; import { EventType } from '../../entity/event/event'; /** - * @typedef {BaseResponse} BaseEventResponse + * @typedef {allOf|BaseResponse} BaseEventResponse * @property {string} name.required - Name of the borrel. - * @property {BaseUserResponse.model} createdBy.required - Creator of the event. + * @property {BaseUserResponse} createdBy.required - Creator of the event. * @property {string} startDate.required - The starting date of the event. * @property {string} endDate.required - The end date of the event. * @property {string} type.required - The tpye of event. @@ -37,7 +37,7 @@ export interface BaseEventResponse extends BaseResponse { } /** - * @typedef {BaseResponse} BaseEventShiftResponse + * @typedef {allOf|BaseResponse} BaseEventShiftResponse * @property {string} name.required - Name of the shift. */ export interface BaseEventShiftResponse extends BaseResponse { @@ -45,7 +45,7 @@ export interface BaseEventShiftResponse extends BaseResponse { } /** - * @typedef {BaseEventShiftResponse} EventShiftResponse + * @typedef {allOf|BaseEventShiftResponse} EventShiftResponse * @property {Array} roles.required - Which roles can fill in this shift. */ export interface EventShiftResponse extends BaseEventShiftResponse { @@ -53,7 +53,7 @@ export interface EventShiftResponse extends BaseEventShiftResponse { } /** - * @typedef {EventShiftResponse} EventInShiftResponse + * @typedef {allOf|EventShiftResponse} EventInShiftResponse * @property {Array} answers - Answers for this shift. */ export interface EventInShiftResponse extends EventShiftResponse { @@ -61,8 +61,8 @@ export interface EventInShiftResponse extends EventShiftResponse { } /** - * @typedef PaginatedEventShiftResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata + * @typedef {object} PaginatedEventShiftResponse + * @property {PaginationResult} _pagination.required - Pagination metadata * @property {Array} records.required - Returned event shifts */ export interface PaginatedEventShiftResponse { @@ -71,7 +71,7 @@ export interface PaginatedEventShiftResponse { } /** - * @typedef {BaseEventResponse} EventResponse + * @typedef {allOf|BaseEventResponse} EventResponse * @property {Array} shifts.required - Shifts for this event */ export interface EventResponse extends BaseEventResponse { @@ -79,8 +79,8 @@ export interface EventResponse extends BaseEventResponse { } /** - * @typedef BaseEventAnswerResponse - * @property {BaseUserResponse.model} user.required - Participant that filled in their availability + * @typedef {object} BaseEventAnswerResponse + * @property {BaseUserResponse} user.required - Participant that filled in their availability * @property {string} availability - Filled in availability per slot. * @property {boolean} selected.required - Whether this user is selected for the shift in the event */ @@ -91,8 +91,8 @@ export interface BaseEventAnswerResponse { } /** - * @typedef PaginatedBaseEventResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata + * @typedef {object} PaginatedBaseEventResponse + * @property {PaginationResult} _pagination.required - Pagination metadata * @property {Array} records.required - Returned borrel Schemas */ export interface PaginatedBaseEventResponse { @@ -101,7 +101,7 @@ export interface PaginatedBaseEventResponse { } /** - * @typedef {BaseUserResponse} EventPlanningSelectedCount + * @typedef {allOf|BaseUserResponse} EventPlanningSelectedCount * @property {integer} count.required - Number of times this user was selected for this shift */ export interface EventPlanningSelectedCount extends BaseUserResponse { diff --git a/src/controller/response/financial-mutation-response.ts b/src/controller/response/financial-mutation-response.ts index 2ea4c684b..fa6c38706 100644 --- a/src/controller/response/financial-mutation-response.ts +++ b/src/controller/response/financial-mutation-response.ts @@ -21,9 +21,9 @@ import { TransferResponse } from './transfer-response'; import { BaseTransactionResponse } from './transaction-response'; /** - * @typedef FinancialMutationResponse - * @property {string} type.required - Type of mutation ('transfer' or 'transaction') (Optional) - * @property {object} mutation - Details of mutation, this can be either of type TransferResponse or BaseTransactionResponse + * @typedef {object} FinancialMutationResponse + * @property {string} type.required - enum:transfer,transaction - Type of mutation ('transfer' or 'transaction') (Optional) + * @property {oneOf|TransferResponse|BaseTransactionResponse} mutation - Details of mutation, this can be either of type TransferResponse or BaseTransactionResponse */ export interface FinancialMutationResponse { type: 'transfer' | 'transaction', @@ -31,9 +31,9 @@ export interface FinancialMutationResponse { } /** - * @typedef PaginatedFinancialMutationResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned mutations + * @typedef {object} PaginatedFinancialMutationResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned mutations */ export interface PaginatedFinancialMutationResponse { _pagination: PaginationResult, diff --git a/src/controller/response/invoice-response.ts b/src/controller/response/invoice-response.ts index 36e13f1d6..068c04e82 100644 --- a/src/controller/response/invoice-response.ts +++ b/src/controller/response/invoice-response.ts @@ -23,10 +23,9 @@ import { PaginationResult } from '../../helpers/pagination'; import { InvoiceState } from '../../entity/invoices/invoice-status'; /** - * @typedef InvoiceStatusResponse - * @property {BaseUserResponse.model} changedBy.required - The user that changed the invoice status. - * @property {string} state.required - The state of the invoice, - * can be either CREATED, SENT, PAID or DELETED. + * @typedef {object} InvoiceStatusResponse + * @property {BaseUserResponse} changedBy.required - The user that changed the invoice status. + * @property {string} state.required - enum:CREATED,SENT,PAID,DELETED - The state of the invoice */ export interface InvoiceStatusResponse { state: keyof typeof InvoiceState, @@ -34,10 +33,10 @@ export interface InvoiceStatusResponse { } /** - * @typedef InvoiceEntryResponse + * @typedef {object} InvoiceEntryResponse * @property {string} description.required - The description of the entry * @property {integer} amount.required - Amount of products sold. - * @property {DineroObject.model} priceInclVat.required - The price per product. + * @property {DineroObject} priceInclVat.required - The price per product. * @property {number} vatPercentage.required - The percentage of VAT applied to this entry */ export interface InvoiceEntryResponse { @@ -48,12 +47,12 @@ export interface InvoiceEntryResponse { } /** - * @typedef {BaseResponse} BaseInvoiceResponse - * @property {BaseUserResponse.model} to.required - The person who was invoiced. + * @typedef {allOf|BaseResponse} BaseInvoiceResponse + * @property {BaseUserResponse} to.required - The person who was invoiced. * @property {string} addressee.required - Name of the addressed. * @property {string} description.required - Description of the invoice. - * @property {InvoiceStatusResponse.model} currentState.required - The current state of the invoice. - * @property {TransferResponse.model} transfer - Transfer linked to the invoice. + * @property {InvoiceStatusResponse} currentState.required - The current state of the invoice. + * @property {TransferResponse} transfer - Transfer linked to the invoice. */ export interface BaseInvoiceResponse extends BaseResponse { to: BaseUserResponse, @@ -64,25 +63,25 @@ export interface BaseInvoiceResponse extends BaseResponse { } /** - * @typedef {BaseInvoiceResponse} InvoiceResponse - * @property {Array.} invoiceEntries.required - The entries of the invoice + * @typedef {allOf|BaseInvoiceResponse} InvoiceResponse + * @property {Array} invoiceEntries.required - The entries of the invoice */ export interface InvoiceResponse extends BaseInvoiceResponse { invoiceEntries: InvoiceEntryResponse[], } /** - * @typedef {BaseInvoiceResponse} InvoiceResponseTypes - * @property {Array.} invoiceEntries - The entries of the invoice + * @typedef {allOf|BaseInvoiceResponse} InvoiceResponseTypes + * @property {Array} invoiceEntries - The entries of the invoice */ export interface InvoiceResponseTypes extends BaseInvoiceResponse { invoiceEntries?: InvoiceEntryResponse[], } /** - * @typedef PaginatedInvoiceResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned Invoices + * @typedef {object} PaginatedInvoiceResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned Invoices */ export interface PaginatedInvoiceResponse { _pagination: PaginationResult, diff --git a/src/controller/response/message-response.ts b/src/controller/response/message-response.ts index 209a823ba..3dfc8dbed 100644 --- a/src/controller/response/message-response.ts +++ b/src/controller/response/message-response.ts @@ -17,7 +17,7 @@ */ /** - * @typedef MessageResponse + * @typedef {object} MessageResponse * @property {string} message.required - The message response text. */ export default interface MessageResponse { diff --git a/src/controller/response/payout-request-response.ts b/src/controller/response/payout-request-response.ts index ddeefebce..9c0cb1e87 100644 --- a/src/controller/response/payout-request-response.ts +++ b/src/controller/response/payout-request-response.ts @@ -22,10 +22,10 @@ import { PayoutRequestState } from '../../entity/transactions/payout-request-sta import { PaginationResult } from '../../helpers/pagination'; /** - * @typedef {BaseResponse} BoilerPayoutRequestResponse - * @property {BaseUserResponse.model} requestedBy.required - The user that requested a payout - * @property {BaseUserResponse.model} approvedBy - The user that potentially approved the payout request - * @property {DineroObjectResponse.model} amount.required - The amount requested to be paid out + * @typedef {allOf|BaseResponse} BoilerPayoutRequestResponse + * @property {BaseUserResponse} requestedBy.required - The user that requested a payout + * @property {BaseUserResponse} approvedBy - The user that potentially approved the payout request + * @property {DineroObjectResponse} amount.required - The amount requested to be paid out */ interface BoilerPayoutRequestResponse extends BaseResponse { requestedBy: BaseUserResponse, @@ -34,7 +34,7 @@ interface BoilerPayoutRequestResponse extends BaseResponse { } /** - * @typedef {BoilerPayoutRequestResponse} BasePayoutRequestResponse + * @typedef {allOf|BoilerPayoutRequestResponse} BasePayoutRequestResponse * @property {string} status - The current status of the payout request */ export interface BasePayoutRequestResponse extends BoilerPayoutRequestResponse { @@ -42,7 +42,7 @@ export interface BasePayoutRequestResponse extends BoilerPayoutRequestResponse { } /** - * @typedef {BaseResponse} PayoutRequestStatusResponse + * @typedef {allOf|BaseResponse} PayoutRequestStatusResponse * @property {string} state.required - The state of this status change */ export interface PayoutRequestStatusResponse extends BaseResponse { @@ -50,8 +50,8 @@ export interface PayoutRequestStatusResponse extends BaseResponse { } /** - * @typedef {BoilerPayoutRequestResponse} PayoutRequestResponse - * @property {Array.} status.required - Statuses of this + * @typedef {allOf|BoilerPayoutRequestResponse} PayoutRequestResponse + * @property {Array} status.required - Statuses of this * payout response over time * @property {string} bankAccountNumber.required - Bank account number * @property {string} bankAccountName.required - Name of the account owner @@ -63,9 +63,9 @@ export interface PayoutRequestResponse extends BoilerPayoutRequestResponse { } /** - * @typedef PaginatedBasePayoutRequestResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned payout requests + * @typedef {object} PaginatedBasePayoutRequestResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned payout requests */ export interface PaginatedBasePayoutRequestResponse { _pagination: PaginationResult, diff --git a/src/controller/response/point-of-sale-response.ts b/src/controller/response/point-of-sale-response.ts index bf3d76deb..cb082ba52 100644 --- a/src/controller/response/point-of-sale-response.ts +++ b/src/controller/response/point-of-sale-response.ts @@ -21,15 +21,15 @@ import { BaseUserResponse } from './user-response'; import { PaginationResult } from '../../helpers/pagination'; /** - * @typedef {BaseResponse} BasePointOfSaleResponse + * @typedef {allOf|BaseResponse} BasePointOfSaleResponse * @property {string} name.required - The name of the point-of-sale. */ export interface BasePointOfSaleResponse extends BaseResponse { name: string, } /** - * @typedef {BasePointOfSaleResponse} PointOfSaleResponse - * @property {BaseUserResponse.model} owner - The owner of the point-of-sale. + * @typedef {allOf|BasePointOfSaleResponse} PointOfSaleResponse + * @property {BaseUserResponse} owner - The owner of the point-of-sale. * @property {number} revision.required - Revision of the POS * @property {boolean} useAuthentication.required - Whether this POS requires users to * authenticate themselves before making a transaction @@ -41,9 +41,9 @@ export interface PointOfSaleResponse extends BasePointOfSaleResponse { } /** - * @typedef PaginatedPointOfSaleResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned points of sale + * @typedef {object} PaginatedPointOfSaleResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned points of sale */ export interface PaginatedPointOfSaleResponse { _pagination: PaginationResult, @@ -51,8 +51,8 @@ export interface PaginatedPointOfSaleResponse { } /** - * @typedef {PointOfSaleResponse} PointOfSaleWithContainersResponse - * @property {Array.} containers.required - The containers + * @typedef {allOf|PointOfSaleResponse} PointOfSaleWithContainersResponse + * @property {Array} containers.required - The containers * in the point-of-sale. */ export interface PointOfSaleWithContainersResponse extends PointOfSaleResponse { diff --git a/src/controller/response/product-category-response.ts b/src/controller/response/product-category-response.ts index 79dd69fc3..ac49adc74 100644 --- a/src/controller/response/product-category-response.ts +++ b/src/controller/response/product-category-response.ts @@ -19,7 +19,7 @@ import BaseResponse from './base-response'; import { PaginationResult } from '../../helpers/pagination'; /** - * @typedef {BaseResponse} ProductCategoryResponse + * @typedef {allOf|BaseResponse} ProductCategoryResponse * @property {string} name.required - The name of the productCategory. */ export interface ProductCategoryResponse extends BaseResponse { @@ -27,9 +27,9 @@ export interface ProductCategoryResponse extends BaseResponse { } /** - * @typedef PaginatedProductCategoryResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned product categories + * @typedef {object} PaginatedProductCategoryResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned product categories */ export interface PaginatedProductCategoryResponse { _pagination: PaginationResult, diff --git a/src/controller/response/product-response.ts b/src/controller/response/product-response.ts index e979b2ad7..cc471e6b9 100644 --- a/src/controller/response/product-response.ts +++ b/src/controller/response/product-response.ts @@ -23,10 +23,10 @@ import { DineroObjectResponse } from './dinero-response'; import { BaseVatGroupResponse } from './vat-group-response'; /** - * @typedef {BaseResponse} BaseProductResponse + * @typedef {allOf|BaseResponse} BaseProductResponse * @property {string} name.required - The name of the product. - * @property {DineroObjectResponse.model} priceInclVat.required - The price of the product. - * @property {BaseVatGroupResponse.model} vat.required - The VAT percentage + * @property {DineroObjectResponse} priceInclVat.required - The price of the product. + * @property {BaseVatGroupResponse} vat.required - The VAT percentage */ export interface BaseProductResponse extends BaseResponse { name: string, @@ -35,12 +35,12 @@ export interface BaseProductResponse extends BaseResponse { } /** - * @typedef {BaseProductResponse} ProductResponse + * @typedef {allOf|BaseProductResponse} ProductResponse * @property {integer} revision.required - The product revision ID - * @property {BaseUserResponse.model} owner.required - The owner of the product. - * @property {ProductCategoryResponse.model} category.required - + * @property {BaseUserResponse} owner.required - The owner of the product. + * @property {ProductCategoryResponse} category.required - * The category the product belongs to. - * @property {DineroObjectResponse.model} priceExclVat.required - The price of the product + * @property {DineroObjectResponse} priceExclVat.required - The price of the product * excluding VAT * @property {string} image - The URL to the picture representing this product. * @property {number} alcoholPercentage.required - The percentage of alcohol in this product. @@ -55,9 +55,9 @@ export interface ProductResponse extends BaseProductResponse { } /** - * @typedef PaginatedProductResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned products + * @typedef {object} PaginatedProductResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned products */ export interface PaginatedProductResponse { _pagination: PaginationResult, diff --git a/src/controller/response/rbac/action-response.ts b/src/controller/response/rbac/action-response.ts index 0938347c4..fbe16c6d1 100644 --- a/src/controller/response/rbac/action-response.ts +++ b/src/controller/response/rbac/action-response.ts @@ -19,11 +19,11 @@ import RelationResponse from './relation-response'; /** - * @typedef ActionResponse - + * @typedef {object} ActionResponse - * The action contains the name of the action and a list of permissions per action. * Typically the action name is one of the CRUD values 'create', 'read', 'update', and 'delete'. - * @property {string} action - The name of the action performed on the entity. - * @property {Array.} relations - The ownership relations with permissions. + * @property {string} action.required - The name of the action performed on the entity. + * @property {Array} relations.required - The ownership relations with permissions. */ export default interface ActionResponse { action: string; diff --git a/src/controller/response/rbac/entity-response.ts b/src/controller/response/rbac/entity-response.ts index dff775a17..1c1ebace3 100644 --- a/src/controller/response/rbac/entity-response.ts +++ b/src/controller/response/rbac/entity-response.ts @@ -19,10 +19,10 @@ import ActionResponse from './action-response'; /** - * @typedef EntityResponse - + * @typedef {object} EntityResponse - * The entity contains a name and a list of permissions per action. - * @property {string} entity - The name of the entity for which the permissions are. - * @property {Array.} actions - The permissions per action. + * @property {string} entity.required - The name of the entity for which the permissions are. + * @property {Array} actions.required - The permissions per action. */ export default interface EntityResponse { entity: string; diff --git a/src/controller/response/rbac/relation-response.ts b/src/controller/response/rbac/relation-response.ts index 94ae115ab..defc38866 100644 --- a/src/controller/response/rbac/relation-response.ts +++ b/src/controller/response/rbac/relation-response.ts @@ -19,12 +19,12 @@ import { AllowedAttribute } from '../../../rbac/role-manager'; /** - * @typedef RelationResponse - + * @typedef {object} RelationResponse - * The relation response contains the name of the ownership relation towards the entity, * and the list of attributes for which the role gives access. * Typical ownership relations are 'own', 'created', and 'all'. - * @property {string} relation - The the ownership relation towards the entity. - * @property {Array.} attributes - The attributes of the entity for which there is access. + * @property {string} relation.required - The the ownership relation towards the entity. + * @property {Array} attributes.required - The attributes of the entity for which there is access. */ export default interface RelationResponse { relation: string; diff --git a/src/controller/response/rbac/role-response.ts b/src/controller/response/rbac/role-response.ts index c21eb814e..c29f29f66 100644 --- a/src/controller/response/rbac/role-response.ts +++ b/src/controller/response/rbac/role-response.ts @@ -19,10 +19,10 @@ import EntityResponse from './entity-response'; /** - * @typedef RoleResponse - + * @typedef {object} RoleResponse - * A role contains a unique name, and a list of permissions per entity. * @property {string} role.required - The name of the role. - * @property {Array.} entities - The permissions with regards to the entity. + * @property {Array} entities.required - The permissions with regards to the entity. */ export default interface RoleResponse { role: string; diff --git a/src/controller/response/simple-file-response.ts b/src/controller/response/simple-file-response.ts index 2c12c14f2..ca421574d 100644 --- a/src/controller/response/simple-file-response.ts +++ b/src/controller/response/simple-file-response.ts @@ -19,10 +19,10 @@ import BaseResponse from './base-response'; import { UserResponse } from './user-response'; /** - * @typedef {BaseResponse} SimpleFileResponse + * @typedef {allOf|BaseResponse} SimpleFileResponse * @property {string} downloadName.required - The filename of the file * @property {string} location.required - The location of the file in storage - * @property {UserResponse.model} createdBy.required - The user who created this file + * @property {UserResponse} createdBy.required - The user who created this file */ export interface SimpleFileResponse extends BaseResponse { downloadName: string; diff --git a/src/controller/response/stripe-response.ts b/src/controller/response/stripe-response.ts index 0852ca3a2..4910b7a91 100644 --- a/src/controller/response/stripe-response.ts +++ b/src/controller/response/stripe-response.ts @@ -22,7 +22,7 @@ import { StripeDepositState } from '../../entity/deposit/stripe-deposit-status'; import { BaseUserResponse } from './user-response'; /** - * @typedef {BaseResponse} StripePaymentIntentResponse + * @typedef {allOf|BaseResponse} StripePaymentIntentResponse * @property {string} stripeId.required - ID of the intent in Stripe. * @property {string} clientSecret.required - The client secret of the created Payment Intent. */ @@ -31,20 +31,23 @@ export interface StripePaymentIntentResponse extends BaseResponse { clientSecret: string; } +// TODO find a fix for integer enums. +// * @property {integer} state.required - enum:1,2,3,4 - State of the Stripe deposit. It can be 1 ('CREATED'), 2 ('PROCESSING'), 3 ('SUCCEEDED'), or 4 ('FAILED') +// @see https://github.com/BRIKEV/express-jsdoc-swagger/issues/257 /** - * @typedef {BaseResponse} StripeDepositStatusResponse - * @property {number} state.required - State of the Stripe deposit. It can be 1 ('CREATED'), 2 ('PROCESSING'), 3 ('SUCCEEDED'), or 4 ('FAILED') + * @typedef {allOf|BaseResponse} StripeDepositStatusResponse + * @property {integer} state.required - State of the Stripe deposit. It can be 1 ('CREATED'), 2 ('PROCESSING'), 3 ('SUCCEEDED'), or 4 ('FAILED') */ export interface StripeDepositStatusResponse extends BaseResponse { state: StripeDepositState; } /** - * @typedef {BaseResponse} StripeDepositResponse + * @typedef {allOf|BaseResponse} StripeDepositResponse * @property {string} stripeId.required - The ID of the payment intent in Stripe - * @property {Array.} depositStatus.required - Current status of the deposit - * @property {DineroObjectResponse.model} amount.required - The amount deposited - * @property {BaseUserResponse.model} to.required - User that deposited money + * @property {Array} depositStatus.required - Current status of the deposit + * @property {DineroObjectResponse} amount.required - The amount deposited + * @property {BaseUserResponse} to.required - User that deposited money */ export interface StripeDepositResponse extends BaseResponse { stripeId: string; diff --git a/src/controller/response/transaction-report-response.ts b/src/controller/response/transaction-report-response.ts index 159029686..b2104a8d2 100644 --- a/src/controller/response/transaction-report-response.ts +++ b/src/controller/response/transaction-report-response.ts @@ -54,7 +54,7 @@ export interface TransactionReport { } /** - * @typedef TransactionFilterParameters + * @typedef {object} TransactionFilterParameters * @property {Array} transactionId * @property {number} fromId * @property {number} createdById @@ -72,10 +72,10 @@ export interface TransactionReport { */ /** - * @typedef TransactionReportVatEntryResponse - * @property {BaseVatGroupResponse.model} vat.required - The vat group of this entry - * @property {DineroObjectResponse.model} totalInclVat.required - The price of this entry incl. vat - * @property {DineroObjectResponse.model} totalExclVat.required - The price of this entry excl. vat + * @typedef {object} TransactionReportVatEntryResponse + * @property {BaseVatGroupResponse} vat.required - The vat group of this entry + * @property {DineroObjectResponse} totalInclVat.required - The price of this entry incl. vat + * @property {DineroObjectResponse} totalExclVat.required - The price of this entry excl. vat */ export interface TransactionReportVatEntryResponse { vat: BaseVatGroupResponse, @@ -84,10 +84,10 @@ export interface TransactionReportVatEntryResponse { } /** - * @typedef TransactionReportCategoryEntryResponse - * @property {ProductCategoryResponse.model} category.required - The category of this entry - * @property {DineroObjectResponse.model} totalInclVat.required - The price of this entry incl. vat - * @property {DineroObjectResponse.model} totalExclVat.required - The price of this entry excl. vat + * @typedef {object} TransactionReportCategoryEntryResponse + * @property {ProductCategoryResponse} category.required - The category of this entry + * @property {DineroObjectResponse} totalInclVat.required - The price of this entry incl. vat + * @property {DineroObjectResponse} totalExclVat.required - The price of this entry excl. vat */ export interface TransactionReportCategoryEntryResponse { category: ProductCategoryResponse, @@ -96,11 +96,11 @@ export interface TransactionReportCategoryEntryResponse { } /** - * @typedef TransactionReportEntryResponse + * @typedef {object} TransactionReportEntryResponse * @property {integer} count.required - The amount of times this product is in the report - * @property {BaseProductResponse.model} product.required - The product for this entry - * @property {DineroObjectResponse.model} totalInclVat.required - The price of this entry incl. vat - * @property {DineroObjectResponse.model} totalExclVat.required - The price of this entry excl. vat + * @property {BaseProductResponse} product.required - The product for this entry + * @property {DineroObjectResponse} totalInclVat.required - The price of this entry incl. vat + * @property {DineroObjectResponse} totalExclVat.required - The price of this entry excl. vat */ export interface TransactionReportEntryResponse { count: number, @@ -110,10 +110,10 @@ export interface TransactionReportEntryResponse { } /** - * @typedef TransactionReportDataResponse - * @property {Array.} entries.required - The entries grouped by product - * @property {Array.} categories.required - The entries grouped by category - * @property {Array.} vat.required - The entries grouped by vat + * @typedef {object} TransactionReportDataResponse + * @property {Array} entries.required - The entries grouped by product + * @property {Array} categories.required - The entries grouped by category + * @property {Array} vat.required - The entries grouped by vat */ export interface TransactionReportDataResponse { entries: TransactionReportEntryResponse[], @@ -122,11 +122,11 @@ export interface TransactionReportDataResponse { } /** - * @typedef TransactionReportResponse - * @property {TransactionFilterParameters.model} parameters.required - The parameters used for the report - * @property {TransactionReportDataResponse.model} data.required - The data that makes up the report - * @property {DineroObjectResponse.model} totalExclVat.required - The total amount of money excl. vat of this report - * @property {DineroObjectResponse.model} totalInclVat.required - The total amount of money inc. vat of this report + * @typedef {object} TransactionReportResponse + * @property {TransactionFilterParameters} parameters.required - The parameters used for the report + * @property {TransactionReportDataResponse} data.required - The data that makes up the report + * @property {DineroObjectResponse} totalExclVat.required - The total amount of money excl. vat of this report + * @property {DineroObjectResponse} totalInclVat.required - The total amount of money inc. vat of this report */ export interface TransactionReportResponse { parameters: TransactionFilterParameters, diff --git a/src/controller/response/transaction-response.ts b/src/controller/response/transaction-response.ts index 2aefc0f76..75ae2e288 100644 --- a/src/controller/response/transaction-response.ts +++ b/src/controller/response/transaction-response.ts @@ -17,7 +17,7 @@ */ import { DineroObject } from 'dinero.js'; import BaseResponse from './base-response'; -import { BasePointOfSaleResponse } from './point-of-sale-response'; +import {BasePointOfSaleResponse, PointOfSaleResponse} from './point-of-sale-response'; import { BaseContainerResponse } from './container-response'; import { BaseProductResponse } from './product-response'; import { BaseUserResponse } from './user-response'; @@ -25,14 +25,14 @@ import { DineroObjectResponse } from './dinero-response'; import { PaginationResult } from '../../helpers/pagination'; /** - * @typedef {BaseResponse} BaseTransactionResponse - * @property {BaseUserResponse.model} from.required - The account from which the transaction + * @typedef {allOf|BaseResponse} BaseTransactionResponse + * @property {BaseUserResponse} from.required - The account from which the transaction * is subtracted. - * @property {BaseUserResponse.model} createdBy - The user that created the transaction, if not + * @property {BaseUserResponse} createdBy - The user that created the transaction, if not * same as 'from'.. - * @property {BasePointOfSaleResponse.model} pointOfSale.required - The POS at which this transaction + * @property {BasePointOfSaleResponse} pointOfSale.required - The POS at which this transaction * has been created - * @property {Dinero.model} value.required - Total sum of subtransactions + * @property {Dinero} value.required - Total sum of subtransactions */ export interface BaseTransactionResponse extends BaseResponse { from: BaseUserResponse, @@ -42,34 +42,34 @@ export interface BaseTransactionResponse extends BaseResponse { } /** - * @typedef {BaseResponse} TransactionResponse - * @property {BaseUserResponse.model} from.required - The account from which the transaction + * @typedef {allOf|BaseResponse} TransactionResponse + * @property {BaseUserResponse} from.required - The account from which the transaction * is subtracted. - * @property {BaseUserResponse.model} createdBy - The user that created the transaction, if not + * @property {BaseUserResponse} createdBy - The user that created the transaction, if not * same as 'from'. - * @property {Array.} subTransactions.required - The subtransactions + * @property {Array} subTransactions.required - The subtransactions * belonging to this transaction. - * @property {BasePointOfSaleResponse.model} pointOfSale.required - The POS at which this transaction + * @property {PointOfSaleResponse} pointOfSale.required - The POS at which this transaction * has been created - * @property {DineroObjectResponse.model} totalPriceInclVat.required - The total cost of the + * @property {DineroObjectResponse} totalPriceInclVat.required - The total cost of the * transaction */ export interface TransactionResponse extends BaseResponse { from: BaseUserResponse, createdBy?: BaseUserResponse, subTransactions: SubTransactionResponse[], - pointOfSale: BasePointOfSaleResponse, + pointOfSale: PointOfSaleResponse, totalPriceInclVat: DineroObjectResponse, } /** - * @typedef {BaseResponse} SubTransactionResponse - * @property {BaseUserResponse.model} to.required - The account that the transaction is added to. - * @property {BaseContainerResponse.model} container.required - The container from which all + * @typedef {allOf|BaseResponse} SubTransactionResponse + * @property {BaseUserResponse} to.required - The account that the transaction is added to. + * @property {BaseContainerResponse} container.required - The container from which all * products in the SubTransactionRows are bought - * @property {Array.} subTransactionRows.required - The rows of this + * @property {Array} subTransactionRows.required - The rows of this * SubTransaction - * @property {DineroObjectResponse.model} totalPriceInclVat.required - The total cost of the sub + * @property {DineroObjectResponse} totalPriceInclVat.required - The total cost of the sub * transaction */ export interface SubTransactionResponse extends BaseResponse { @@ -80,10 +80,10 @@ export interface SubTransactionResponse extends BaseResponse { } /** - * @typedef {BaseResponse} SubTransactionRowResponse - * @property {BaseProductResponse.model} product.required - The product that has been bought + * @typedef {allOf|BaseResponse} SubTransactionRowResponse + * @property {BaseProductResponse} product.required - The product that has been bought * @property {number} amount.required - The amount that has been bought - * @property {DineroObjectResponse.model} totalPriceInclVat.required - The cost of the + * @property {DineroObjectResponse} totalPriceInclVat.required - The cost of the * sub transaction row */ export interface SubTransactionRowResponse extends BaseResponse { @@ -93,9 +93,9 @@ export interface SubTransactionRowResponse extends BaseResponse { } /** - * @typedef PaginatedBaseTransactionResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned banners + * @typedef {object} PaginatedBaseTransactionResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned banners */ export interface PaginatedBaseTransactionResponse { _pagination: PaginationResult, diff --git a/src/controller/response/transfer-response.ts b/src/controller/response/transfer-response.ts index 6632911c0..3c8e61055 100644 --- a/src/controller/response/transfer-response.ts +++ b/src/controller/response/transfer-response.ts @@ -26,16 +26,16 @@ import { BasePayoutRequestResponse } from './payout-request-response'; import { FineResponse, UserFineGroupResponse } from './debtor-response'; /** - * @typedef {BaseResponse} TransferResponse + * @typedef {allOf|BaseResponse} TransferResponse * @property {string} description.required - Description of the transfer - * @property {Dinero.model} amount.required - Amount of money being transferred - * @property {BaseUserResponse.model} from - from which user the money is being transferred - * @property {BaseUserResponse.model} to - to which user the money is being transferred. - * @property {BaseInvoiceResponse.model} invoice - invoice belonging to this transfer - * @property {StripeDepositResponse.model} deposit - deposit belonging to this transfer - * @property {BasePayoutRequestResponse.model} payoutRequest - payout request belonging to this transfer - * @property {FineResponse.model} fine - fine belonging to this transfer - * @property {UserFineGroupResponse.model} waivedFines - fines that have been waived by this transfer + * @property {Dinero} amount.required - Amount of money being transferred + * @property {BaseUserResponse} from - from which user the money is being transferred + * @property {BaseUserResponse} to - to which user the money is being transferred. + * @property {BaseInvoiceResponse} invoice - invoice belonging to this transfer + * @property {StripeDepositResponse} deposit - deposit belonging to this transfer + * @property {BasePayoutRequestResponse} payoutRequest - payout request belonging to this transfer + * @property {FineResponse} fine - fine belonging to this transfer + * @property {UserFineGroupResponse} waivedFines - fines that have been waived by this transfer */ export interface TransferResponse extends BaseResponse { amount: DineroObjectResponse; @@ -50,9 +50,9 @@ export interface TransferResponse extends BaseResponse { } /** - * @typedef PaginatedTransferResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned transfers + * @typedef {object} PaginatedTransferResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned transfers */ export interface PaginatedTransferResponse { _pagination: PaginationResult, diff --git a/src/controller/response/update-key-response.ts b/src/controller/response/update-key-response.ts index f91486481..f24c6db23 100644 --- a/src/controller/response/update-key-response.ts +++ b/src/controller/response/update-key-response.ts @@ -19,7 +19,7 @@ import BaseResponse from './base-response'; /** - * @typedef UpdateKeyResponse + * @typedef {object} UpdateKeyResponse * @property {string} key.required - The key to return */ export default interface UpdateKeyResponse extends BaseResponse { diff --git a/src/controller/response/user-response.ts b/src/controller/response/user-response.ts index 30a663b31..957fbbd7a 100644 --- a/src/controller/response/user-response.ts +++ b/src/controller/response/user-response.ts @@ -20,7 +20,7 @@ import { PaginationResult } from '../../helpers/pagination'; import { TermsOfServiceStatus } from '../../entity/user/user'; /** - * @typedef {BaseResponse} BaseUserResponse + * @typedef {allOf|BaseResponse} BaseUserResponse * @property {string} firstName.required - The name of the user. * @property {string} lastName.required - The last name of the user * @property {string} nickname - The nickname of the user @@ -32,7 +32,7 @@ export interface BaseUserResponse extends BaseResponse { } /** - * @typedef {BaseUserResponse} UserResponse + * @typedef {allOf|BaseUserResponse} UserResponse * @property {boolean} active.required - Whether the user activated * @property {boolean} deleted.required - Whether the user is deleted * @property {string} type.required - The type of user @@ -55,9 +55,9 @@ export interface UserResponse extends BaseUserResponse { } /** - * @typedef PaginatedUserResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned users + * @typedef {object} PaginatedUserResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned users */ export interface PaginatedUserResponse { _pagination: PaginationResult, diff --git a/src/controller/response/vat-group-response.ts b/src/controller/response/vat-group-response.ts index 8eb09f2f5..ab1d3dab2 100644 --- a/src/controller/response/vat-group-response.ts +++ b/src/controller/response/vat-group-response.ts @@ -21,7 +21,7 @@ import VatGroup, { VatDeclarationPeriod } from '../../entity/vat-group'; import BaseResponse from './base-response'; /** - * @typedef {BaseResponse} BaseVatGroupResponse + * @typedef {allOf|BaseResponse} BaseVatGroupResponse * @property {number} percentage.required - Percentage of VAT * @property {boolean} hidden.required - Whether VAT should be hidden */ @@ -31,9 +31,15 @@ export interface BaseVatGroupResponse extends BaseResponse { } /** - * @typedef PaginatedVatGroupResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned VAT groups + * @typedef {allOf|BaseVatGroupResponse} VatGroupResponse + * @property {string} name.rquired - Name of the VAT group + * @property {boolean} deleted.required - Whether this group is soft-deleted + */ + +/** + * @typedef {object} PaginatedVatGroupResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned VAT groups */ export interface PaginatedVatGroupResponse { _pagination: PaginationResult, @@ -41,11 +47,11 @@ export interface PaginatedVatGroupResponse { } /** - * @typedef VatDeclarationRow + * @typedef {object} VatDeclarationRow * @property {number} id.required - ID of the VAT group * @property {string} name.required - Name of the VAT group * @property {number} percentage.required - Percentage of VAT in this group - * @property {Array.} values.required - Amount of VAT to be paid to the tax administration + * @property {Array} values.required - Amount of VAT to be paid to the tax administration * per period */ export interface VatDeclarationRow { @@ -56,10 +62,10 @@ export interface VatDeclarationRow { } /** - * @typedef VatDeclarationResponse + * @typedef {object} VatDeclarationResponse * @property {number} calendarYear.required - Calendar year of this result table * @property {string} period.required - The used VAT declaration period the rows below are based upon - * @property {Array.} rows.required - The rows of the result table + * @property {Array} rows.required - The rows of the result table */ export interface VatDeclarationResponse { calendarYear: number; diff --git a/src/controller/response/voucher-group-response.ts b/src/controller/response/voucher-group-response.ts index 134986152..a4c42c0e1 100644 --- a/src/controller/response/voucher-group-response.ts +++ b/src/controller/response/voucher-group-response.ts @@ -21,12 +21,12 @@ import { PaginationResult } from '../../helpers/pagination'; import { DineroObjectResponse } from './dinero-response'; /** - * @typedef {BaseResponse} VoucherGroupResponse + * @typedef {allOf|BaseResponse} VoucherGroupResponse * @property {string} name.required - Name of the voucher group * @property {string} activeStartDate - Start date of the voucher group * @property {string} activeEndDate.required - End date of the voucher group - * @property {Array.} users.required - Users in the voucher group - * @property {DineroObjectRequest.model} balance.required - Start balance to be assigned + * @property {Array} users.required - Users in the voucher group + * @property {DineroObjectRequest} balance.required - Start balance to be assigned * to the voucher users * @property {number} amount.required - Amount of users to be assigned to the voucher group */ @@ -40,9 +40,9 @@ export default interface VoucherGroupResponse extends BaseResponse { } /** - * @typedef PaginatedVoucherGroupResponse - * @property {PaginationResult.model} _pagination.required - Pagination metadata - * @property {Array.} records.required - Returned voucher groups + * @typedef {object} PaginatedVoucherGroupResponse + * @property {PaginationResult} _pagination.required - Pagination metadata + * @property {Array} records.required - Returned voucher groups */ export interface PaginatedVoucherGroupResponse { _pagination: PaginationResult, diff --git a/src/controller/root-controller.ts b/src/controller/root-controller.ts index d55dbea56..e42b75679 100644 --- a/src/controller/root-controller.ts +++ b/src/controller/root-controller.ts @@ -15,12 +15,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { Request, Response } from 'express'; -import log4js, { Logger } from 'log4js'; -import { getConnection } from 'typeorm'; -import BaseController, { BaseControllerOptions } from './base-controller'; +import {Request, Response} from 'express'; +import log4js, {Logger} from 'log4js'; +import {getConnection} from 'typeorm'; +import BaseController, {BaseControllerOptions} from './base-controller'; import Policy from './policy'; -import { parseRequestPagination } from '../helpers/pagination'; +import {parseRequestPagination} from '../helpers/pagination'; import BannerService from '../service/banner-service'; export default class RootController extends BaseController { @@ -59,15 +59,15 @@ export default class RootController extends BaseController { } /** - * Returns all existing banners - * @route GET /open/banners + * GET /open/banners + * @summary Returns all existing banners * @operationId getAllOpenBanners - * @group banners - Operations of banner controller + * @tags banners - Operations of banner controller * @param {integer} take.query - How many banners the endpoint should return * @param {integer} skip.query - How many banners should be skipped (for pagination) - * @returns {PaginatedBannerResponse.model} 200 - All existing banners - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {PaginatedBannerResponse} 200 - All existing banners + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async returnAllBanners(req: Request, res: Response): Promise { this.logger.trace('Get all banners by', req.ip); @@ -85,7 +85,7 @@ export default class RootController extends BaseController { // handle request try { - res.json(await BannerService.getBanners({}, { take, skip })); + res.json(await BannerService.getBanners({}, {take, skip})); } catch (error) { this.logger.error('Could not return all banners:', error); res.status(500).json('Internal server error.'); @@ -93,12 +93,12 @@ export default class RootController extends BaseController { } /** - * Ping the backend to check whether everything is working correctly - * @route GET /ping + * GET /ping + * @summary Ping the backend to check whether everything is working correctly * @operationId ping - * @group root - Operations of the root controller - * @returns {string} 200 - Success - * @returns {string} 500 - Internal server error (database error) + * @tags root - Operations of the root controller + * @return {string} 200 - Success + * @return {string} 500 - Internal server error (database error) */ public async ping(req: Request, res: Response): Promise { this.logger.trace('Ping by', req.ip); diff --git a/src/controller/simple-file-controller.ts b/src/controller/simple-file-controller.ts index a3c95a61f..8c948c771 100644 --- a/src/controller/simple-file-controller.ts +++ b/src/controller/simple-file-controller.ts @@ -72,18 +72,24 @@ export default class SimpleFileController extends BaseController { }; } + + /** + * @typedef {object} FileUpload + * @property {string} name.required - The name of the file + * @property {string} file - file - binary + */ + /** - * Upload a file with the given name. - * @route POST /files + * POST /files + * @summary Upload a file with the given name. * @operationId createFile - * @group files - Operations of the simple files controller + * @tags files - Operations of the simple files controller * @consumes multipart/form-data - * @param {file} file.formData - * @param {string} name.formData + * @param {FileUpload} request.body.required - simple file - multipart/form-data * @security JWT - * @returns {SimpleFileResponse.model} 200 - The uploaded file entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {SimpleFileResponse} 200 - The uploaded file entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async uploadFile(req: RequestWithToken, res: Response): Promise { this.logger.trace('Upload simple file by user', req.token.user); @@ -110,15 +116,15 @@ export default class SimpleFileController extends BaseController { } /** - * Download a file with the given id. - * @route GET /files/{id} + * GET /files/{id} + * @summary Download a file with the given id. * @operationId getFile - * @group files - Operations of the simple files controller + * @tags files - Operations of the simple files controller * @param {integer} id.path.required - The id of the file which should be downloaded * @security JWT - * @returns {Buffer} 200 - The requested file - * @returns {string} 404 - File not found - * @returns {string} 500 - Internal server error + * @return {string} 200 - The requested file + * @return {string} 404 - File not found + * @return {string} 500 - Internal server error */ public async downloadFile(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -139,15 +145,15 @@ export default class SimpleFileController extends BaseController { } /** - * Delete the file with the given id. - * @route DELETE /files/{id} + * DELETE /files/{id} + * @summary Delete the file with the given id. * @operationId deleteFile - * @group files - Operations of the simple files controller + * @tags files - Operations of the simple files controller * @param {integer} id.path.required - The id of the file which should be deleted * @security JWT - * @returns {Buffer} 204 - Success - * @returns {string} 404 - File not found - * @returns {string} 500 - Internal server error + * @return {string} 204 - Success + * @return {string} 404 - File not found + * @return {string} 500 - Internal server error */ public async deleteFile(req: RequestWithToken, res: Response): Promise { const { id } = req.params; diff --git a/src/controller/stripe-controller.ts b/src/controller/stripe-controller.ts index 47a4a339f..37380c343 100644 --- a/src/controller/stripe-controller.ts +++ b/src/controller/stripe-controller.ts @@ -57,13 +57,13 @@ export default class StripeController extends BaseController { } /** - * Start the stripe deposit flow - * @route POST /stripe/deposit + * POST /stripe/deposit + * @summary Start the stripe deposit flow * @operationId deposit - * @group Stripe - Operations of the stripe controller - * @param {StripeRequest.model} stripe.body.required - The deposit that should be created - * @returns {StripePaymentIntentResponse.model} 200 - Payment Intent information - * @returns {string} 500 - Internal server error + * @tags Stripe - Operations of the stripe controller + * @param {StripeRequest} request.body.required - The deposit that should be created + * @return {StripePaymentIntentResponse} 200 - Payment Intent information + * @return {string} 500 - Internal server error * @security JWT */ public async createStripeDeposit(req: RequestWithToken, res: Response): Promise { diff --git a/src/controller/stripe-webhook-controller.ts b/src/controller/stripe-webhook-controller.ts index 6217c54f6..40517e846 100644 --- a/src/controller/stripe-webhook-controller.ts +++ b/src/controller/stripe-webhook-controller.ts @@ -56,9 +56,9 @@ export default class StripeWebhookController extends BaseController { * * @route POST /stripe/webhook * @operationId webhook - * @group Stripe - Operations of the stripe controller - * @returns 200 - Success - * @returns 400 - Not + * @tags Stripe - Operations of the stripe controller + * @return 200 - Success + * @return 400 - Not */ public async handleWebhookEvent(req: RequestWithRawBody, res: Response): Promise { const { rawBody } = req; diff --git a/src/controller/test-controller.ts b/src/controller/test-controller.ts index b53c4bca4..bb9696fef 100644 --- a/src/controller/test-controller.ts +++ b/src/controller/test-controller.ts @@ -53,13 +53,13 @@ export default class TestController extends BaseController { } /** - * Get a beautiful Hello World email to your inbox - * @route POST /test/helloworld + * POST /test/helloworld + * @summary Get a beautiful Hello World email to your inbox * @operationId helloworld - * @group test- Operations of the test controller + * @tags test- Operations of the test controller * @security JWT - * @returns {string} 204 - Success - * @returns {string} 500 - Internal server error + * @return {string} 204 - Success + * @return {string} 500 - Internal server error */ public async helloWorld(req: RequestWithToken, res: Response): Promise { this.logger.trace('Hello world email by', req.token.user.id); diff --git a/src/controller/transaction-controller.ts b/src/controller/transaction-controller.ts index 11fcae088..3228e65c5 100644 --- a/src/controller/transaction-controller.ts +++ b/src/controller/transaction-controller.ts @@ -85,10 +85,10 @@ export default class TransactionController extends BaseController { } /** - * Get a list of all transactions - * @route GET /transactions + * GET /transactions + * @summary Get a list of all transactions * @operationId getAllTransactions - * @group transactions - Operations of the transaction controller + * @tags transactions - Operations of the transaction controller * @security JWT * @param {integer} fromId.query - From-user for selected transactions * @param {integer} createdById.query - User that created selected transaction @@ -102,7 +102,7 @@ export default class TransactionController extends BaseController { * @param {string} tillDate.query - End date for selected transactions (exclusive) * @param {integer} take.query - How many transactions the endpoint should return * @param {integer} skip.query - How many transactions should be skipped (for pagination) - * @returns {PaginatedBaseTransactionResponse.model} 200 - A list of all transactions + * @return {PaginatedBaseTransactionResponse} 200 - A list of all transactions */ public async getAllTransactions(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all transactions by user', req.token.user); @@ -132,17 +132,17 @@ export default class TransactionController extends BaseController { } /** - * Creates a new transaction - * @route POST /transactions + * POST /transactions + * @summary Creates a new transaction * @operationId createTransaction - * @group transactions - Operations of the transaction controller - * @param {TransactionRequest.model} transaction.body.required - + * @tags transactions - Operations of the transaction controller + * @param {TransactionRequest} request.body.required - * The transaction which should be created * @security JWT - * @returns {TransactionResponse.model} 200 - The created transaction entity - * @returns {string} 400 - Validation error - * @returns {string} 403 - Insufficient balance error - * @returns {string} 500 - Internal server error + * @return {TransactionResponse} 200 - The created transaction entity + * @return {string} 400 - Validation error + * @return {string} 403 - Insufficient balance error + * @return {string} 500 - Internal server error */ public async createTransaction(req: RequestWithToken, res: Response): Promise { const body = req.body as TransactionRequest; @@ -170,14 +170,14 @@ export default class TransactionController extends BaseController { } /** - * Get a single transaction - * @route GET /transactions/{id} + * GET /transactions/{id} + * @summary Get a single transaction * @operationId getSingleTransaction - * @group transactions - Operations of the transaction controller + * @tags transactions - Operations of the transaction controller * @param {integer} id.path.required - The id of the transaction which should be returned * @security JWT - * @returns {TransactionResponse.model} 200 - Single transaction with given id - * @returns {string} 404 - Nonexistent transaction id + * @return {TransactionResponse} 200 - Single transaction with given id + * @return {string} 404 - Nonexistent transaction id */ public async getTransaction(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -202,18 +202,18 @@ export default class TransactionController extends BaseController { } /** - * Updates the requested transaction - * @route PATCH /transactions/{id} + * PATCH /transactions/{id} + * @summary Updates the requested transaction * @operationId updateTransaction - * @group transactions - Operations of transaction controller + * @tags transactions - Operations of transaction controller * @param {integer} id.path.required - The id of the transaction which should be updated - * @param {TransactionRequest.model} transaction.body.required - + * @param {TransactionRequest} request.body.required - * The updated transaction * @security JWT - * @returns {TransactionResponse.model} 200 - The requested transaction entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {TransactionResponse} 200 - The requested transaction entity + * @return {string} 400 - Validation error + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async updateTransaction(req: RequestWithToken, res: Response): Promise { const body = req.body as TransactionRequest; @@ -240,14 +240,14 @@ export default class TransactionController extends BaseController { } /** - * Deletes a transaction - * @route DELETE /transactions/{id} + * DELETE /transactions/{id} + * @summary Deletes a transaction * @operationId deleteTransaction - * @group transactions - Operations of the transaction controller + * @tags transactions - Operations of the transaction controller * @param {integer} id.path.required - The id of the transaction which should be deleted * @security JWT - * @returns {TransactionResponse.model} 200 - The deleted transaction - * @returns {string} 404 - Nonexistent transaction id + * @return {TransactionResponse} 200 - The deleted transaction + * @return {string} 404 - Nonexistent transaction id */ // eslint-disable-next-line class-methods-use-this public async deleteTransaction(req: RequestWithToken, res: Response): Promise { @@ -268,16 +268,16 @@ export default class TransactionController extends BaseController { } /** - * Function to validate the transaction immediatly after it is created - * @route POST /transactions/validate + * POST /transactions/validate + * @summary Function to validate the transaction immediatly after it is created * @operationId validateTransaction - * @group transactions - Operations of the transaction controller - * @param {TransactionRequest.model} transaction.body.required - + * @tags transactions - Operations of the transaction controller + * @param {TransactionRequest} request.body.required - * The transaction which should be validated - * @returns {Boolean} 200 - Transaction validated + * @return {boolean} 200 - Transaction validated * @security JWT - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async validateTransaction(req: RequestWithToken, res: Response): Promise { const body = req.body as TransactionRequest; @@ -302,7 +302,7 @@ export default class TransactionController extends BaseController { * other if transaction createdby is and linked via organ * own if user is connected to transaction * @param req - Request with TransactionRequest in the body - * @returns whether transaction is connected to user token + * @return whether transaction is connected to user token */ static async postRelation(req: RequestWithToken): Promise { const request = req.body as TransactionRequest; @@ -322,7 +322,7 @@ export default class TransactionController extends BaseController { * organ if user is not connected to transaction via organ * own if user is connected to transaction * @param req - Request with transaction id as param - * @returns whether transaction is connected to user token + * @return whether transaction is connected to user token */ static async getRelation(req: RequestWithToken): Promise { const transaction = await Transaction.findOne({ diff --git a/src/controller/transfer-controller.ts b/src/controller/transfer-controller.ts index 745ff6866..50ecae082 100644 --- a/src/controller/transfer-controller.ts +++ b/src/controller/transfer-controller.ts @@ -66,7 +66,7 @@ export default class TransferController extends BaseController { * own if user is connected to transaction * organ if user is connected to transaction via organ * @param req - * @returns whether transaction is connected to used token + * @return whether transaction is connected to used token */ static async getRelation(req: RequestWithToken): Promise { const transfer = await Transfer.findOne({ where: { id: parseInt(req.params.id, 10) }, relations: ['to', 'from'] }); @@ -83,15 +83,15 @@ export default class TransferController extends BaseController { } /** - * Returns all existing transfers - * @route GET /transfers + * GET /transfers + * @summary Returns all existing transfers * @operationId getAllTransfers - * @group transfers - Operations of transfer controller + * @tags transfers - Operations of transfer controller * @security JWT * @param {integer} take.query - How many transfers the endpoint should return * @param {integer} skip.query - How many transfers should be skipped (for pagination) - * @returns {Array.} 200 - All existing transfers - * @returns {string} 500 - Internal server error + * @return {Array.} 200 - All existing transfers + * @return {string} 500 - Internal server error */ public async returnAllTransfers(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -118,15 +118,15 @@ export default class TransferController extends BaseController { } /** - * Returns the requested transfer - * @route GET /transfers/{id} + * GET /transfers/{id} + * @summary Returns the requested transfer * @operationId getSingleTransfer - * @group transfers - Operations of transfer controller + * @tags transfers - Operations of transfer controller * @param {integer} id.path.required - The id of the transfer which should be returned * @security JWT - * @returns {TransferResponse.model} 200 - The requested transfer entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {TransferResponse} 200 - The requested transfer entity + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async returnTransfer(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -147,16 +147,16 @@ export default class TransferController extends BaseController { } /** - * Post a new transfer. - * @route POST /transfers + * POST /transfers + * @summary Post a new transfer. * @operationId createTransfer - * @group transfers - Operations of transfer controller - * @param {TransferRequest.model} transfer.body.required + * @tags transfers - Operations of transfer controller + * @param {TransferRequest} request.body.required * - The transfer which should be created * @security JWT - * @returns {TransferResponse.model} 200 - The created transfer entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {TransferResponse} 200 - The created transfer entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async postTransfer(req: RequestWithToken, res: Response) : Promise { const request = req.body as TransferRequest; diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index 9a28c9289..94dfee57e 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -292,7 +292,7 @@ export default class UserController extends BaseController { * 'organ' if user is connected to User via organ * 'own' if user is connected to User * @param req - * @returns whether User is connected to used token + * @return whether User is connected to used token */ static getRelation(req: RequestWithToken): string { if (userTokenInOrgan(req, asNumber(req.params.id))) return 'organ'; @@ -311,10 +311,10 @@ export default class UserController extends BaseController { } /** - * Get a list of all users - * @route GET /users + * GET /users + * @summary Get a list of all users * @operationId getAllUsers - * @group users - Operations of user controller + * @tags users - Operations of user controller * @security JWT * @param {integer} take.query - How many users the endpoint should return * @param {integer} skip.query - How many users should be skipped (for pagination) @@ -322,8 +322,8 @@ export default class UserController extends BaseController { * @param {boolean} active.query - Filter based if the user is active * @param {boolean} ofAge.query - Filter based if the user is 18+ * @param {integer} id.query - Filter based on user ID - * @param {string} type.query.enum{MEMBER,ORGAN,VOUCHER,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE} - Filter based on user type. - * @returns {PaginatedUserResponse.model} 200 - A list of all users + * @param {string} type.query - enum:MEMBER,ORGAN,VOUCHER,LOCAL_USER,LOCAL_ADMIN,INVOICE,AUTOMATIC_INVOICE - Filter based on user type. + * @return {PaginatedUserResponse} 200 - A list of all users */ public async getAllUsers(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all users by user', req.token.user); @@ -351,16 +351,16 @@ export default class UserController extends BaseController { } /** - * Get all users of user type - * @route GET /users/usertype/{userType} + * GET /users/usertype/{userType} + * @summary Get all users of user type * @operationId getAllUsersOfUserType - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {string} userType.path.required - The userType of the requested users * @security JWT * @param {integer} take.query - How many users the endpoint should return * @param {integer} skip.query - How many users should be skipped (for pagination) - * @returns {PaginatedUserResponse.model} 200 - A list of all users - * @returns {string} 404 - Nonexistent usertype + * @return {PaginatedUserResponse} 200 - A list of all users + * @return {string} 404 - Nonexistent usertype */ public async getAllUsersOfUserType(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -384,17 +384,17 @@ export default class UserController extends BaseController { } /** - * Put an users pin code - * @route PUT /users/{id}/authenticator/pin + * PUT /users/{id}/authenticator/pin + * @summary Put an users pin code * @operationId updateUserPin - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user - * @param {UpdatePinRequest.model} update.body.required - + * @param {UpdatePinRequest} request.body.required - * The PIN code to update to * @security JWT - * @returns 200 - Update success - * @returns {string} 400 - Validation Error - * @returns {string} 404 - Nonexistent user id + * @return 200 - Update success + * @return {string} 400 - Validation Error + * @return {string} 404 - Nonexistent user id */ public async updateUserPin(req: RequestWithToken, res: Response): Promise { const { params } = req; @@ -426,17 +426,17 @@ export default class UserController extends BaseController { } /** - * Put a users NFC code - * @route PUT /users/{id}/authenticator/nfc + * PUT /users/{id}/authenticator/nfc + * @summary Put a users NFC code * @operationId updateUserNfc - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user - * @param {UpdateNfcRequest.model} update.body.required - + * @param {UpdateNfcRequest} request.body.required - * The NFC code to update to * @security JWT - * @returns 200 - Update success - * @returns {string} 400 - Validation Error - * @returns {string} 404 - Nonexistent user id + * @return 200 - Update success + * @return {string} 400 - Validation Error + * @return {string} 404 - Nonexistent user id */ public async updateUserNfc(req: RequestWithToken, res: Response): Promise { const { params } = req; @@ -468,16 +468,16 @@ export default class UserController extends BaseController { } /** - * Delete a nfc code - * @route DELETE /users/{id}/authenticator/nfc + * DELETE /users/{id}/authenticator/nfc + * @summary Delete a nfc code * @operationId deleteUserNfc - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @security JWT - * @returns 200 - Delete nfc success - * @returns {string} 400 - Validation Error - * @returns {string} 403 - Nonexistent user nfc - * @returns {string} 404 - Nonexistent user id + * @return 200 - Delete nfc success + * @return {string} 400 - Validation Error + * @return {string} 403 - Nonexistent user nfc + * @return {string} 404 - Nonexistent user id */ public async deleteUserNfc(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -506,15 +506,15 @@ export default class UserController extends BaseController { } /** - * POST an users update to new key code - * @route POST /users/{id}/authenticator/key + * POST /users/{id}/authenticator/key + * @summary POST an users update to new key code * @operationId updateUserKey - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @security JWT - * @returns {UpdateKeyResponse.model} 200 - The new key - * @returns {string} 400 - Validation Error - * @returns {string} 404 - Nonexistent user id + * @return {UpdateKeyResponse} 200 - The new key + * @return {string} 400 - Validation Error + * @return {string} 404 - Nonexistent user id */ public async updateUserKey(req: RequestWithToken, res: Response): Promise { const { params } = req; @@ -542,15 +542,15 @@ export default class UserController extends BaseController { } /** - * Delete a users key code - * @route Delete /users/{id}/authenticator/key + * Delete /users/{id}/authenticator/key + * @summary Delete a users key code * @operationId deleteUserKey - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @security JWT - * @returns 200 - Deletion succesfull - * @returns {string} 400 - Validation Error - * @returns {string} 404 - Nonexistent user id + * @return 200 - Deletion succesfull + * @return {string} 400 - Validation Error + * @return {string} 404 - Nonexistent user id */ public async deleteUserKey(req: RequestWithToken, res: Response): Promise { const { params } = req; @@ -575,17 +575,17 @@ export default class UserController extends BaseController { } /** - * Put a user's local password - * @route PUT /users/{id}/authenticator/local + * PUT /users/{id}/authenticator/local + * @summary Put a user's local password * @operationId updateUserLocalPassword - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user - * @param {UpdateLocalRequest.model} update.body.required - + * @param {UpdateLocalRequest} request.body.required - * The password update * @security JWT - * @returns 204 - Update success - * @returns {string} 400 - Validation Error - * @returns {string} 404 - Nonexistent user id + * @return 204 - Update success + * @return {string} 400 - Validation Error + * @return {string} 404 - Nonexistent user id */ public async updateUserLocalPassword(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -618,17 +618,17 @@ export default class UserController extends BaseController { } /** - * Get an organs members - * @route GET /users/{id}/members + * GET /users/{id}/members + * @summary Get an organs members * @operationId getOrganMembers - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @param {integer} take.query - How many members the endpoint should return * @param {integer} skip.query - How many members should be skipped (for pagination) * @security JWT - * @returns {PaginatedUserResponse.model} 200 - All members of the organ - * @returns {string} 404 - Nonexistent user id - * @returns {string} 400 - User is not an organ + * @return {PaginatedUserResponse} 200 - All members of the organ + * @return {string} 404 - Nonexistent user id + * @return {string} 400 - User is not an organ */ public async getOrganMembers(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -669,14 +669,14 @@ export default class UserController extends BaseController { } /** - * Get an individual user - * @route GET /users/{id} + * GET /users/{id} + * @summary Get an individual user * @operationId getIndividualUser - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - userID * @security JWT - * @returns {UserResponse.model} 200 - Individual user - * @returns {string} 404 - Nonexistent user id + * @return {UserResponse} 200 - Individual user + * @return {string} 404 - Nonexistent user id */ public async getIndividualUser(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -699,15 +699,15 @@ export default class UserController extends BaseController { } /** - * Create a new user - * @route POST /users + * POST /users + * @summary Create a new user * @operationId createUser - * @group users - Operations of user controller - * @param {CreateUserRequest.model} user.body.required - + * @tags users - Operations of user controller + * @param {CreateUserRequest} request.body.required - * The user which should be created * @security JWT - * @returns {User.model} 200 - New user - * @returns {string} 400 - Bad request + * @return {UserResponse} 200 - New user + * @return {string} 400 - Bad request */ // eslint-disable-next-line class-methods-use-this public async createUser(req: RequestWithToken, res: Response): Promise { @@ -729,18 +729,16 @@ export default class UserController extends BaseController { } } - // TODO make a specification that can handle undefined attributes. /** - * Update a user - * @route PATCH /users/{id} + * PATCH /users/{id} + * @summary Update a user * @operationId updateUser - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user - * @param {UpdateUserRequest.model} user.body.required - - * The user which should be updated + * @param {UpdateUserRequest} request.body.required - The user which should be updated * @security JWT - * @returns {UpdateUserRequest.model} 200 - New user - * @returns {string} 400 - Bad request + * @return {UserResponse} 200 - New user + * @return {string} 400 - Bad request */ public async updateUser(req: RequestWithToken, res: Response): Promise { const body = req.body as UpdateUserRequest; @@ -789,14 +787,14 @@ export default class UserController extends BaseController { } /** - * Delete a single user - * @route DELETE /users/{id} + * DELETE /users/{id} + * @summary Delete a single user * @operationId deleteUser - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @security JWT - * @returns {string} 204 - User successfully deleted - * @returns {string} 400 - Cannot delete yourself + * @return {string} 204 - User successfully deleted + * @return {string} 400 - Cannot delete yourself */ public async deleteUser(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -827,14 +825,14 @@ export default class UserController extends BaseController { } /** - * Accept the Terms of Service if you have not accepted it yet - * @route POST /users/acceptTos + * POST /users/acceptTos + * @summary Accept the Terms of Service if you have not accepted it yet * @operationId acceptTos - * @group users - Operations of the User controller - * @param {AcceptTosRequest.model} params.body.required - "Tosrequest body" + * @tags users - Operations of the User controller + * @param {AcceptTosRequest} request.body.required - "Tosrequest body" * @security JWT - * @returns {string} 204 - ToS accepted - * @returns {string} 400 - ToS already accepted + * @return {string} 204 - ToS accepted + * @return {string} 400 - ToS already accepted */ public async acceptToS(req: RequestWithToken, res: Response): Promise { this.logger.trace('Accept ToS for user', req.token.user); @@ -864,15 +862,15 @@ export default class UserController extends BaseController { } /** - * Get an user's products - * @route GET /users/{id}/products + * GET /users/{id}/products + * @summary Get an user's products * @operationId getUsersProducts - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @param {integer} take.query - How many products the endpoint should return * @param {integer} skip.query - How many products should be skipped (for pagination) * @security JWT - * @returns {PaginatedProductResponse.model} 200 - List of products. + * @return {PaginatedProductResponse} 200 - List of products. */ public async getUsersProducts(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -907,17 +905,17 @@ export default class UserController extends BaseController { } /** - * Returns the user's containers - * @route GET /users/{id}/containers + * GET /users/{id}/containers + * @summary Returns the user's containers * @operationId getUsersContainers - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @security JWT * @param {integer} take.query - How many containers the endpoint should return * @param {integer} skip.query - How many containers should be skipped (for pagination) - * @returns {PaginatedContainerResponse.model} 200 - All users updated containers - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {PaginatedContainerResponse} 200 - All users updated containers + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async getUsersContainers(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -954,17 +952,17 @@ export default class UserController extends BaseController { } /** - * Returns the user's Points of Sale - * @route GET /users/{id}/pointsofsale + * GET /users/{id}/pointsofsale + * @summary Returns the user's Points of Sale * @operationId getUsersPointsOfSale - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @param {integer} take.query - How many points of sale the endpoint should return * @param {integer} skip.query - How many points of sale should be skipped (for pagination) * @security JWT - * @returns {PaginatedPointOfSaleResponse.model} 200 - All users updated point of sales - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {PaginatedPointOfSaleResponse} 200 - All users updated point of sales + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async getUsersPointsOfSale(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -1001,10 +999,10 @@ export default class UserController extends BaseController { } /** - * Get an user's transactions (from, to or created) - * @route GET /users/{id}/transactions + * GET /users/{id}/transactions + * @summary Get an user's transactions (from, to or created) * @operationId getUsersTransactions - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user that should be involved * in all returned transactions * @param {integer} fromId.query - From-user for selected transactions @@ -1019,7 +1017,7 @@ export default class UserController extends BaseController { * @param {integer} take.query - How many transactions the endpoint should return * @param {integer} skip.query - How many transactions should be skipped (for pagination) * @security JWT - * @returns {PaginatedBaseTransactionResponse.model} 200 - List of transactions. + * @return {PaginatedBaseTransactionResponse} 200 - List of transactions. */ public async getUsersTransactions(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -1062,10 +1060,10 @@ export default class UserController extends BaseController { } /** - * Get an user's transfers - * @route GET /users/{id}/transfers + * GET /users/{id}/transfers + * @summary Get an user's transfers * @operationId getUsersTransfers - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user that should be involved * in all returned transfers * @param {integer} take.query - How many transfers the endpoint should return @@ -1074,7 +1072,7 @@ export default class UserController extends BaseController { * @param {integer} toId.query - To-user for selected transfers * @param {integer} id.query - ID of selected transfers * @security JWT - * @returns {PaginatedTransferResponse.model} 200 - List of transfers. + * @return {PaginatedTransferResponse} 200 - List of transfers. */ public async getUsersTransfers(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -1122,16 +1120,16 @@ export default class UserController extends BaseController { } /** - * Authenticate as another user - * @route POST /users/{id}/authenticate + * POST /users/{id}/authenticate + * @summary Authenticate as another user * @operationId authenticateAs - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user that should be authenticated as * @security JWT - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. - * @returns {string} 404 - User not found error. - * @returns {string} 403 - Authentication error. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. + * @return {string} 404 - User not found error. + * @return {string} 403 - Authentication error. */ public async authenticateAsUser(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -1173,14 +1171,14 @@ export default class UserController extends BaseController { } /** - * Get all users that the user can authenticate as - * @route GET /users/{id}/authenticate + * GET /users/{id}/authenticate + * @summary Get all users that the user can authenticate as * @operationId getUserAuthenticatable - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user to get authentications of * @security JWT - * @returns {string} 404 - User not found error. - * @returns {Array.} 200 - A list of all users the given ID can authenticate + * @return {string} 404 - User not found error. + * @return {Array.} 200 - A list of all users the given ID can authenticate */ public async getUserAuthenticatable(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -1207,14 +1205,14 @@ export default class UserController extends BaseController { } /** - * Get all roles assigned to the user. - * @route GET /users/{id}/roles + * GET /users/{id}/roles + * @summary Get all roles assigned to the user. * @operationId getUserRoles - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user to get the roles from * @security JWT - * @returns {Array.} 200 - The roles of the user - * @returns {string} 404 - User not found error. + * @return {Array.} 200 - The roles of the user + * @return {string} 404 - User not found error. */ public async getUserRoles(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -1241,16 +1239,16 @@ export default class UserController extends BaseController { } /** - * Get all financial mutations of a user. - * @route GET /users/{id}/financialmutations + * GET /users/{id}/financialmutations + * @summary Get all financial mutations of a user. * @operationId getUsersFinancialMutations - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user to get the mutations from * @param {integer} take.query - How many transactions the endpoint should return * @param {integer} skip.query - How many transactions should be skipped (for pagination) * @security JWT - * @returns {PaginatedFinancialMutationResponse.model} 200 - The financial mutations of the user - * @returns {string} 404 - User not found error. + * @return {PaginatedFinancialMutationResponse} 200 - The financial mutations of the user + * @return {string} 404 - User not found error. */ public async getUsersFinancialMutations(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -1286,14 +1284,14 @@ export default class UserController extends BaseController { } /** - * Get all deposits of a user that are still being processed by Stripe - * @route GET /users/{id}/deposits + * GET /users/{id}/deposits + * @summary Get all deposits of a user that are still being processed by Stripe * @operationId getUsersProcessingDeposits - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user to get the deposits from * @security JWT - * @returns {Array.} 200 - The processing deposits of a user - * @returns {string} 404 - User not found error. + * @return {Array.} 200 - The processing deposits of a user + * @return {string} 404 - User not found error. */ public async getUsersProcessingDeposits(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -1317,19 +1315,19 @@ export default class UserController extends BaseController { } /** - * Get transaction report for the given user - * @route GET /users/{id}/transactions/report + * GET /users/{id}/transactions/report + * @summary Get transaction report for the given user * @operationId getUsersTransactionsReport - * @group users - Operations of user controller + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user to get the transaction report from * @security JWT - * @returns {Array.} 200 - The transaction report of the user + * @return {Array.} 200 - The transaction report of the user * @param {string} fromDate.query - Start date for selected transactions (inclusive) * @param {string} tillDate.query - End date for selected transactions (exclusive) * @param {integer} fromId.query - From-user for selected transactions * @param {integer} toId.query - To-user for selected transactions * @param {boolean} exclusiveToId.query - If all sub-transactions should be to the toId user, default true - * @returns {string} 404 - User not found error. + * @return {string} 404 - User not found error. */ public async getUsersTransactionsReport(req: RequestWithToken, res: Response): Promise { const parameters = req.params; @@ -1366,15 +1364,15 @@ export default class UserController extends BaseController { } /** - * Waive all given user's fines - * @route POST /users/{id}/fines/waive - * @group users - Operations of user controller + * POST /users/{id}/fines/waive + * @summary Waive all given user's fines + * @tags users - Operations of user controller * @param {integer} id.path.required - The id of the user * @operationId waiveUserFines * @security JWT - * @returns {string} 204 - Success - * @returns {string} 400 - User has no fines. - * @returns {string} 404 - User not found error. + * @return {string} 204 - Success + * @return {string} 400 - User has no fines. + * @return {string} 404 - User not found error. */ public async waiveUserFines(req: RequestWithToken, res: Response): Promise { const { id: rawId } = req.params; diff --git a/src/controller/vat-group-controller.ts b/src/controller/vat-group-controller.ts index 29d13ca51..aa91db28c 100644 --- a/src/controller/vat-group-controller.ts +++ b/src/controller/vat-group-controller.ts @@ -87,10 +87,10 @@ export default class VatGroupController extends BaseController { } /** - * Get a list of all VAT groups - * @route GET /vatgroups + * GET /vatgroups + * @summary Get a list of all VAT groups * @operationId getAllVatGroups - * @group vatGroups - Operations of the VAT groups controller + * @tags vatGroups - Operations of the VAT groups controller * @security JWT * @param {integer} vatGroupId.query - ID of the VAT group * @param {string} name.query - Name of the VAT group @@ -98,7 +98,7 @@ export default class VatGroupController extends BaseController { * @param {boolean} deleted.query - Whether the VAT groups should be hidden if zero * @param {integer} take.query - How many transactions the endpoint should return * @param {integer} skip.query - How many transactions should be skipped (for pagination) - * @returns {PaginatedVatGroupResponse.model} 200 - A list of all VAT groups + * @return {PaginatedVatGroupResponse} 200 - A list of all VAT groups */ public async getAllVatGroups(req: RequestWithToken, res: Response): Promise { this.logger.trace('Get all VAT groups by user', req.token.user); @@ -128,15 +128,15 @@ export default class VatGroupController extends BaseController { } /** - * Returns the requested VAT group - * @route GET /vatgroups/{id} + * GET /vatgroups/{id} + * @summary Returns the requested VAT group * @operationId getSingleVatGroup - * @group vatGroups - Operations of the VAT groups controller + * @tags vatGroups - Operations of the VAT groups controller * @security JWT * @param {integer} id.path.required - The ID of the VAT group which should be returned - * @returns {VatGroup.model} 200 - The requested VAT group entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {VatGroupResponse} 200 - The requested VAT group entity + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async getSingleVatGroup(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -158,15 +158,15 @@ export default class VatGroupController extends BaseController { } /** - * Create a new VAT group - * @route POST /vatgroups + * POST /vatgroups + * @summary Create a new VAT group * @operationId createVatGroup - * @group vatGroups - Operations of the VAT group controller - * @param {VatGroupRequest.model} vatGroup.body.required - The VAT group which should be created + * @tags vatGroups - Operations of the VAT group controller + * @param {VatGroupRequest} request.body.required - The VAT group which should be created * @security JWT - * @returns {VatGroup.model} 200 - The created VAT group entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {VatGroupResponse} 200 - The created VAT group entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createVatGroup(req: RequestWithToken, res: Response): Promise { const body = req.body as VatGroupRequest; @@ -191,17 +191,17 @@ export default class VatGroupController extends BaseController { } /** - * Create a new VAT group - * @route PATCH /vatgroups/{id} + * PATCH /vatgroups/{id} + * @summary Create a new VAT group * @operationId updateVatGroup - * @group vatGroups - Operations of the VAT group controller + * @tags vatGroups - Operations of the VAT group controller * @param {integer} id.path.required - The ID of the VAT group which should be updated - * @param {UpdateVatGroupRequest.model} vatGroup.body.required - The VAT group information + * @param {UpdateVatGroupRequest} request.body.required - The VAT group information * @security JWT - * @returns {VatGroup.model} 200 - The created VAT group entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {VatGroupResponse} 200 - The created VAT group entity + * @return {string} 400 - Validation error + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async updateVatGroup(req: RequestWithToken, res: Response): Promise { const body = req.body as UpdateVatGroupRequest; @@ -239,14 +239,14 @@ export default class VatGroupController extends BaseController { } /** - * Get the VAT collections needed for VAT declarations - * @route GET /vatgroups/declaration + * GET /vatgroups/declaration + * @summary Get the VAT collections needed for VAT declarations * @operationId getVatDeclarationAmounts - * @group vatGroups - Operations of the VAT groups controller + * @tags vatGroups - Operations of the VAT groups controller * @security JWT * @param {number} year.query.required - Calendar year for VAT declarations * @param {string} period.query.required - Period for VAT declarations - * @returns {PaginatedVatGroupResponse.model} 200 - A list of all VAT groups with declarations + * @return {PaginatedVatGroupResponse} 200 - A list of all VAT groups with declarations */ public async getVatDeclarationAmounts(req: RequestWithToken, res: Response): Promise { let params; diff --git a/src/controller/voucher-group-controller.ts b/src/controller/voucher-group-controller.ts index cc04cd427..004492549 100644 --- a/src/controller/voucher-group-controller.ts +++ b/src/controller/voucher-group-controller.ts @@ -64,16 +64,16 @@ export default class VoucherGroupController extends BaseController { } /** - * Returns all existing voucher groups - * @route GET /vouchergroups + * GET /vouchergroups + * @summary Returns all existing voucher groups * @operationId getAllVouchergroups - * @group vouchergroups - Operations of voucher group controller + * @tags vouchergroups - Operations of voucher group controller * @security JWT * @param {integer} take.query - How many voucher groups the endpoint should return * @param {integer} skip.query - How many voucher groups should be skipped (for pagination) - * @returns {PaginatedVoucherGroupResponse.model} 200 - All existingvoucher + * @return {PaginatedVoucherGroupResponse} 200 - All existingvoucher * groups without users - * @returns {string} 500 - Internal server error + * @return {string} 500 - Internal server error */ public async getAllVoucherGroups(req: RequestWithToken, res: Response): Promise { const { body } = req; @@ -100,16 +100,16 @@ export default class VoucherGroupController extends BaseController { } /** - * Creates a new voucher group - * @route POST /vouchergroups + * POST /vouchergroups + * @summary Creates a new voucher group * @operationId createVouchergroup - * @group vouchergroups - Operations of voucher group controller - * @param {VoucherGroupRequest.model} vouchergroup.body.required - + * @tags vouchergroups - Operations of voucher group controller + * @param {VoucherGroupRequest} request.body.required - * The voucher group which should be created * @security JWT - * @returns {VoucherGroupResponse.model} 200 - The created voucher group entity - * @returns {string} 400 - Validation error - * @returns {string} 500 - Internal server error + * @return {VoucherGroupResponse} 200 - The created voucher group entity + * @return {string} 400 - Validation error + * @return {string} 500 - Internal server error */ public async createVoucherGroup(req: RequestWithToken, res: Response): Promise { const body = req.body as VoucherGroupRequest; @@ -131,15 +131,15 @@ export default class VoucherGroupController extends BaseController { } /** - * Returns the requested voucher group - * @route GET /vouchergroups/{id} + * GET /vouchergroups/{id} + * @summary Returns the requested voucher group * @operationId getVouchergroupId - * @group vouchergroups - Operations of voucher group controller + * @tags vouchergroups - Operations of voucher group controller * @param {integer} id.path.required - The id of the voucher group which should be returned * @security JWT - * @returns {VoucherGroupResponse.model} 200 - The requested voucher group entity - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {VoucherGroupResponse} 200 - The requested voucher group entity + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async getVoucherGroupById(req: RequestWithToken, res: Response): Promise { const { id } = req.params; @@ -161,18 +161,18 @@ export default class VoucherGroupController extends BaseController { } /** - * Updates the requested voucher group - * @route PATCH /vouchergroups/{id} + * PATCH /vouchergroups/{id} + * @summary Updates the requested voucher group * @operationId updateVoucherGroup - * @group vouchergroups - Operations of voucher group controller + * @tags vouchergroups - Operations of voucher group controller * @param {integer} id.path.required - The id of the voucher group which should be updated - * @param {VoucherGroupRequest.model} vouchergroup.body.required - + * @param {VoucherGroupRequest} request.body.required - * The updated voucher group * @security JWT - * @returns {VoucherGroupResponse.model} 200 - The requested voucher group entity - * @returns {string} 400 - Validation error - * @returns {string} 404 - Not found error - * @returns {string} 500 - Internal server error + * @return {VoucherGroupResponse} 200 - The requested voucher group entity + * @return {string} 400 - Validation error + * @return {string} 404 - Not found error + * @return {string} 500 - Internal server error */ public async updateVoucherGroup(req: RequestWithToken, res: Response): Promise { const body = req.body as VoucherGroupRequest; diff --git a/src/declaration/swagger-model-validator/index.d.ts b/src/declaration/swagger-model-validator/index.d.ts index fe7223fca..1975254ab 100644 --- a/src/declaration/swagger-model-validator/index.d.ts +++ b/src/declaration/swagger-model-validator/index.d.ts @@ -29,7 +29,10 @@ declare module 'swagger-model-validator' { } export interface SwaggerSpecification { - definitions: any; + test? : string + components?: { + schemas: any + }; validateModel( modelName: string, object: object, diff --git a/src/gewis/controller/gewis-authentication-controller.ts b/src/gewis/controller/gewis-authentication-controller.ts index 700bc1aaf..02c6bfac6 100644 --- a/src/gewis/controller/gewis-authentication-controller.ts +++ b/src/gewis/controller/gewis-authentication-controller.ts @@ -103,16 +103,16 @@ export default class GewisAuthenticationController extends BaseController { } /** - * GEWIS login verification based on gewisweb JWT tokens. + * POST /authentication/gewisweb + * @summary GEWIS login verification based on gewisweb JWT tokens. * This method verifies the validity of the gewisweb JWT token, and returns a SudoSOS * token if the GEWIS token is valid. - * @route POST /authentication/gewisweb * @operationId gewisWebAuthentication - * @group authenticate - Operations of authentication controller - * @param {GewiswebAuthenticationRequest.model} req.body.required - The mock login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {MessageResponse.model} 403 - The created json web token. - * @returns {string} 400 - Validation error. + * @tags authenticate - Operations of authentication controller + * @param {GewiswebAuthenticationRequest} request.body.required - The mock login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {MessageResponse} 403 - The created json web token. + * @return {string} 400 - Validation error. */ public async gewiswebLogin(req: Request, res: Response): Promise { const body = req.body as GewiswebAuthenticationRequest; @@ -166,15 +166,15 @@ export default class GewisAuthenticationController extends BaseController { } /** - * LDAP login and hand out token + * POST /authentication/GEWIS/LDAP + * @summary LDAP login and hand out token * If user has never signed in before this also creates an GEWIS account. - * @route POST /authentication/GEWIS/LDAP * @operationId gewisLDAPAuthentication - * @group authenticate - Operations of authentication controller - * @param {AuthenticationLDAPRequest.model} req.body.required - The LDAP login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {AuthenticationLDAPRequest} request.body.required - The LDAP login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. + * @return {string} 403 - Authentication error. */ public async ldapLogin(req: Request, res: Response): Promise { const body = req.body as AuthenticationLDAPRequest; @@ -190,14 +190,14 @@ export default class GewisAuthenticationController extends BaseController { } /** - * PIN login and hand out token. - * @route POST /authentication/GEWIS/pin + * POST /authentication/GEWIS/pin + * @summary PIN login and hand out token. * @operationId gewisPinAuthentication - * @group authenticate - Operations of authentication controller - * @param {GEWISAuthenticationPinRequest.model} req.body.required - The PIN login. - * @returns {AuthenticationResponse.model} 200 - The created json web token. - * @returns {string} 400 - Validation error. - * @returns {string} 403 - Authentication error. + * @tags authenticate - Operations of authentication controller + * @param {GEWISAuthenticationPinRequest} request.body.required - The PIN login. + * @return {AuthenticationResponse} 200 - The created json web token. + * @return {string} 400 - Validation error. + * @return {string} 403 - Authentication error. */ public async gewisPINLogin(req: Request, res: Response): Promise { const { pin, gewisId } = req.body as GEWISAuthenticationPinRequest; diff --git a/src/gewis/controller/request/gewis-authentication-pin-request.ts b/src/gewis/controller/request/gewis-authentication-pin-request.ts index 073ff59b1..989128c46 100644 --- a/src/gewis/controller/request/gewis-authentication-pin-request.ts +++ b/src/gewis/controller/request/gewis-authentication-pin-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef GEWISAuthenticationPinRequest + * @typedef {object} GEWISAuthenticationPinRequest * @property {number} gewisId.required * @property {string} pin.required */ diff --git a/src/gewis/controller/request/gewisweb-authentication-request.ts b/src/gewis/controller/request/gewisweb-authentication-request.ts index 2676a7b60..90570f745 100644 --- a/src/gewis/controller/request/gewisweb-authentication-request.ts +++ b/src/gewis/controller/request/gewisweb-authentication-request.ts @@ -17,7 +17,7 @@ */ /** - * @typedef GewiswebAuthenticationRequest + * @typedef {object} GewiswebAuthenticationRequest * @property {string} token.required The gewisweb JWT token. * @property {string} nonce.required The nonce used in the newly signed JWT token. */ diff --git a/src/gewis/entity/gewis-user-response.ts b/src/gewis/controller/response/gewis-user-response.ts similarity index 87% rename from src/gewis/entity/gewis-user-response.ts rename to src/gewis/controller/response/gewis-user-response.ts index e22a4c536..405936ee3 100644 --- a/src/gewis/entity/gewis-user-response.ts +++ b/src/gewis/controller/response/gewis-user-response.ts @@ -15,10 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { UserResponse } from '../../controller/response/user-response'; +import { UserResponse } from '../../../controller/response/user-response'; /** - * @typedef {UserResponse} GewisUserResponse + * @typedef {allOf|UserResponse} GewisUserResponse * @property {integer} gewisId - The m-Number of the user */ export interface GewisUserResponse extends UserResponse { diff --git a/src/gewis/gewis.ts b/src/gewis/gewis.ts index 144e47a34..547945b83 100644 --- a/src/gewis/gewis.ts +++ b/src/gewis/gewis.ts @@ -26,7 +26,7 @@ import { bindUser, LDAPUser } from '../helpers/ad'; import GewiswebToken from './gewisweb-token'; import { parseRawUserToResponse, RawUser } from '../helpers/revision-to-response'; import Bindings from '../helpers/bindings'; -import { GewisUserResponse } from './entity/gewis-user-response'; +import { GewisUserResponse } from './controller/response/gewis-user-response'; export interface RawGewisUser extends RawUser { gewisId: number diff --git a/src/helpers/pagination.ts b/src/helpers/pagination.ts index 6e6916894..c521a4736 100644 --- a/src/helpers/pagination.ts +++ b/src/helpers/pagination.ts @@ -32,7 +32,7 @@ export interface PaginationParameters { } /** - * @typedef PaginationResult + * @typedef {object} PaginationResult * @property {integer} take.required Number of records queried * @property {integer} skip.required Number of skipped records * @property {integer} count.required Total number of resulting records diff --git a/src/index.ts b/src/index.ts index c4da42c72..d0ea49337 100644 --- a/src/index.ts +++ b/src/index.ts @@ -300,7 +300,6 @@ export default async function createApp(): Promise { application.app.use('/v1/files', new SimpleFileController(options).getRouter()); application.app.use('/v1/test', new TestController(options).getRouter()); } - // Start express application. logger.info(`Server listening on port ${process.env.HTTP_PORT}.`); application.server = application.app.listen(process.env.HTTP_PORT); diff --git a/src/middleware/request-validator-middleware.ts b/src/middleware/request-validator-middleware.ts index 04a7d90f0..d5953047a 100644 --- a/src/middleware/request-validator-middleware.ts +++ b/src/middleware/request-validator-middleware.ts @@ -37,13 +37,17 @@ export default class RequestValidatorMiddleware { /** * Creates a new request model validator middleware instance. + * @param specification - the swagger specification. * @param validator - the validator properties. */ public constructor(specification: SwaggerSpecification, validator: BodyValidator) { this.specification = specification; this.validator = validator; - if (!specification.definitions[validator.modelName]) { + // Edge case if there are no models. + if (!specification.components?.schemas) throw new Error(`Model '${validator.modelName}' not defined.`); + + if (!specification.components.schemas[validator.modelName]) { throw new Error(`Model '${validator.modelName}' not defined.`); } } diff --git a/src/service/container-service.ts b/src/service/container-service.ts index 012962ec2..8eba3a91b 100644 --- a/src/service/container-service.ts +++ b/src/service/container-service.ts @@ -209,7 +209,7 @@ export default class ContainerService { revision: response.products_revision, alcoholpercentage: response.products_alcoholPercentage, vat_id: response.products_vatId, - vat_hidden: response.vat_hidden, + vat_hidden: !!response.vat_hidden, vat_percentage: response.vat_percentage, category_id: response.products_categoryId, category_name: response.category_name, diff --git a/src/service/product-service.ts b/src/service/product-service.ts index b98905d4d..7433aa8dd 100644 --- a/src/service/product-service.ts +++ b/src/service/product-service.ts @@ -153,7 +153,7 @@ export default class ProductService { const vat: BaseVatGroupResponse = { id: rawProduct.vat_id, percentage: rawProduct.vat_percentage, - hidden: rawProduct.vat_hidden, + hidden: !!rawProduct.vat_hidden, }; return { diff --git a/src/service/transfer-service.ts b/src/service/transfer-service.ts index 74455c977..1a0b0ab7a 100644 --- a/src/service/transfer-service.ts +++ b/src/service/transfer-service.ts @@ -125,8 +125,8 @@ export default class TransferService { relations: ['from', 'to', 'invoice', 'invoice.invoiceStatus', 'deposit', 'deposit.depositStatus', - 'payoutRequest', 'payoutRequest.payoutRequestStatus', - 'fine', 'fine.userFineGroup', + 'payoutRequest', 'payoutRequest.payoutRequestStatus', 'payoutRequest.requestedBy', + 'fine', 'fine.userFineGroup', 'fine.userFineGroup.user', 'waivedFines', 'waivedFines.fines', 'waivedFines.fines.userFineGroup', ], take, diff --git a/src/start/swagger.ts b/src/start/swagger.ts index 1cfe5242d..84cc3b72a 100644 --- a/src/start/swagger.ts +++ b/src/start/swagger.ts @@ -15,53 +15,79 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -/* eslint-disable no-new */ import { promises as fs } from 'fs'; import * as path from 'path'; import express from 'express'; import swaggerUi from 'express-swaggerize-ui'; import Validator, { SwaggerSpecification } from 'swagger-model-validator'; -import generateSpecAndMount from 'express-swagger-generator'; +import expressJSDocSwagger from 'express-jsdoc-swagger'; +import log4js, { Logger } from 'log4js'; export default class Swagger { + private static logger: Logger = log4js.getLogger('SwaggerGenerator'); + /** * Generate Swagger specification on-demand and serve it. * @param app - The express application to mount on. - * @param files - The files that need to be parsed. + * @param filesPattern - Glob pattern to find your jsdoc files * @returns The Swagger specification with model validator. */ - public static generateSpecification(app: express.Application, ...files: string[]) - : SwaggerSpecification { - const swagger = generateSpecAndMount(app); - const swaggerOptions = { - swaggerDefinition: { + public static generateSpecification(app: express.Application, filesPattern: string[]): Promise { + return new Promise((resolve, reject) => { + const options = { info: { - title: process.env.npm_package_name, - description: process.env.npm_package_description, - version: process.env.npm_package_version, + version: process.env.npm_package_version ? process.env.npm_package_version : 'v1.0.0', + title: process.env.npm_package_name ? process.env.npm_package_name : 'SudoSOS', + description: process.env.npm_package_description ? process.env.npm_package_description : 'SudoSOS', }, - host: process.env.API_HOST, - basePath: process.env.API_BASEPATH, - produces: [ - 'application/json', + 'schemes': [ + 'http', + 'https', + ], + servers: [ + { + url: `http://${process.env.API_HOST}${process.env.API_BASEPATH}`, + description: 'Development server', + }, ], - schemes: ['http', 'https'], - securityDefinitions: { + security: { JWT: { - type: 'apiKey', - in: 'header', - name: 'Authorization', - description: '', + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', }, }, - }, - basedir: __dirname, // app absolute path - files, - }; + baseDir: __dirname, + // Glob pattern to find your jsdoc files + filesPattern, + swaggerUIPath: '/api-docs', + exposeSwaggerUI: true, // Expose Swagger UI + exposeApiDocs: true, // Expose API Docs JSON + apiDocsPath: '/api-docs.json', + }; - const swaggerSpec = swagger(swaggerOptions) as SwaggerSpecification; - new Validator(swaggerSpec); - return swaggerSpec; + const instance = expressJSDocSwagger(app)(options); + + instance.on('finish', (swaggerObject) => { + Swagger.logger.trace('Swagger specification generation finished'); + new Validator(swaggerObject); + void fs.writeFile( + path.join(process.cwd(), 'out/swagger.json'), + JSON.stringify(swaggerObject), + { encoding: 'utf-8' }, + ).catch((e) => { + console.error(e); + }); + instance.removeAllListeners(); + resolve(swaggerObject); // Resolve the promise with the swaggerObject + }); + + instance.on('error', (error) => { + Swagger.logger.error('Error generating Swagger specification:', error); + instance.removeAllListeners(); + reject(error); // Reject the promise in case of an error + }); + }); } /** @@ -95,28 +121,22 @@ export default class Swagger { return specification; } - // Generate Swagger specification on-demand in development environments. - return Swagger.generateSpecification(app, - path.join(process.cwd(), 'src/entity/*.ts'), - path.join(process.cwd(), 'src/entity/**/*.ts'), - path.join(process.cwd(), 'src/gewis/entity/*.ts'), - path.join(process.cwd(), 'src/declaration/*.ts'), - path.join(process.cwd(), 'src/**/controller/*.ts'), - path.join(process.cwd(), 'src/**/controller/response/**/*.ts'), - path.join(process.cwd(), 'src/**/controller/request/**/*.ts'), - path.join(process.cwd(), 'src/**/helpers/pagination.ts')); + return Swagger.generateSpecification(app, [ + '../controller/*.ts', + '../helpers/pagination.ts', + + '../controller/request/*.ts', + '../controller/response/*.ts', + '../controller/response/**/*.ts', + '../gewis/controller/**/*.ts', + ]); } } if (require.main === module) { // Only execute directly if this is the main execution file. const app = express(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises + fs.mkdir('out', { recursive: true }) - .then(() => Swagger.initialize(app)) - .then((specification) => fs.writeFile( - path.join(process.cwd(), 'out/swagger.json'), - JSON.stringify(specification), - { encoding: 'utf-8' }, - )); + .then(async () => { await Swagger.initialize(app); }); } diff --git a/test/unit/controller/container-controller.ts b/test/unit/controller/container-controller.ts index 504ea67ab..9bff890f4 100644 --- a/test/unit/controller/container-controller.ts +++ b/test/unit/controller/container-controller.ts @@ -300,12 +300,13 @@ describe('ContainerController', async (): Promise => { // success code expect(res.status).to.equal(200); asRequested(container, res.body); - expect(ctx.specification.validateModel( + const valid = (ctx.specification.validateModel( 'ContainerWithProductsResponse', res.body, false, true, - ).valid).to.be.true; + )); + expect(valid.valid).to.be.true; } it('should return an HTTP 200 and the container with the given id if admin', async () => { const container = await Container.findOne({ where: { id: 1 }, relations: ['owner'] }); @@ -376,12 +377,13 @@ describe('ContainerController', async (): Promise => { .get('/containers/1/products') .set('Authorization', `Bearer ${ctx.adminToken}`); expect(res.status).to.equal(200); - expect(ctx.specification.validateModel( + const valid = ctx.specification.validateModel( 'PaginatedProductResponse', res.body, false, true, - ).valid).to.be.true; + ); + expect(valid.valid).to.be.true; }); it('should return an HTTP 200 and all the products in the container if admin', async () => { const res = await request(ctx.app) diff --git a/test/unit/controller/transaction-controller.ts b/test/unit/controller/transaction-controller.ts index b51bc6f85..099f704e4 100644 --- a/test/unit/controller/transaction-controller.ts +++ b/test/unit/controller/transaction-controller.ts @@ -36,6 +36,8 @@ import { TransactionRequest } from '../../../src/controller/request/transaction- import { defaultPagination, PAGINATION_DEFAULT, PaginationResult } from '../../../src/helpers/pagination'; import { inUserContext, UserFactory } from '../../helpers/user-factory'; import MemberAuthenticator from '../../../src/entity/authenticator/member-authenticator'; +import {tr} from "date-fns/locale"; +import {validDate} from "../../../src/controller/request/validators/duration-spec"; describe('TransactionController', (): void => { let ctx: { @@ -579,12 +581,14 @@ describe('TransactionController', (): void => { .get(`/transactions/${trans.id}`) .set('Authorization', `Bearer ${ctx.organMemberToken}`); expect(res.status).to.equal(200); - expect(ctx.specification.validateModel( + // TODO Fix disallowExtraProperties to be `true` + // See https://github.com/GEWIS/sudosos-backend/issues/117 + const valid = ctx.specification.validateModel( 'TransactionResponse', res.body, false, - true, - ).valid).to.be.true; + false); + expect(valid.valid).to.be.true; }); it('should return HTTP 403 if not admin and not connected via organ', async () => { const trans = await Transaction.findOne({ relations: ['from'], where: { from: { id: ctx.users[3].id } } }); @@ -607,7 +611,7 @@ describe('TransactionController', (): void => { 'TransactionResponse', res.body, false, - true, + false, ).valid).to.be.true; }); it('should return an HTTP 403 if user is not connected to createdBy via organ', async () => { @@ -714,7 +718,7 @@ describe('TransactionController', (): void => { 'TransactionResponse', res.body, false, - true, + false, ).valid).to.be.true; expect(res.body).to.not.eql(toUpdate); @@ -779,7 +783,7 @@ describe('TransactionController', (): void => { 'TransactionResponse', res.body, false, - true, + false, ).valid).to.be.true; expect(res.body).to.eql(deletedTransaction); diff --git a/test/unit/controller/user-controller.ts b/test/unit/controller/user-controller.ts index c95fee741..c019a5ba7 100644 --- a/test/unit/controller/user-controller.ts +++ b/test/unit/controller/user-controller.ts @@ -1189,11 +1189,13 @@ describe('UserController', (): void => { .set('Authorization', `Bearer ${ctx.adminToken}`) .query(parameters); expect(res.status).to.equal(200); + // TODO Fix disallowExtraProperties to be `true` + // See https://github.com/GEWIS/sudosos-backend/issues/117 const validation = ctx.specification.validateModel( 'TransactionReportResponse', res.body, false, - true, + false, ); expect(validation.valid).to.be.true; }); @@ -1339,7 +1341,7 @@ describe('UserController', (): void => { 'PaginatedFinancialMutationResponse', res.body, false, - true, + false, ).valid).to.be.true; }); it('should adhere to pagination', async () => { @@ -1959,7 +1961,7 @@ describe('UserController', (): void => { 'StripeDepositResponse', b, false, - true, + false, ); expect(validation.valid).to.be.true; }); diff --git a/test/unit/entity/transformer/test-model.ts b/test/unit/entity/transformer/test-model.ts index 18a430b54..bd5ed85cc 100644 --- a/test/unit/entity/transformer/test-model.ts +++ b/test/unit/entity/transformer/test-model.ts @@ -18,10 +18,9 @@ import * as express from 'express'; import { SwaggerSpecification } from 'swagger-model-validator'; import Swagger from '../../../../src/start/swagger'; -import { sourceFile } from '../../../setup'; /** - * @typedef TestModel + * @typedef {object} TestModel * @property {string} name.required - The name of the model. * @property {number} value.required - A test value. */ @@ -32,5 +31,5 @@ export class TestModel { } export async function getSpecification(app: express.Application): Promise { - return Swagger.generateSpecification(app, sourceFile(__filename)); + return Swagger.generateSpecification(app, ['../../test/unit/entity/transformer/test-model.ts']); } diff --git a/test/unit/gewis/gewis.ts b/test/unit/gewis/gewis.ts index ae800e9c7..bbd0f7192 100644 --- a/test/unit/gewis/gewis.ts +++ b/test/unit/gewis/gewis.ts @@ -39,7 +39,7 @@ import RoleManager from '../../../src/rbac/role-manager'; import UserController from '../../../src/controller/user-controller'; import TokenMiddleware from '../../../src/middleware/token-middleware'; import { PaginatedUserResponse } from '../../../src/controller/response/user-response'; -import { GewisUserResponse } from '../../../src/gewis/entity/gewis-user-response'; +import { GewisUserResponse } from '../../../src/gewis/controller/response/gewis-user-response'; describe('GEWIS Helper functions', async (): Promise => { let ctx: { diff --git a/test/unit/swagger.ts b/test/unit/swagger.ts index bba05cfbe..db042eee3 100644 --- a/test/unit/swagger.ts +++ b/test/unit/swagger.ts @@ -42,7 +42,7 @@ describe('Swagger', (): void => { const app = express(); const specification = await Swagger.initialize(app); - expect(specification.definitions).to.exist; + expect(specification.components.schemas).to.exist; const res = await request(app) .get('/api-docs.json'); expect(res.status).to.equal(200); @@ -56,7 +56,7 @@ describe('Swagger', (): void => { const app = express(); const specification = await Swagger.initialize(app); - expect(specification.definitions).to.exist; + expect(specification.components.schemas).to.exist; const res = await request(app) .get('/api-docs.json'); expect(res.status).to.equal(200); From 7015c5ca66d8cc2d6c19638d4998f5ef54fdabfd Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 2 Jan 2024 15:16:41 +0100 Subject: [PATCH 13/13] Fixed bug in swagger spec for the fines eligible --- src/controller/debtor-controller.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controller/debtor-controller.ts b/src/controller/debtor-controller.ts index f2cd05a2c..9debeadcd 100644 --- a/src/controller/debtor-controller.ts +++ b/src/controller/debtor-controller.ts @@ -168,15 +168,15 @@ export default class DebtorController extends BaseController { } /** - * Return all users that had at most -5 euros balance both now and on the reference date - * For all these users, also return their fine based on the reference date. - * @route GET /fines/eligible + * GET /fines/eligible + * @summary Return all users that had at most -5 euros balance both now and on the reference date. + * For all these users, also return their fine based on the reference date. * @tags debtors - Operations of the debtor controller * @operationId calculateFines * @security JWT - * @param {Array.} userTypes[].query - List of all user types fines should be calculated for - * @param {Array.} referenceDates[].query.required - Dates to base the fines on. Every returned user has at - * least five euros debt on every reference date. The height of the fine is based on the first date in the array. + * @param {Array} userTypes.query - List of all user types fines should be calculated for 1 (MEMBER), 2 (ORGAN), 3 (VOUCHER), 4 (LOCAL_USER), 5 (LOCAL_ADMIN), 6 (INVOICE), 7 (AUTOMATIC_INVOICE). + * @param {Array} referenceDates.query.required - Dates to base the fines on. Every returned user has at + * least five euros debt on every reference date. The height of the fine is based on the first date in the array. * @return {Array} 200 - List of eligible fines * @return {string} 400 - Validation error * @return {string} 500 - Internal server error