diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 7ec85be3..6d7c3c31 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -149,7 +149,7 @@ jobs: docker container restart mongodb - name: Run tests - run: yarn test --ci + run: CI=true yarn test - name: Build sources run: yarn build-release --noEmit diff --git a/.gitignore b/.gitignore index 2949f9cd..7970b779 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ yarn-error.log* lerna-debug.log* .DS_Store +# test profiling data +profiling + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 3d769e4e..fa9c1e18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -77,39 +77,16 @@ "console": "integratedTerminal" }, { - "name": "Debug Jest Tests", + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "type": "node", "request": "launch", - "env": { - "NODE_OPTIONS": "--experimental-vm-modules" - }, - "runtimeArgs": [ - "--inspect-brk", - "${workspaceRoot}/node_modules/.bin/jest", - "--runInBand", - "history" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" - }, - { - "type": "node", - "name": "vscode-jest-tests.v2", - "request": "launch", - "env": { - "NODE_OPTIONS": "--experimental-vm-modules" - }, - "args": [ - "${workspaceRoot}/node_modules/.bin/jest", - "--runInBand", - "--watchAll=false", - "--testNamePattern", - "${jest.testNamePattern}", - "--runTestsByPath", - "${jest.testFile}" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" + "name": "Debug Tests", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run"], + "smartStep": true, + "console": "integratedTerminal" } ] } \ No newline at end of file diff --git a/jest.config.cjs b/jest.config.cjs deleted file mode 100644 index 29335854..00000000 --- a/jest.config.cjs +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - testTimeout: 2 * 60 * 1000, - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1' - }, - extensionsToTreatAsEsm: ['.ts'], - transform: { - '^.+\\.(mt|t|cj|j)s$': [ - 'ts-jest', - { - useESM: true - } - ] - }, - testEnvironment: 'node', - testMatch: [ - '/**/__tests__/*.ts' - ] -} diff --git a/package.json b/package.json index eb9421e0..881b7f80 100644 --- a/package.json +++ b/package.json @@ -8,21 +8,17 @@ "license": "AGPL-3.0-or-later", "devDependencies": { "@types/auth0": "^3.3.2", - "@types/jest": "^29.4.0", "@types/node": "^18.13.0", "@types/supertest": "^2.0.12", "@types/underscore": "^1.11.4", "cross-env": "^7.0.3", "husky": "^8.0.1", - "jest": "^29.7.0", - "jest-extended": "^4.0.2", "mongodb-memory-server": "^10.1.2", "nock": "^13.3.0", "supertest": "^6.3.3", - "ts-jest": "^29.2.5", "ts-standard": "^12.0.0", "typescript": "4.9.5", - "wait-for-expect": "^3.0.2" + "vitest": "^3.1.2" }, "dependencies": { "@apollo/server": "^4.11.2", @@ -73,11 +69,11 @@ "scripts": { "lint": "yarn ts-standard", "fix": "yarn ts-standard --fix", - "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --runInBand", + "test": "LOG_LEVEL=silent vitest run --silent", "build": "tsc -p tsconfig.json", "build-release": "tsc -p tsconfig.release.json", "clean": "tsc -b --clean && rm -rf build/*", - "serve": "yarn build && node --experimental-json-modules build/main.js", + "serve": "yarn build && node --experimental-json-modules build/src/main.js", "serve-dev": "echo \"🚨 LOCAL_DEV_BYPASS_AUTH enabled 🚨\" && LOCAL_DEV_BYPASS_AUTH=true yarn serve", "seed-db": "./seed-db.sh", "add-countries": "yarn build && node build/db/utils/jobs/AddCountriesJob.js", @@ -103,7 +99,6 @@ "ignore": [ "build", "hacks", - "**/*.test.ts", "db-migrations" ] }, diff --git a/src/__tests__/areas.ts b/src/__tests__/areas.ts deleted file mode 100644 index aea41608..00000000 --- a/src/__tests__/areas.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { ApolloServer } from '@apollo/server' -import muuid from 'uuid-mongodb' -import { jest } from '@jest/globals' -import MutableAreaDataSource from '../model/MutableAreaDataSource.js' -import MutableMediaDataSource from '../model/MutableMediaDataSource.js' -import MutableOrganizationDataSource from '../model/MutableOrganizationDataSource.js' -import { MediaObjectGQLInput } from '../db/MediaObjectTypes.js' -import { AreaType } from '../db/AreaTypes.js' -import { OrganizationEditableFieldsType, OrganizationType, OrgType } from '../db/OrganizationTypes.js' -import { queryAPI, setUpServer } from '../utils/testUtils.js' -import { muuidToString } from '../utils/helpers.js' -import { InMemoryDB } from '../utils/inMemoryDB.js' -import express from 'express' - -jest.setTimeout(60000) - -describe('areas API', () => { - let server: ApolloServer - let user: muuid.MUUID - let userUuid: string - let app: express.Application - let inMemoryDB: InMemoryDB - - // Mongoose models for mocking pre-existing state. - let areas: MutableAreaDataSource - let organizations: MutableOrganizationDataSource - let usa: AreaType - let ca: AreaType - let wa: AreaType - - beforeAll(async () => { - ({ server, inMemoryDB, app } = await setUpServer()) - // Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format - // "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==". - user = muuid.mode('relaxed').v4() - userUuid = muuidToString(user) - }) - - beforeEach(async () => { - await inMemoryDB.clear() - areas = MutableAreaDataSource.getInstance() - organizations = MutableOrganizationDataSource.getInstance() - usa = await areas.addCountry('usa') - ca = await areas.addArea(user, 'CA', usa.metadata.area_id) - wa = await areas.addArea(user, 'WA', usa.metadata.area_id) - }) - - afterAll(async () => { - await server.stop() - await inMemoryDB.close() - }) - - async function insertMediaObjectsForArea (areaId: string, mediaCount: number): Promise { - const newMediaListInput: MediaObjectGQLInput[] = [] - for (let i = 0; i < mediaCount; i++) { - newMediaListInput.push({ - userUuid: 'a2eb6353-65d1-445f-912c-53c6301404bd', - width: 800, - height: 600, - format: 'jpeg', - size: 45000, - mediaUrl: `/areaPhoto${i}.jpg`, - entityTag: { - entityType: 1, - entityId: areaId - } - }) - } - - const media = MutableMediaDataSource.getInstance() - await media.addMediaObjects(newMediaListInput) - } - - describe('queries', () => { - const areaQuery = ` - query area($input: ID) { - area(uuid: $input) { - uuid - organizations { - orgId - } - } - } - ` - let alphaFields: OrganizationEditableFieldsType - let alphaOrg: OrganizationType - - beforeEach(async () => { - alphaFields = { - displayName: 'USA without CA Org', - associatedAreaIds: [usa.metadata.area_id], - excludedAreaIds: [ca.metadata.area_id] - } - alphaOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, alphaFields) - .then((res: OrganizationType | null) => { - if (res === null) throw new Error('Failure mocking organization.') - return res - }) - }) - - it('retrieves an area omitting organizations that exclude it', async () => { - const response = await queryAPI({ - query: areaQuery, - operationName: 'area', - variables: { input: ca.metadata.area_id }, - userUuid, - app - }) - expect(response.statusCode).toBe(200) - const areaResult = response.body.data.area - expect(areaResult.uuid).toBe(muuidToString(ca.metadata.area_id)) - // Even though alphaOrg associates with ca's parent, usa, it excludes - // ca and so should not be listed. - expect(areaResult.organizations).toHaveLength(0) - }) - - it.each([userUuid, undefined])('retrieves an area and lists associated organizations', async (userId) => { - const response = await queryAPI({ - query: areaQuery, - operationName: 'area', - variables: { input: wa.metadata.area_id }, - userUuid: userId, - app - }) - - expect(response.statusCode).toBe(200) - const areaResult = response.body.data.area - expect(areaResult.uuid).toBe(muuidToString(wa.metadata.area_id)) - expect(areaResult.organizations).toHaveLength(1) - expect(areaResult.organizations[0].orgId).toBe(muuidToString(alphaOrg.orgId)) - }) - }) - - it('returns paginated Media when requested', async () => { - await insertMediaObjectsForArea(usa.metadata.area_id.toString(), 11) - - const areaQueryWithPaginatedMedia = ` - query area($uuid: ID!, $input: EmbeddedAreaMediaInput) { - area(uuid: $uuid) { - mediaPagination(input: $input) { - areaUuid - mediaConnection { - edges { - node { - id - mediaUrl - } - cursor - } - pageInfo { - hasNextPage - totalItems - endCursor - } - } - } - } - } - ` - const response = await queryAPI({ - query: areaQueryWithPaginatedMedia, - operationName: 'area', - variables: { - uuid: usa.metadata.area_id, - input: { - first: 5, - after: null - } - }, - userUuid, - app - }) - expect(response.statusCode).toBe(200) - const areaResult = response.body.data.area - expect(areaResult.mediaPagination.mediaConnection.edges).toHaveLength(5) - expect(areaResult.mediaPagination.mediaConnection.pageInfo.totalItems).toBe(11) - expect(areaResult.mediaPagination.mediaConnection.pageInfo.hasNextPage).toBe(true) - }) -}) diff --git a/src/__tests__/bulkImport.test.ts b/src/__tests__/bulkImport.test.ts deleted file mode 100644 index be8a54c3..00000000 --- a/src/__tests__/bulkImport.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import {ApolloServer} from "@apollo/server"; -import muuid from "uuid-mongodb"; -import express from "express"; -import {InMemoryDB} from "../utils/inMemoryDB.js"; -import {queryAPI, setUpServer} from "../utils/testUtils.js"; -import {muuidToString} from "../utils/helpers.js"; -import exampleImportData from './import-example.json' assert {type: 'json'}; -import {AreaType} from "../db/AreaTypes.js"; -import {BulkImportResultType} from "../db/BulkImportTypes.js"; -import MutableClimbDataSource from "../model/MutableClimbDataSource.js"; -import BulkImportDataSource from "../model/BulkImportDataSource.js"; - -describe('bulkImportAreas', () => { - const query = ` - mutation bulkImportAreas($input: BulkImportInput!) { - bulkImportAreas(input: $input) { - addedAreas { - uuid - metadata { - area_id - } - } - updatedAreas { - uuid - metadata { - area_id - } - } - addedOrUpdatedClimbs { - id - } - } - } - ` - - let server: ApolloServer - let user: muuid.MUUID - let userUuid: string - let app: express.Application - let inMemoryDB: InMemoryDB - let testArea: AreaType - - let bulkImport: BulkImportDataSource - let climbs: MutableClimbDataSource - - beforeAll(async () => { - ({server, inMemoryDB, app} = await setUpServer()) - // Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format - // "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==". - user = muuid.mode('relaxed').v4() - userUuid = muuidToString(user) - bulkImport = BulkImportDataSource.getInstance() - climbs = MutableClimbDataSource.getInstance() - }) - - beforeEach(async () => { - await inMemoryDB.clear() - await bulkImport.addCountry('usa') - testArea = await bulkImport.addArea(user, "Test Area", null, "us") - }) - - afterAll(async () => { - await server.stop() - await inMemoryDB.close() - }) - - it('should return 403 if no user', async () => { - const res = await queryAPI({ - app, - query, - operationName: 'bulkImportAreas', - variables: {input: exampleImportData} - }) - expect(res.statusCode).toBe(200) - expect(res.body.errors[0].message).toBe('Not Authorised!') - }) - - it('should return 403 if user is not an editor', async () => { - const res = await queryAPI({ - app, - userUuid, - query, - operationName: 'bulkImportAreas', - variables: {input: exampleImportData} - }) - expect(res.statusCode).toBe(200) - expect(res.body.errors[0].message).toBe('Not Authorised!') - }) - - it('should return 200 if user is an editor', async () => { - const res = await queryAPI({ - app, - userUuid, - roles: ['editor'], - query, - operationName: 'bulkImportAreas', - variables: {input: exampleImportData} - }) - expect(res.status).toBe(200) - }) - - it('should import data', async () => { - const res = await queryAPI({ - app, - userUuid, - roles: ['editor'], - query, - operationName: 'bulkImportAreas', - variables: { - input: { - areas: [ - ...exampleImportData.areas, - { - uuid: testArea.metadata.area_id, - areaName: "Updated Test Area", - } - ] - } - } - }); - expect(res.body.errors).toBeFalsy() - - const result = res.body.data.bulkImportAreas as BulkImportResultType - expect(result.addedAreas.length).toBe(4) - - const committedAreas = await Promise.all(result.addedAreas.map((area) => bulkImport.findOneAreaByUUID(muuid.from(area.metadata.area_id)))); - expect(committedAreas.length).toBe(4); - - const committedClimbs = await Promise.all(result.addedOrUpdatedClimbs.map((climb) => climbs.findOneClimbByMUUID(climb._id))); - expect(committedClimbs.length).toBe(2); - - const updatedAreas = await Promise.all(result.updatedAreas.map((area) => bulkImport.findOneAreaByUUID(muuid.from(area.metadata.area_id)))); - expect(updatedAreas.length).toBe(1); - expect(updatedAreas[0].area_name).toBe("Updated Test Area"); - }) -}); \ No newline at end of file diff --git a/src/__tests__/fixtures/data.fixtures.ts b/src/__tests__/fixtures/data.fixtures.ts new file mode 100644 index 00000000..171820df --- /dev/null +++ b/src/__tests__/fixtures/data.fixtures.ts @@ -0,0 +1,244 @@ +import { + ClimbChangeInputType, + ClimbType, + DisciplineType +} from '../../db/ClimbTypes' +import { AreaType } from '../../db/AreaTypes' +import { dbTest } from './mongo.fixtures' +import muuid, { MUUID } from 'uuid-mongodb' +import { muuidToString } from '../../utils/helpers' +import isoCountries, { Alpha3Code } from 'i18n-iso-countries' +import { UserPublicProfile } from '../../db/UserTypes' +import { createGradeObject, gradeContextToGradeScales } from '../../GradeUtils' +import { getScale, GradeScalesTypes } from '@openbeta/sandbag' +import CountriesLngLat from '../../data/countries-with-lnglat.json' +import { TickStyle, TickAttemptType } from '../../db/TickTypes' + +export const allowableStyleMap: Record = { + Lead: ['Onsight', 'Flash', 'Redpoint', 'Pinkpoint', 'Attempt', 'Frenchfree'], + Solo: ['Onsight', 'Flash', 'Redpoint', 'Attempt'], + Boulder: ['Flash', 'Send', 'Attempt'], + TR: ['Send', 'Attempt'], + Follow: ['Send', 'Attempt'], + Aid: ['Send', 'Attempt'] +} + +export function choose (arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)] +} + +interface DbTestContext { + user: MUUID + userUuid: string + profile: UserPublicProfile + + addArea: ( + name?: string, + extra?: Partial<{ + leaf: boolean + boulder: boolean + parent: MUUID | AreaType + }>, + ) => Promise + countryCode: Alpha3Code + country: AreaType + area: AreaType + + addClimb: (props?: Partial) => Promise + climb: ClimbType + + gradeSystemFor: (climb: { type: DisciplineType }) => GradeScalesTypes + + /** + * Given the country that has been supplied to this test context, what would be a + * valid grade to generate for a given climb object + */ + randomGrade: (climb: ClimbType | { type: DisciplineType }) => string +} + +beforeAll(() => { + const availableCountries: Alpha3Code[] = Object.keys( + isoCountries.getAlpha3Codes() + ).filter((country) => CountriesLngLat[country]) as Alpha3Code[] + + for (const country of availableCountries) { + if (gradeContextToGradeScales[country] !== undefined) continue + gradeContextToGradeScales[country] = gradeContextToGradeScales.US + } +}) + +export const dataFixtures = dbTest.extend({ + user: async ({ task }, use) => await use(muuid.v4()), + userUuid: async ({ user }, use) => await use(muuidToString(user)), + profile: async ({ task, user, users, userUuid }, use) => { + await users.createOrUpdateUserProfile(user, { + userUuid, + username: task.id.replace('-', ''), + email: 'cat@example.com' + }) + + const profile = await users.getUserPublicProfileByUuid(user) + assert(profile != null) + await use(profile) + + await users.deleteFromCacheByFields({ username: task.id }) + }, + + countryCode: async ({ client, areas }, use) => { + // atomically fetch & reserve one unused country: + const res = await client.db() + .collection('test_countries') + .findOneAndUpdate( + { reserved: false }, + { $set: { reserved: true } }, + { returnDocument: 'after' } + ) + + if (res.value == null) throw new Error('no more country codes in the pool') + const code = res.value.code as Alpha3Code + + await use(code) + + // teardown: + // release this area back to the queue + await areas.areaModel.deleteOne({ area_name: isoCountries.getName(code, 'en') }) + await expect(areas.areaModel.find({ pathTokens: { $size: 1 } }).then(x => x.map(i => i.area_name))) + .not + .toContain(isoCountries.getName(code, 'en')) + await client.db() + .collection('test_countries') + .updateOne({ code }, { $set: { reserved: false } }) + }, + + country: async ({ areas, countryCode }, use) => { + const country = await areas.addCountry(countryCode) + assert(country !== null) + + assert(country.shortCode) + gradeContextToGradeScales[country.shortCode] = gradeContextToGradeScales.US + + await use(country) + + await areas.areaModel.deleteMany({ 'embeddedRelations.ancestors._id': country._id }) + await areas.areaModel.deleteOne({ _id: country._id }).orFail() + await expect(areas.areaModel.find({ pathTokens: { $size: 1 } }).then(x => x.map(i => i.area_name))) + .not + .toContain(country.area_name) + }, + + addArea: async ({ task, country, user, areas }, use) => { + async function addArea ( + name?: string, + extra?: Partial<{ + leaf: boolean + boulder: boolean + parent: MUUID | AreaType + }> + ): Promise { + function isArea (x: any): x is AreaType { + return typeof x.metadata?.area_id !== 'undefined' + } + + if (name === undefined || name === 'test') { + name = task.id + process.uptime().toString() + } + + let parent: MUUID | undefined + if (extra?.parent != null) { + if (isArea(extra.parent)) { + parent = extra.parent.metadata?.area_id + } else { + parent = extra.parent + } + } + + return await areas.addArea( + user, + name, + parent ?? country.metadata.area_id, + undefined, + undefined, + extra?.leaf, + extra?.boulder + ) + } + + await use(addArea) + + await areas.areaModel.deleteMany({ area_name: { $regex: `^${task.id}` } }) + }, + area: async ({ addArea }, use) => { + await use(await addArea()) + }, + + addClimb: async ({ climbs, area, task, user }, use) => { + async function addClimb ( + data?: Partial + ): Promise { + const [id] = await climbs.addOrUpdateClimbs(user, area.metadata.area_id, [ + { + name: task.id + process.uptime().toString(), + disciplines: data?.disciplines ?? { + sport: true + }, + description: 'A good warm up problem', + location: 'Start from the left arete', + protection: '2 bolts', + boltsCount: 2, + ...(data ?? {}) + } + ]) + + const climb = await climbs.findOneClimbByMUUID(muuid.from(id)) + assert(climb != null) + return climb + } + + await use(addClimb) + + await climbs.climbModel.deleteMany({ + name: { $regex: `^${task.id}` } + }) + }, + climb: async ({ addClimb }, use) => { + await use(await addClimb()) + }, + + gradeSystemFor: async ({ country }, use) => { + const ctx = gradeContextToGradeScales[country.gradeContext] + assert(ctx !== undefined) + + const generate = (climb: { type: DisciplineType }): GradeScalesTypes => { + const key = Object.keys(climb.type).filter((type) => climb.type[type])[0] + const system: GradeScalesTypes = + gradeContextToGradeScales[country.gradeContext]?.[key] + assert(system) + return system + } + + await use(generate) + }, + + randomGrade: async ({ country, gradeSystemFor }, use) => { + const ctx = gradeContextToGradeScales[country.gradeContext] + assert(ctx !== undefined) + + const generate = (climb: ClimbType): string => { + const system = gradeSystemFor(climb) + + const scale = getScale(system) + assert(scale, `no support for system ${system}`) + const grade = scale.getGrade(Math.floor(Math.random() * 100)) + assert(grade) + + const record = createGradeObject(grade, climb.type, ctx) + assert(record !== undefined) + + const first = record[Object.keys(record)[0]] + assert(first) + return first + } + + await use(generate) + } +}) diff --git a/src/__tests__/fixtures/gql.fixtures.ts b/src/__tests__/fixtures/gql.fixtures.ts new file mode 100644 index 00000000..3b3925ea --- /dev/null +++ b/src/__tests__/fixtures/gql.fixtures.ts @@ -0,0 +1,134 @@ +import { ApolloServer, BaseContext } from '@apollo/server' +import express, { Application } from 'express' +import request from 'supertest' +import jwt from 'jsonwebtoken' +import { expressMiddleware } from '@apollo/server/express4' +import bodyParser from 'body-parser' +import { applyMiddleware } from 'graphql-middleware' +import { graphqlSchema } from '../../graphql/resolvers' +import cors from 'cors' +import { dataFixtures } from './data.fixtures' +import { createContext, permissions } from '../../auth' +import { ASTNode, print } from 'graphql' + +let server: ApolloServer +let app: Application + +interface ServerTestFixtures { + ctx: { + server: ApolloServer + app: Application + } + query: (opts: QueryAPIProps) => Promise +} + +export interface QueryAPIProps { + query?: string | ASTNode + operationName?: string + variables?: any + userUuid?: string + roles?: string[] + port?: number + endpoint?: string + app?: express.Application + body?: any +} + +export const gqlTest = dataFixtures.extend({ + ctx: async ({ + climbs, areas, bulkImport, + organizations, + ticks, + history, + media, + users + }, use) => { + if (app === undefined) { + app = express() + } + + if (server === undefined) { + const schema = applyMiddleware( + graphqlSchema, + permissions.generate(graphqlSchema) + ) + + const dataSources = ({ + climbs, + areas, + bulkImport, + organizations, + ticks, + history, + media, + users + }) + + server = new ApolloServer({ + schema, + introspection: false, + plugins: [] + }) + + // server must be started before applying middleware + await server.start() + + app.use('/', + bodyParser.json({ limit: '10mb' }), + cors(), + express.json(), + expressMiddleware(server, { + context: async ({ req }) => ({ dataSources, ...await createContext({ req }) }) + }) + ) + } + + await use({ + app, server + }) + }, + + query: async ({ ctx }, use) => { + await use( + async ({ + query, + operationName, + variables, + userUuid, + roles = [], + endpoint = '/' + }: QueryAPIProps) => { + // Avoid needing to pass in actual signed tokens. + const jwtSpy = vi.spyOn(jwt, 'verify') + + jwtSpy.mockImplementation(() => { + return { + // Roles defined at https://manage.auth0.com/dashboard/us/dev-fmjy7n5n/roles + 'https://tacos.openbeta.io/roles': roles, + 'https://tacos.openbeta.io/uuid': userUuid + } + }) + + // It can be convienient to pass in the results of some + // gql`dksjfhsdkjf` as in many IDE's this will provide + // syntax highlighting and will create a more useful error + // message when a mistake in the query literal is made. + if (query !== undefined && typeof query !== 'string') { + query = print(query) + } + + const queryObj = { query, operationName, variables } + + let req = request(ctx.app) + .post(endpoint) + .send(queryObj) + + if (userUuid != null) { + req = req.set('Authorization', 'Bearer placeholder-jwt-see-SpyOn') + } + + return await req + } + ) + } +}) diff --git a/src/__tests__/fixtures/mongo.fixtures.ts b/src/__tests__/fixtures/mongo.fixtures.ts new file mode 100644 index 00000000..0a7decb6 --- /dev/null +++ b/src/__tests__/fixtures/mongo.fixtures.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-empty-pattern */ +// To explain the rule for this file: Object destructuring is REQUIRED for vitest fixtures because +// of how they utilize autoloading. +import { MongoClient } from 'mongodb' +import mongoose, { Connection } from 'mongoose' +import MutableAreaDataSource from '../../model/MutableAreaDataSource' +import MutableClimbDataSource from '../../model/MutableClimbDataSource' +import BulkImportDataSource from '../../model/BulkImportDataSource' +import ChangeLogDataSource from '../../model/ChangeLogDataSource' +import MutableMediaDataSource from '../../model/MutableMediaDataSource' +import MutableOrganizationDataSource from '../../model/MutableOrganizationDataSource' +import TickDataSource from '../../model/TickDataSource' +import UserDataSource from '../../model/UserDataSource' +import { MUUID } from 'uuid-mongodb' +import { inject } from 'vitest' +import { ClimbEditOperationType } from '../../db/ClimbTypes' +import { OperationType } from '../../db/AreaTypes' + +interface DbTestContext { + uri: string + client: MongoClient + mongoose: Connection + + areas: MutableAreaDataSource + climbs: MutableClimbDataSource + bulkImport: BulkImportDataSource + organizations: MutableOrganizationDataSource + ticks: TickDataSource + history: ChangeLogDataSource + media: MutableMediaDataSource + users: UserDataSource + changeLog: ChangeLogDataSource + + waitForChanges: (props: WaitProps, op?: () => Promise) => Promise +} + +export const dbTest = test.extend({ + uri: async ({ }, use) => await use(inject('uri')), + client: [async ({ uri }, use) => { + const client = new MongoClient(uri) + await client.connect() + await use(client) + await client.close() + }, { auto: true }], + + mongoose: [async ({ uri }, use) => { + await mongoose.connect(uri) + await use(mongoose.connection) + }, { auto: true }], + + areas: async ({ }, use) => await use(MutableAreaDataSource.getInstance()), + climbs: async ({ }, use) => await use(MutableClimbDataSource.getInstance()), + bulkImport: async ({ }, use) => await use(BulkImportDataSource.getInstance()), + organizations: async ({ }, use) => await use(MutableOrganizationDataSource.getInstance()), + ticks: async ({ }, use) => await use(TickDataSource.getInstance()), + history: async ({ }, use) => await use(ChangeLogDataSource.getInstance()), + media: async ({ }, use) => await use(MutableMediaDataSource.getInstance()), + users: async ({ }, use) => await use(UserDataSource.getInstance()), + changeLog: async ({ }, use) => await use(ChangeLogDataSource.getInstance()), + + waitForChanges: async ({ mongoose }, use) => { + async function wait ( + props: WaitProps, + op?: () => Promise + ): Promise { + const { document, operation, user } = props + const match: any = {} + // match a changeset based on whether the changes include the target document. + if (document !== undefined) match['fullDocument.changes.fullDocument._id'] = document._id + if (user !== undefined) match['fullDocument.editedBy'] = user + if (operation !== undefined) match['fullDocument.operation'] = operation + + const pipeline = [{ $match: match }] + const stream = mongoose.watch(pipeline, { fullDocument: 'updateLookup' }) + + // 1) Trigger the change that should produce the event that we are waiting fot + const result = (op !== undefined) ? await op() : undefined as T + + // 2) Then wait for it + await stream.hasNext() + await stream.next() + await stream.close() + + return result + } + + await use(wait) + } +}) + +interface WaitProps { + operation?: OperationType | ClimbEditOperationType + document?: { _id: mongoose.Types.ObjectId | MUUID } + user?: MUUID +} diff --git a/src/__tests__/history.ts b/src/__tests__/history.ts deleted file mode 100644 index 0c02f392..00000000 --- a/src/__tests__/history.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { ApolloServer } from '@apollo/server' -import muuid from 'uuid-mongodb' -import { jest } from '@jest/globals' -import MutableAreaDataSource from '../model/MutableAreaDataSource.js' -import MutableOrganizationDataSource from '../model/MutableOrganizationDataSource.js' -import MutableClimbDataSource from '../model/MutableClimbDataSource.js' -import { AreaType } from '../db/AreaTypes.js' -import { OrganizationType, OrgType } from '../db/OrganizationTypes.js' -import { muuidToString } from '../utils/helpers.js' -import { queryAPI, setUpServer } from '../utils/testUtils.js' -import { InMemoryDB } from '../utils/inMemoryDB.js' -import express from 'express' - -jest.setTimeout(60000) - -describe('history API', () => { - let server: ApolloServer - let user: muuid.MUUID - let userUuid: string - let app: express.Application - let inMemoryDB: InMemoryDB - - // Mongoose models for mocking pre-existing state. - let areas: MutableAreaDataSource - let organizations: MutableOrganizationDataSource - let climbs: MutableClimbDataSource - - beforeAll(async () => { - ({ server, inMemoryDB, app } = await setUpServer()) - // Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format - // "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==". - user = muuid.mode('relaxed').v4() - userUuid = muuidToString(user) - }) - - beforeEach(async () => { - await inMemoryDB.clear() - areas = MutableAreaDataSource.getInstance() - organizations = MutableOrganizationDataSource.getInstance() - climbs = MutableClimbDataSource.getInstance() - }) - - afterAll(async () => { - await server.stop() - await inMemoryDB.close() - }) - - describe('queries', () => { - const FRAGMENT_CHANGE_HISTORY = ` - fragment ChangeHistoryFields on History { - id - createdAt - operation - editedBy - changes { - dbOp - changeId - updateDescription { - updatedFields - } - fullDocument { - ... on Area { - areaName - uuid - metadata { - leaf - areaId - } - } - ... on Climb { - id - name - uuid - } - ... on Organization { - orgId - } - } - } - } - ` - - const QUERY_RECENT_CHANGE_HISTORY = ` - ${FRAGMENT_CHANGE_HISTORY} - query ($filter: AllHistoryFilter) { - getChangeHistory(filter: $filter) { - ...ChangeHistoryFields - } - } - ` - - let usa: AreaType - let ca: AreaType - let alphaOrg: OrganizationType - let climbIds: string[] - - it('queries recent change history successfully', async () => { - // Make changes to be tracked. - usa = await areas.addCountry('usa') - ca = await areas.addArea(user, 'CA', usa.metadata.area_id) - const alphaFields = { - displayName: 'Alpha OpenBeta Club', - associatedAreaIds: [usa.metadata.area_id], - email: 'admin@alphaopenbeta.com' - } - alphaOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, alphaFields) - climbIds = await climbs.addOrUpdateClimbs(user, ca.metadata.area_id, [{ name: 'Alpha Climb' }]) - - // Query for changes and ensure they are tracked. - const resp = await queryAPI({ - query: QUERY_RECENT_CHANGE_HISTORY, - variables: { filter: {} }, - userUuid, - app - }) - expect(resp.statusCode).toBe(200) - const histories = resp.body.data.getChangeHistory - expect(histories.length).toBe(3) - - // Latest change first - // Note: addCountry is not captured by history. - const [climbChange, orgChange, areaChange] = histories - - expect(climbChange.operation).toBe('updateClimb') - expect(climbChange.editedBy).toBe(userUuid) - - /** - * Four changes (Ordering is non-deterministic) - * 1. Insert the climb - * 2. Update the parent area - * 3. Update aggregate object on crag - * 4. Update the parent area - */ - expect(climbChange.changes.length).toBe(4) - const insertChange = climbChange.changes.filter(c => c.dbOp === 'insert')[0] - const updateChange = climbChange.changes.filter(c => c.dbOp === 'update')[0] - expect(insertChange.fullDocument.uuid).toBe(climbIds[0]) - expect(updateChange.fullDocument.uuid).toBe(muuidToString(ca.metadata.area_id)) - - expect(orgChange.operation).toBe('addOrganization') - expect(orgChange.editedBy).toBe(userUuid) - expect(orgChange.changes[0].fullDocument.orgId).toBe(muuidToString(alphaOrg.orgId)) - - expect(areaChange.operation).toBe('addArea') - expect(areaChange.editedBy).toBe(userUuid) - }) - }) -}) diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 00000000..e868bd2e --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,77 @@ +import { ChangeStream, MongoClient } from 'mongodb' +import { MongoMemoryReplSet } from 'mongodb-memory-server' +import mongoose from 'mongoose' +import { checkVar, defaultPostConnect } from '../db' +import type { TestProject } from 'vitest/node' +import isoCountries, { Alpha3Code } from 'i18n-iso-countries' +import CountriesLngLat from '../data/countries-with-lnglat.json' + +/** + * In-memory Mongo replset used for testing. + * More portable than requiring user to set up Mongo in a background Docker process. + * Need a replset to faciliate transactions. + */ +let mongod: MongoMemoryReplSet +let uri: string +let _stream: ChangeStream + +// https://vitest.dev/config/ +declare module 'vitest' { + export interface ProvidedContext { + /** The mongod URI (for tests to connect to) */ + uri: string + } +} + +async function setupSharedCountryQueue (): Promise { + const availableCountries: Alpha3Code[] = Object.keys( + isoCountries.getAlpha3Codes() + ).filter((country) => CountriesLngLat[country]) as Alpha3Code[] + + const client = new MongoClient(uri) + await client.connect() + const db = await client.db() + await db.collection('test_countries').insertMany( + availableCountries.map(code => ({ code, reserved: false })) + ) + await client.close() +} + +export async function setup (project: TestProject): Promise { + mongod = await MongoMemoryReplSet.create({ + // Stream listener listens on DB denoted by 'MONGO_DBNAME' env var. + replSet: { count: 1, storageEngine: 'wiredTiger', dbName: checkVar('MONGO_DBNAME') } + }) + + uri = await mongod.getUri(checkVar('MONGO_DBNAME')) + await mongoose.connect(uri, { autoIndex: false }) + // Set to 'true' to enable verbose mode + mongoose.set('debug', false) + _stream = await defaultPostConnect() + _stream.on('change', (doc) => { + // Dummy consumer to make sure doc changes get offloaded + }) + + await setupSharedCountryQueue() + project.provide('uri', uri) + + // Vitest, like most modern test environments, supports a means by which we can re-create + // an empty database context every time a file changes and the tests begin their rerun. + project.onTestsRerun(async () => { + const client = new MongoClient(uri) + try { + await client.connect() + await client.db().dropDatabase() + await setupSharedCountryQueue() + } finally { + await client.close() + } + }) +} + +export async function teardown (): Promise { + await mongoose.disconnect() + await mongoose.connection.close(true) + await _stream.close() + await mongod.stop() +} diff --git a/src/__tests__/ticks.ts b/src/__tests__/ticks.ts deleted file mode 100644 index 224fb8b8..00000000 --- a/src/__tests__/ticks.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { ApolloServer } from '@apollo/server' -import muuid from 'uuid-mongodb' -import { jest } from '@jest/globals' -import { queryAPI, setUpServer } from '../utils/testUtils.js' -import { muuidToString } from '../utils/helpers.js' -import { TickInput } from '../db/TickTypes.js' -import TickDataSource from '../model/TickDataSource.js' -import UserDataSource from '../model/UserDataSource.js' -import { UpdateProfileGQLInput } from '../db/UserTypes.js' -import { InMemoryDB } from '../utils/inMemoryDB.js' -import express from 'express' -import MutableClimbDataSource from '../model/MutableClimbDataSource.js' -import MutableAreaDataSource from '../model/MutableAreaDataSource.js' -import { ClimbChangeInputType } from '../db/ClimbTypes.js' - -jest.setTimeout(110000) - -const newClimbsToAdd: ClimbChangeInputType[] = [ - { - name: 'Sport 1', - disciplines: { - sport: true - }, - description: 'The best climb', - location: '5m left of the big tree', - protection: '5 quickdraws' - }, - { - name: 'Deep water 1', - disciplines: { - deepwatersolo: true - } - } -] - -describe('ticks API', () => { - let server: ApolloServer - let user: muuid.MUUID - let userUuid: string - let app: express.Application - let inMemoryDB: InMemoryDB - - // Mongoose models for mocking pre-existing state. - let ticks: TickDataSource - let users: UserDataSource - let tickOne: TickInput - let climbs: MutableClimbDataSource - let areas: MutableAreaDataSource - - beforeAll(async () => { - ({ server, inMemoryDB, app } = await setUpServer()) - user = muuid.v4() - userUuid = muuidToString(user) - - tickOne = { - name: 'Route One', - notes: 'Nice slab', - climbId: 'tbd', // need to create a climb for tick validation - userId: userUuid, - style: 'Lead', - attemptType: 'Onsight', - dateClimbed: new Date('2016-07-20T17:30:15+05:30'), - grade: '5.8', - source: 'MP' - } - }) - - beforeEach(async () => { - ticks = TickDataSource.getInstance() - users = UserDataSource.getInstance() - climbs = MutableClimbDataSource.getInstance() - areas = MutableAreaDataSource.getInstance() - await inMemoryDB.clear() - - // Add climbs because add/update tick requires type validation - await areas.addCountry('usa') - const newDestination = await areas.addArea(user, 'California', null, 'usa') - const routesArea = await areas.addArea(user, 'Sport & Trad', newDestination.metadata.area_id) - - const newIDs = await climbs.addOrUpdateClimbs(user, routesArea.metadata.area_id, newClimbsToAdd) - // Update tick inputs with generated climb IDs - tickOne.climbId = newIDs[0] - }) - - afterAll(async () => { - await server.stop() - await inMemoryDB.close() - }) - - describe('queries', () => { - const userQuery = ` - query userTicks($userId: MUUID, $username: String) { - userTicks(userId: $userId, username: $username) { - _id - name - notes - climbId - style - attemptType - dateClimbed - grade - userId - } - } - ` - const userTickByClimbQuery = ` - query userTicksByClimbId($userId: String, $climbId: String) { - userTicksByClimbId(userId: $userId, climbId: $climbId) { - _id - name - notes - climbId - style - attemptType - dateClimbed - grade - userId - } - } - ` - - it('queries by userId', async () => { - const userProfileInput: UpdateProfileGQLInput = { - userUuid, - username: 'cat.dog', - email: 'cat@example.com' - } - await users.createOrUpdateUserProfile(user, userProfileInput) - await ticks.addTick(tickOne) - const response = await queryAPI({ - query: userQuery, - variables: { userId: userUuid }, - userUuid, - app - }) - expect(response.statusCode).toBe(200) - const res = response.body.data.userTicks - expect(res).toHaveLength(1) - expect(res[0].name).toBe(tickOne.name) - }) - - it('queries by username', async () => { - const userProfileInput: UpdateProfileGQLInput = { - userUuid, - username: 'cat.dog', - email: 'cat@example.com' - } - await users.createOrUpdateUserProfile(user, userProfileInput) - await ticks.addTick(tickOne) - const response = await queryAPI({ - query: userQuery, - variables: { username: 'cat.dog' }, - userUuid, - app - }) - expect(response.statusCode).toBe(200) - const res = response.body.data.userTicks - expect(res).toHaveLength(1) - expect(res[0].name).toBe(tickOne.name) - }) - - it('queries by userId and climbId', async () => { - await ticks.addTick(tickOne) - const response = await queryAPI({ - query: userTickByClimbQuery, - variables: { userId: userUuid, climbId: tickOne.climbId }, - userUuid, - app - }) - expect(response.statusCode).toBe(200) - const res = response.body.data.userTicksByClimbId - expect(res).toHaveLength(1) - expect(res[0].name).toBe(tickOne.name) - }) - }) - - describe('mutations', () => { - const createQuery = ` - mutation ($input: Tick!) { - tick: addTick(input: $input) { - _id - name - notes - climbId - userId - style - attemptType - dateClimbed - grade - source - } - } - ` - const updateQuery = ` - mutation ($input: TickFilter!) { - tick: editTick(input: $input) { - _id - name - notes - climbId - userId - style - attemptType - dateClimbed - grade - source - } - } - ` - it('creates and updates a tick', async () => { - const createResponse = await queryAPI({ - query: createQuery, - variables: { input: tickOne }, - userUuid, - roles: ['user_admin'], - app - }) - - expect(createResponse.statusCode).toBe(200) - const createTickRes = createResponse.body.data.tick - expect(createTickRes.name).toBe(tickOne.name) - expect(createTickRes.notes).toBe(tickOne.notes) - expect(createTickRes.climbId).toBe(tickOne.climbId) - expect(createTickRes.userId).toBe(tickOne.userId) - expect(createTickRes.style).toBe(tickOne.style) - expect(createTickRes.attemptType).toBe(tickOne.attemptType) - expect(createTickRes.dateClimbed).toBe(new Date(tickOne.dateClimbed).getTime()) - expect(createTickRes.grade).toBe(tickOne.grade) - expect(createTickRes.source).toBe(tickOne.source) - expect(createTickRes._id).toBeTruthy() - - const updateResponse = await queryAPI({ - query: updateQuery, - variables: { - input: { - _id: createTickRes._id, - updatedTick: { - name: 'Updated Route One', - climbId: tickOne.climbId, - userId: userUuid, - dateClimbed: new Date('2022-11-10T12:00:00Z'), - grade: 'new grade', - source: 'OB' - } - } - }, - userUuid, - roles: [], // ['user_admin'], - app - }) - - expect(updateResponse.statusCode).toBe(200) - expect(updateResponse.body.data.tick.name).toBe('Updated Route One') - }) - it('verifies date formats correctly', async () => { - const validDateTick = { - ...tickOne, - dateClimbed: new Date('2022-11-10T15:30:00Z').getTime() - } - const validResponse = await queryAPI({ - query: createQuery, - variables: { input: validDateTick }, - userUuid, - roles: ['user_admin'], - app - }) - expect(validResponse.statusCode).toBe(200) - expect(validResponse.body.data.tick.dateClimbed) - .toBe(new Date('2022-11-10T15:30:00Z').getTime()) - }) - }) -}) diff --git a/src/db/edit/streamListener.ts b/src/db/edit/streamListener.ts index 9212a7b2..25d11077 100644 --- a/src/db/edit/streamListener.ts +++ b/src/db/edit/streamListener.ts @@ -85,7 +85,7 @@ const onChange = async (change: ChangeStreamDocument): Promise => { fullDocument: fullDocument as SupportedCollectionTypes, updateDescription, dbOp - }) + }).catch(console.error) } case 'insert': { const dbOp = 'insert' diff --git a/src/db/export/json/area.resolver.test.ts b/src/db/export/json/area.resolver.test.ts index c70fa532..3c1265d2 100644 --- a/src/db/export/json/area.resolver.test.ts +++ b/src/db/export/json/area.resolver.test.ts @@ -23,7 +23,7 @@ describe('area resolvers', () => { } ] - function assertNameResolver (areaName: string | undefined, expected: string) { + function assertNameResolver (areaName: string | undefined, expected: string): void { expect(resolveAreaFileName({ area_name: areaName })).toBe(expected) } @@ -42,7 +42,7 @@ describe('area resolvers', () => { { name: 'should ignore slashes in names', input: ['test/', 'test2\\'], expected: path.join('test', 'test2') } ] - function assertSubPathResolver (path: string[], expected: string) { + function assertSubPathResolver (path: string[], expected: string): void { expect(resolveAreaSubPath({ pathTokens: path })).toBe(expected) } diff --git a/src/db/export/json/async-file.processor.test.ts b/src/db/export/json/async-file.processor.test.ts index c02ddf19..4f293a17 100644 --- a/src/db/export/json/async-file.processor.test.ts +++ b/src/db/export/json/async-file.processor.test.ts @@ -1,18 +1,20 @@ +import { logger } from '../../../logger' +import { Processor } from '../common/processor' import { asyncFileProcessor, Writer } from './async-file.processor' import path from 'path' interface TestType { name: string, path?: string[] } describe('file processor', () => { - const writer = jest.fn(async (_data, _path) => await Promise.resolve()) + const writer = vi.fn(async (_data, _path) => await Promise.resolve()) const testData: TestType[] = [{ name: 'test', path: ['one', 'two'] }, { name: 'test2' }] const testPath = 'testPath' - function assertWriterCalledFor (data: TestType) { + function assertWriterCalledFor (data: TestType): void { expect(writer).toHaveBeenCalledWith(JSON.stringify(data), path.resolve(testPath, ...data.path ?? '', `${data.name}.json`)) } - function createProcessor (w: Writer = writer) { + function createProcessor (w: Writer = writer): Processor { return asyncFileProcessor({ basePath: testPath, fileNameResolver: (data: TestType) => data.name, @@ -21,11 +23,13 @@ describe('file processor', () => { }) } - function withFailedWriteOn (failingData: { name: string }) { + function withFailedWriteOn (failingData: TestType): (data: any, path: any) => Promise { return async (data, path) => { + logger.info(data, failingData) if (data === JSON.stringify(failingData)) { - return await Promise.reject('error') + return await Promise.reject(new Error('IO: We made this error')) } + return await writer(data, path) } } @@ -42,7 +46,10 @@ describe('file processor', () => { it('should continue batch processing on error', async () => { const processor = createProcessor(withFailedWriteOn(testData[0])) - await expect(processor(testData, 0)).rejects.toContain('Failed to write 1/2 files') + // First, check that our failed writer fires as expected + await expect(async () => await withFailedWriteOn(testData[0])(JSON.stringify(testData[0]), 'path')).rejects.toThrow('IO: We made this error') + // now in the context of a strem, we should expect 1 out of two possible files to fail + await expect(async () => await processor(testData, 0)).rejects.toThrow('Failed to write 1/2 files') assertWriterCalledFor(testData[1]) }) diff --git a/src/db/import/usa/__tests__/Tree.test.ts b/src/db/import/usa/__tests__/Tree.test.ts index ebb61a16..2640de90 100644 --- a/src/db/import/usa/__tests__/Tree.test.ts +++ b/src/db/import/usa/__tests__/Tree.test.ts @@ -1,3 +1,4 @@ +import { logger } from '../../../../logger' import { Tree, createRootNode } from '../AreaTree' const path1 = 'Oregon|Central Oregon|Paulina Peak|Vigilantes de Obsidiana|Roca Rhodales' @@ -45,7 +46,7 @@ describe('Area Tree data structure', () => { const leaf = tree.atPath(path1) if (leaf !== undefined) { const ancestors = leaf.getAncestors() - console.log(ancestors) + logger.debug(ancestors) expect(ancestors.length).toEqual(path1.split('|').length + 1) // all element of path1 + 1 for US root expect(ancestors[0]).toEqual(countryRoot?.uuid) const stateRoot = tree.atPath('Oregon') diff --git a/src/db/index.ts b/src/db/index.ts index e31ae385..c5af1a40 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -81,7 +81,7 @@ export const gracefulExit = async (exitCode: number = 0): Promise => { } export const defaultPostConnect = async (changeStreamListener = streamListener): Promise => { - console.log('Kudos!') + logger.debug('defaultPostConnect- Kudos!') await createIndexes() return await changeStreamListener() } diff --git a/src/db/utils/__tests__/Aggregate.test.ts b/src/db/utils/__tests__/Aggregate.test.ts index f4ea7418..693b7bad 100644 --- a/src/db/utils/__tests__/Aggregate.test.ts +++ b/src/db/utils/__tests__/Aggregate.test.ts @@ -1,4 +1,3 @@ -import { jest } from '@jest/globals' import { logger } from '../../../logger' import { aggregateCragStats, merge } from '../Aggregate' import { AggregateType } from '../../AreaTypes' @@ -40,7 +39,7 @@ describe('Aggregate merge', () => { describe('Aggregate Crag Stats', () => { it('Provides crag stat aggregates in US grade context', () => { - jest.spyOn(logger, 'warn').mockImplementation(() => {}) + vi.spyOn(logger, 'warn').mockImplementation(() => {}) const crag = { gradeContext: 'US', climbs: [ diff --git a/src/graphql/__tests__/areas.test.ts b/src/graphql/__tests__/areas.test.ts new file mode 100644 index 00000000..8ceb3df2 --- /dev/null +++ b/src/graphql/__tests__/areas.test.ts @@ -0,0 +1,164 @@ +import { MediaObjectGQLInput } from '../../db/MediaObjectTypes.js' +import { AreaType } from '../../db/AreaTypes.js' +import { OrganizationEditableFieldsType, OrganizationType, OrgType } from '../../db/OrganizationTypes.js' +import { muuidToString } from '../../utils/helpers.js' +import { gqlTest } from '../../__tests__/fixtures/gql.fixtures.js' +import gql from 'graphql-tag' +interface LocalContext { + includedChild: AreaType + excludedArea: AreaType + alphaFields: OrganizationEditableFieldsType + alphaOrg: OrganizationType + insertMedia: (mediaCount: number, areaId?: string, user?: string) => Promise +} + +const it = gqlTest.extend({ + includedChild: async ({ addArea, area }, use) => await use(await addArea(undefined, { parent: area })), + excludedArea: async ({ addArea, area }, use) => await use(await addArea(undefined, { parent: area })), + alphaFields: async ({ excludedArea, task, area }, use) => await use({ + displayName: task.id, + associatedAreaIds: [area.metadata.area_id], + excludedAreaIds: [excludedArea.metadata.area_id] + }), + alphaOrg: async ({ organizations, user, alphaFields }, use) => { + const org = await organizations.addOrganization(user, OrgType.localClimbingOrganization, alphaFields) + .then((res: OrganizationType | null) => { + if (res === null) throw new Error('Failure mocking organization.') + return res + }) + + await use(org) + await organizations.deleteFromCacheById(org._id) + }, + insertMedia: async ({ task, area, userUuid, media }, use) => { + async function insertMediaObjectsForArea (mediaCount: number, areaId: string = muuidToString(area.metadata.area_id), user = userUuid): Promise { + const newMediaListInput: MediaObjectGQLInput[] = [] + for (let picIndex = 0; picIndex < mediaCount; picIndex++) { + newMediaListInput.push({ + userUuid: user, + width: 800, + height: 600, + size: 45000, + format: 'jpeg', + mediaUrl: `/u/${user}/${areaId}-${picIndex}.jpg`, + entityTag: { + entityType: 1, + entityId: areaId + } + }) + } + + await media.addMediaObjects(newMediaListInput) + } + await use(insertMediaObjectsForArea) + } +}) + +describe('areas API', () => { + describe('queries', () => { + const areaQuery = ` + query area($input: ID) { + area(uuid: $input) { + uuid + organizations { + orgId + } + } + } + ` + + it('retrieves an area omitting organizations that exclude it', async ({ query, userUuid, excludedArea, alphaOrg }) => { + const response = await query({ + query: areaQuery, + operationName: 'area', + variables: { input: muuidToString(excludedArea.metadata.area_id) }, + userUuid + }) + + expect(response.statusCode).toBe(200) + const areaResult = response.body.data.area + expect(areaResult).toBeTruthy() + expect(areaResult.uuid).toBe(muuidToString(excludedArea.metadata.area_id)) + // Even though alphaOrg associates with ca's parent, usa, it excludes + // ca and so should not be listed. + expect(areaResult.organizations).not.toContainEqual({ orgId: alphaOrg.orgId.toString() }) + }) + + it('retrieves an area and lists associated organizations', async ({ query, userUuid, includedChild, alphaOrg }) => { + const response = await query({ + query: areaQuery, + operationName: 'area', + variables: { input: muuidToString(includedChild.metadata.area_id) }, + userUuid + }) + + expect(response.statusCode).toBe(200) + const areaResult = response.body.data.area + expect(areaResult.uuid).toBe(muuidToString(includedChild.metadata.area_id)) + expect(areaResult.organizations).toContainEqual({ orgId: muuidToString(alphaOrg.orgId) }) + }) + + it('retrieves an area and lists associated organizations, even with no auth context', async ({ query, includedChild, alphaOrg }) => { + const response = await query({ + query: areaQuery, + operationName: 'area', + variables: { input: muuidToString(includedChild.metadata.area_id) } + }) + + expect(response.statusCode).toBe(200) + const areaResult = response.body.data.area + expect(areaResult.uuid).toBe(muuidToString(includedChild.metadata.area_id)) + expect(areaResult.organizations).toContainEqual({ orgId: alphaOrg.orgId.toString() }) + expect(areaResult.organizations).toContainEqual({ orgId: alphaOrg.orgId.toString() }) + }) + }) + + it('returns paginated Media when requested', async ({ area, query, insertMedia }) => { + await insertMedia(11) + + const areaQueryWithPaginatedMedia = gql` + query area($uuid: ID!, $input: EmbeddedAreaMediaInput) { + area(uuid: $uuid) { + mediaPagination(input: $input) { + areaUuid + mediaConnection { + edges { + node { + id + mediaUrl + } + cursor + } + pageInfo { + hasNextPage + totalItems + endCursor + } + } + } + } + } + ` + const response = await query({ + query: areaQueryWithPaginatedMedia, + operationName: 'area', + variables: { + uuid: area.metadata.area_id.toString(), + input: { + first: 5, + after: null + } + } + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toBeTruthy() + expect(response.body.data).toBeTruthy() + expect(response.body.data.area).toBeTruthy() + + const areaResult = response.body.data.area + expect(areaResult.mediaPagination.mediaConnection.edges).toHaveLength(5) + expect(areaResult.mediaPagination.mediaConnection.pageInfo.totalItems).toBe(11) + expect(areaResult.mediaPagination.mediaConnection.pageInfo.hasNextPage).toBe(true) + }) +}) diff --git a/src/graphql/__tests__/bulkImport.test.ts b/src/graphql/__tests__/bulkImport.test.ts new file mode 100644 index 00000000..9ad8c197 --- /dev/null +++ b/src/graphql/__tests__/bulkImport.test.ts @@ -0,0 +1,112 @@ +import muuid from 'uuid-mongodb' +import exampleImportData from '../../__tests__/import-example.json' assert {type: 'json'} +import { BulkImportResultType } from '../../db/BulkImportTypes.js' +import { gqlTest } from '../../__tests__/fixtures/gql.fixtures.js' +import { muuidToString } from '../../utils/helpers' + +interface LocalContext { + importData: typeof exampleImportData +} + +const it = gqlTest.extend({ + importData: async ({ country }, use) => await use( + { + areas: exampleImportData.areas.map(x => { + if (x.countryCode !== undefined && x.countryCode !== '') { + return { ...x, countryCode: country.shortCode } + } + + return { ...x } + }) as typeof exampleImportData['areas'] + }) +}) + +describe('bulkImportAreas', () => { + const bulkQuery = ` + mutation bulkImportAreas($input: BulkImportInput!) { + bulkImportAreas(input: $input) { + addedAreas { + uuid + metadata { + area_id + } + } + updatedAreas { + uuid + metadata { + area_id + } + } + addedOrUpdatedClimbs { + id + } + } + } + ` + + it('should return 403 if no user', async ({ query, importData }) => { + const res = await query({ + query: bulkQuery, + operationName: 'bulkImportAreas', + variables: { input: importData } + }) + expect(res.statusCode).toBe(200) + expect(res.body.errors[0].message).toBe('Not Authorised!') + }) + + it('should return 403 if user is not an editor', async ({ query, userUuid, importData }) => { + const res = await query({ + userUuid, + query: bulkQuery, + operationName: 'bulkImportAreas', + variables: { input: importData } + }) + expect(res.statusCode).toBe(200) + expect(res.body.errors[0].message).toBe('Not Authorised!') + }) + + it('should return 200 if user is an editor', async ({ query, importData, userUuid }) => { + const res = await query({ + userUuid, + roles: ['editor'], + query: bulkQuery, + operationName: 'bulkImportAreas', + variables: { input: importData } + }) + expect(res.status).toBe(200) + }) + + it('should import data', async ({ query, userUuid, area, bulkImport, climbs, importData }) => { + const res = await query({ + userUuid, + roles: ['editor'], + query: bulkQuery, + operationName: 'bulkImportAreas', + variables: { + input: { + areas: [ + ...importData.areas, + { + uuid: muuidToString(area.metadata.area_id), + areaName: 'Updated Test Area' + } + ] + } + } + }) + expect(res.body.errors).toBeFalsy() + + const result = res.body.data.bulkImportAreas as BulkImportResultType + expect(result.addedAreas.length).toBe(4) + + const committedAreas = await Promise.all(result.addedAreas.map(async (area) => await bulkImport.findOneAreaByUUID(muuid.from(area.metadata.area_id)))) + expect(committedAreas.length).toBe(4) + + const committedClimbs = await Promise.all(result.addedOrUpdatedClimbs.map(async (climb) => await climbs.findOneClimbByMUUID(climb._id))) + expect(committedClimbs.length).toBe(2) + + const updatedAreas = await Promise.all(result.updatedAreas.map(async (area) => await bulkImport.findOneAreaByUUID(muuid.from(area.metadata.area_id)))) + expect(updatedAreas.length).toBe(1) + expect(updatedAreas[0].area_name).toBe('Updated Test Area') + }) +}) diff --git a/src/__tests__/gradeUtils.ts b/src/graphql/__tests__/gradeUtils.test.ts similarity index 91% rename from src/__tests__/gradeUtils.ts rename to src/graphql/__tests__/gradeUtils.test.ts index 94c8fffe..895d0d65 100644 --- a/src/__tests__/gradeUtils.ts +++ b/src/graphql/__tests__/gradeUtils.test.ts @@ -1,5 +1,5 @@ -import { DisciplineType } from '../db/ClimbTypes.js' -import { sanitizeDisciplines, createGradeObject, gradeContextToGradeScales } from '../GradeUtils.js' +import { DisciplineType } from '../../db/ClimbTypes.js' +import { sanitizeDisciplines, createGradeObject, gradeContextToGradeScales } from '../../GradeUtils.js' describe('Test grade utilities', () => { it('sanitizes bad discipline object', () => { @@ -42,7 +42,7 @@ describe('Test grade utilities', () => { it('creates grade object correctly in US context', () => { const context = gradeContextToGradeScales.US - if (context == null) fail('Bad grade context. Should not happen.') + assert(context != null, 'Bad grade context, should not happen') let actual = createGradeObject('5.9', sanitizeDisciplines({ sport: true }), context) expect(actual).toEqual({ @@ -88,9 +88,9 @@ describe('Test grade utilities', () => { expect(actual).toBeUndefined() }) - it.failing('can alpine ice grades to climbs with discipline ice', () => { + it.fails('can alpine ice grades to climbs with discipline ice', () => { const context = gradeContextToGradeScales.US - if (context == null) fail('Bad grade context. Should not happen.') + assert(context != null, 'Bad grade context, should not happen') const actual = createGradeObject('AI2', sanitizeDisciplines({ ice: true }), context) expect(actual).toEqual({ @@ -100,7 +100,7 @@ describe('Test grade utilities', () => { it('creates grade object correctly in AU context', () => { const context = gradeContextToGradeScales.AU - if (context == null) fail('Bad grade context. Should not happen.') + assert(context != null, 'Bad grade context, should not happen') let actual = createGradeObject('5', sanitizeDisciplines({ sport: true }), context) expect(actual).toEqual({ @@ -139,7 +139,7 @@ describe('Test grade utilities', () => { it('creates grade object correctly in FR context', () => { const context = gradeContextToGradeScales.FR - if (context == null) fail('Bad grade context. Should not happen.') + assert(context != null, 'Bad grade context, should not happen') let actual = createGradeObject('5a', sanitizeDisciplines({ sport: true }), context) expect(actual).toEqual({ @@ -178,7 +178,8 @@ describe('Test grade utilities', () => { it('creates grade object correctly in BRZ context', () => { const context = gradeContextToGradeScales.BRZ - if (context == null) { fail('Bad grade context. Should not happen.') } + assert(context != null, 'Bad grade context, should not happen') + let actual = createGradeObject('V', sanitizeDisciplines({ sport: true }), context) expect(actual).toEqual({ brazilian_crux: 'V' diff --git a/src/graphql/__tests__/history.test.ts b/src/graphql/__tests__/history.test.ts new file mode 100644 index 00000000..cd7425e0 --- /dev/null +++ b/src/graphql/__tests__/history.test.ts @@ -0,0 +1,119 @@ +import mongoose from 'mongoose' +import { OrgType } from '../../db/OrganizationTypes.js' +import { muuidToString } from '../../utils/helpers.js' +import { gqlTest as it } from '../../__tests__/fixtures/gql.fixtures.js' +import muuid from 'uuid-mongodb' +import { ClimbEditOperationType } from '../../db/ClimbTypes.js' + +describe('history API', () => { + describe('queries', () => { + const FRAGMENT_CHANGE_HISTORY = ` + fragment ChangeHistoryFields on History { + id + createdAt + operation + editedBy + changes { + dbOp + changeId + updateDescription { + updatedFields + } + fullDocument { + ... on Area { + areaName + uuid + metadata { + leaf + areaId + } + } + ... on Climb { + id + name + uuid + } + ... on Organization { + orgId + } + } + } + } + ` + + const QUERY_RECENT_CHANGE_HISTORY = ` + ${FRAGMENT_CHANGE_HISTORY} + query ($filter: AllHistoryFilter) { + getChangeHistory(filter: $filter) { + ...ChangeHistoryFields + } + } + ` + + it('queries recent change history successfully', async ({ user, userUuid, query, climbs, organizations, area, country, waitForChanges }) => { + // Make changes to be tracked. + const alphaFields = { + displayName: 'Alpha OpenBeta Club', + associatedAreaIds: [country.metadata.area_id], + email: 'admin@alphaopenbeta.com' + } + + const alphaOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, alphaFields) + const climbIds = await waitForChanges( + { document: area, operation: ClimbEditOperationType.updateClimb }, + async () => await climbs.addOrUpdateClimbs(user, area.metadata.area_id, [{ name: 'Alpha Climb' }]) + ) + + // Query for changes and ensure they are tracked. + const resp = await query({ + query: QUERY_RECENT_CHANGE_HISTORY, + variables: { filter: {} }, + userUuid + }) + + expect(resp.statusCode).toBe(200) + const histories = resp.body.data.getChangeHistory + + const climb = await climbs.findOneClimbByMUUID(muuid.from(climbIds[0])) + + assert(climb) + assert(climb?._change?.historyId) + assert(area._change?.historyId) + assert(alphaOrg._change?.historyId) + + const areaChange = histories.find(item => area._change?.historyId.equals(new mongoose.Types.ObjectId(item.id))) + const orgChange = histories.find(item => alphaOrg._change?.historyId.equals(new mongoose.Types.ObjectId(item.id))) + const climbChange = histories.find(item => climb?._change?.historyId.equals(new mongoose.Types.ObjectId(item.id))) + + assert(climbChange) + assert(orgChange) + assert(areaChange) + + expect(climbChange.editedBy).toBe(userUuid) + + /** + * Four changes (Ordering is non-deterministic) + * 1. Insert the climb + * 2. Update the parent area + * 3. Update aggregate object on crag + * 4. Update the parent area + */ + const climbInsertChange = climbChange.changes.find(c => c.dbOp === 'insert') + const climbUpdateChange = climbChange.changes.find(c => c.dbOp === 'update') + + expect(climbInsertChange).toBeTruthy() + expect(climbInsertChange.fullDocument).toBeTruthy() + expect(climbInsertChange.fullDocument.uuid).toBeTruthy() + + expect(climbInsertChange.fullDocument.uuid).toBe(climbIds[0]) + expect(climbUpdateChange.fullDocument.uuid).toBe(muuidToString(area.metadata.area_id)) + + expect(orgChange.operation).toBe('addOrganization') + expect(orgChange.editedBy).toBe(userUuid) + expect(orgChange.changes[0].fullDocument.orgId).toBe(muuidToString(alphaOrg.orgId)) + + expect(areaChange.operation).toBe('addArea') + expect(areaChange.editedBy).toBe(userUuid) + }) + }) +}) diff --git a/src/__tests__/organizations.ts b/src/graphql/__tests__/organizations.test.ts similarity index 60% rename from src/__tests__/organizations.ts rename to src/graphql/__tests__/organizations.test.ts index f3dd931d..0687f49f 100644 --- a/src/__tests__/organizations.ts +++ b/src/graphql/__tests__/organizations.test.ts @@ -1,58 +1,55 @@ -import { ApolloServer } from '@apollo/server' -import muuid from 'uuid-mongodb' -import MutableAreaDataSource from '../model/MutableAreaDataSource.js' -import MutableOrganizationDataSource from '../model/MutableOrganizationDataSource.js' -import { AreaType } from '../db/AreaTypes.js' -import { OperationType, OrganizationEditableFieldsType, OrganizationType, OrgType } from '../db/OrganizationTypes.js' -import ChangeLogDataSource from '../model/ChangeLogDataSource.js' -import { queryAPI, setUpServer } from '../utils/testUtils.js' -import { muuidToString } from '../utils/helpers.js' +import { OperationType, OrganizationEditableFieldsType, OrganizationType, OrgType } from '../../db/OrganizationTypes.js' +import ChangeLogDataSource from '../../model/ChangeLogDataSource.js' +import { muuidToString } from '../../utils/helpers.js' import { validate as validateMuuid } from 'uuid' -import { InMemoryDB } from '../utils/inMemoryDB.js' -import express from 'express' +import { gqlTest } from '../../__tests__/fixtures/gql.fixtures.js' +import { AreaType } from '../../db/AreaTypes.js' -describe('organizations API', () => { - let server: ApolloServer - let user: muuid.MUUID - let userUuid: string - let app: express.Application - let inMemoryDB: InMemoryDB - - // Mongoose models for mocking pre-existing state. - let areas: MutableAreaDataSource - let organizations: MutableOrganizationDataSource - let usa: AreaType - let ca: AreaType - let wa: AreaType - - beforeAll(async () => { - ({ server, inMemoryDB, app } = await setUpServer()) - // Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format - // "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==". - user = muuid.mode('relaxed').v4() - userUuid = muuidToString(user) - }) - - beforeEach(async () => { - await inMemoryDB.clear() - areas = MutableAreaDataSource.getInstance() - organizations = MutableOrganizationDataSource.getInstance() - usa = await areas.addCountry('usa') - ca = await areas.addArea(user, 'CA', usa.metadata.area_id) - wa = await areas.addArea(user, 'WA', usa.metadata.area_id) - }) +interface LocalContext { + ca: AreaType + wa: AreaType + orgData: OrganizationEditableFieldsType[] + orgs: OrganizationType[] +} - afterAll(async () => { - await server?.stop() - await inMemoryDB?.close() - }) +const it = gqlTest.extend({ + ca: async ({ addArea }, use) => await use(await addArea()), + wa: async ({ addArea }, use) => await use(await addArea()), + orgData: async ({ wa, ca, task }, use) => { + await use([ + { + displayName: `${task.id} Alpha OpenBeta Club`, + associatedAreaIds: [ca.metadata.area_id, wa.metadata.area_id], + email: 'admin@alphaopenbeta.com', + facebookLink: 'https://www.facebook.com/alphaopenbeta', + instagramLink: 'https://www.instagram.com/alphaopenbeta', + hardwareReportLink: 'https://alphaopenbeta.com/reporthardware' + }, + { + displayName: `${task.id} Delta OpenBeta Club`, + email: 'admin@deltaopenbeta.com' + }, + { + displayName: `${task.id}Delta Gamma OpenBeta Club`, + description: 'We are an offshoot of the delta club.\nSee our website for more details.', + excludedAreaIds: [wa.metadata.area_id] + } + ]) + }, + orgs: async ({ organizations, user, orgData }, use) => { + const orgs = await Promise.all(orgData.map(async fields => await organizations.addOrganization(user, OrgType.localClimbingOrganization, fields))) + await use(orgs) + await Promise.all(orgs.map(async (org) => await organizations.deleteFromCacheById(org._id))) + } +}) +describe('organizations API', () => { describe('mutations', () => { const createQuery = ` mutation addOrganization($input: AddOrganizationInput!) { organization: addOrganization(input: $input) { orgId - orgType + orgType displayName associatedAreaIds excludedAreaIds @@ -81,14 +78,14 @@ describe('organizations API', () => { } ` - it('creates and updates an organization', async () => { - const createResponse = await queryAPI({ + it('creates and updates an organization', async ({ query, userUuid, country, addArea }) => { + const areaToExclude = await addArea() + const createResponse = await query({ query: createQuery, operationName: 'addOrganization', variables: { input: { displayName: 'Friends of Openbeta', orgType: 'LOCAL_CLIMBING_ORGANIZATION' } }, userUuid, - roles: ['user_admin'], - app + roles: ['user_admin'] }) expect(createResponse.statusCode).toBe(200) @@ -102,14 +99,14 @@ describe('organizations API', () => { expect(createResponse.body.data.organization.createdBy).toBe(userUuid) expect(createResponse.body.data.organization.updatedBy).toBe(userUuid) - const updateResponse = await queryAPI({ + const updateResponse = await query({ query: updateQuery, operationName: 'updateOrganization', variables: { input: { orgId, - associatedAreaIds: [muuidToString(usa.metadata.area_id)], - excludedAreaIds: [muuidToString(wa.metadata.area_id)], + associatedAreaIds: [muuidToString(country.metadata.area_id)], + excludedAreaIds: [muuidToString(areaToExclude.metadata.area_id)], displayName: 'Allies of Openbeta', website: 'https://alliesofopenbeta.com', email: 'admin@alliesofopenbeta.com', @@ -121,15 +118,14 @@ describe('organizations API', () => { } }, userUuid, - roles: ['user_admin'], - app + roles: ['user_admin'] }) expect(updateResponse.statusCode).toBe(200) expect(updateResponse.body.errors).toBeUndefined() const orgResult = updateResponse.body.data.organization expect(orgResult.orgId).toBe(orgId) - expect(orgResult.associatedAreaIds).toEqual([muuidToString(usa.metadata.area_id)]) - expect(orgResult.excludedAreaIds).toEqual([muuidToString(wa.metadata.area_id)]) + expect(orgResult.associatedAreaIds).toEqual([muuidToString(country.metadata.area_id)]) + expect(orgResult.excludedAreaIds).toEqual([muuidToString(areaToExclude.metadata.area_id)]) expect(orgResult.displayName).toBe('Allies of Openbeta') expect(orgResult.content.website).toBe('https://alliesofopenbeta.com') expect(orgResult.content.email).toBe('admin@alliesofopenbeta.com') @@ -142,7 +138,11 @@ describe('organizations API', () => { // eslint-disable-next-line await new Promise(res => setTimeout(res, 1000)) - const orgHistory = await ChangeLogDataSource.getInstance().getOrganizationChangeSets() + const orgHistory = await ChangeLogDataSource + .getInstance() + .getOrganizationChangeSets() + .then(sets => sets.filter(i => i.editedBy.toString() === userUuid)) + expect(orgHistory).toHaveLength(2) // verify changes in most recent order @@ -161,14 +161,13 @@ describe('organizations API', () => { expect(createRecord[0].fullDocument.displayName).toBe('Friends of Openbeta') }) - it('throws an error if a non-user_admin tries to add an organization', async () => { - const response = await queryAPI({ + it('throws an error if a non-user_admin tries to add an organization', async ({ query, userUuid }) => { + const response = await query({ query: createQuery, operationName: 'addOrganization', variables: { input: { displayName: 'Friends of Openbeta', orgType: 'LOCAL_CLIMBING_ORGANIZATION' } }, userUuid, - roles: ['editor'], - app + roles: ['editor'] }) expect(response.statusCode).toBe(200) expect(response.body.data.organization).toBeNull() @@ -205,140 +204,92 @@ describe('organizations API', () => { } } ` - let alphaFields: OrganizationEditableFieldsType - let deltaFields: OrganizationEditableFieldsType - let gammaFields: OrganizationEditableFieldsType - let alphaOrg: OrganizationType - let deltaOrg: OrganizationType - let gammaOrg: OrganizationType - beforeEach(async () => { - alphaFields = { - displayName: 'Alpha OpenBeta Club', - associatedAreaIds: [ca.metadata.area_id, wa.metadata.area_id], - email: 'admin@alphaopenbeta.com', - facebookLink: 'https://www.facebook.com/alphaopenbeta', - instagramLink: 'https://www.instagram.com/alphaopenbeta', - hardwareReportLink: 'https://alphaopenbeta.com/reporthardware' - } - alphaOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, alphaFields) - .then((res: OrganizationType | null) => { - if (res === null) throw new Error('Failure mocking organization.') - return res - }) - - deltaFields = { - displayName: 'Delta OpenBeta Club', - email: 'admin@deltaopenbeta.com' - } - deltaOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, deltaFields) - .then((res: OrganizationType | null) => { - if (res === null) throw new Error('Failure mocking organization.') - return res - }) - - gammaFields = { - displayName: 'Delta Gamma OpenBeta Club', - description: 'We are an offshoot of the delta club.\nSee our website for more details.', - excludedAreaIds: [wa.metadata.area_id] - } - gammaOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, gammaFields) - .then((res: OrganizationType | null) => { - if (res === null) throw new Error('Failure mocking organization.') - return res - }) - }) - - it('retrieves an organization with an MUUID', async () => { - const response = await queryAPI({ + it('retrieves an organization with an MUUID', async ({ query, userUuid, orgs, orgData, ca, wa }) => { + const response = await query({ query: organizationQuery, operationName: 'organization', - variables: { input: muuidToString(alphaOrg.orgId) }, - userUuid, - app + variables: { input: muuidToString(orgs[0].orgId) }, + userUuid }) expect(response.statusCode).toBe(200) const orgResult = response.body.data.organization - expect(orgResult.orgId).toBe(muuidToString(alphaOrg.orgId)) - expect(orgResult.displayName).toBe(alphaFields.displayName) + expect(orgResult.orgId).toBe(muuidToString(orgs[0].orgId)) + expect(orgResult.displayName).toBe(orgs[0].displayName) expect(orgResult.associatedAreaIds.sort()).toEqual([muuidToString(ca.metadata.area_id), muuidToString(wa.metadata.area_id)].sort()) - expect(orgResult.content.email).toBe(alphaFields.email) - expect(orgResult.content.instagramLink).toBe(alphaFields.instagramLink) - expect(orgResult.content.facebookLink).toBe(alphaFields.facebookLink) - expect(orgResult.content.hardwareReportLink).toBe(alphaFields.hardwareReportLink) + expect(orgResult.content.email).toBe(orgData[0].email) + expect(orgResult.content.instagramLink).toBe(orgData[0].instagramLink) + expect(orgResult.content.facebookLink).toBe(orgData[0].facebookLink) + expect(orgResult.content.hardwareReportLink).toBe(orgData[0].hardwareReportLink) }) - it('retrieves organizations using an exactMatch displayName filter', async () => { - const response = await queryAPI({ + it('retrieves organizations using an exactMatch displayName filter', async ({ query, userUuid, orgs }) => { + const response = await query({ query: organizationsQuery, operationName: 'organizations', - variables: { filter: { displayName: { match: 'Delta OpenBeta Club', exactMatch: true } } }, - userUuid, - app + variables: { filter: { displayName: { match: orgs[1].displayName, exactMatch: true } } }, + userUuid }) expect(response.statusCode).toBe(200) const dataResult = response.body.data.organizations expect(dataResult.length).toBe(1) - expect(dataResult[0].orgId).toBe(muuidToString(deltaOrg.orgId)) + expect(dataResult[0].orgId).toBe(muuidToString(orgs[1].orgId)) }) - it('retrieves organizations using a non-exactMatch displayName filter', async () => { - const response = await queryAPI({ + it('retrieves organizations using a non-exactMatch displayName filter', async ({ query, userUuid, orgs, task }) => { + const response = await query({ query: organizationsQuery, operationName: 'organizations', - variables: { filter: { displayName: { match: 'delta', exactMatch: false } } }, - userUuid, - app + variables: { filter: { displayName: { match: task.id, exactMatch: false } } }, + userUuid }) expect(response.statusCode).toBe(200) const dataResult = response.body.data.organizations - expect(dataResult.length).toBe(2) - expect(dataResult.map(o => o.orgId).sort()).toEqual([muuidToString(deltaOrg.orgId), muuidToString(gammaOrg.orgId)].sort()) + expect(dataResult.map(o => o.orgId).sort()) + .toEqual([muuidToString(orgs[0].orgId), muuidToString(orgs[1].orgId), muuidToString(orgs[2].orgId)].sort()) }) - it('limits organizations returned', async () => { - const response = await queryAPI({ + it('limits organizations returned', async ({ userUuid, query }) => { + const response = await query({ query: organizationsQuery, operationName: 'organizations', variables: { limit: 1 }, - userUuid, - app + userUuid }) expect(response.statusCode).toBe(200) const dataResult = response.body.data.organizations expect(dataResult.length).toBe(1) // Three matching orgs, but only return one. }) - it('retrieves organizations using an associatedAreaIds filter', async () => { - const response = await queryAPI({ + it('retrieves organizations using an associatedAreaIds filter', async ({ userUuid, query, ca, orgs, wa }) => { + const response = await query({ query: organizationsQuery, operationName: 'organizations', variables: { filter: { associatedAreaIds: { includes: [muuidToString(ca.metadata.area_id)] } } }, - userUuid, - app + userUuid }) + // Graphql should convert `includes` from a string[] to MUUID[] expect(response.statusCode).toBe(200) const dataResult = response.body.data.organizations expect(dataResult.length).toBe(1) - expect(dataResult[0].orgId).toBe(muuidToString(alphaOrg.orgId)) + expect(dataResult[0].orgId).toBe(muuidToString(orgs[0].orgId)) }) - it('excludes organizations using an excludedAreaIds filter', async () => { - const response = await queryAPI({ + it('excludes organizations using an excludedAreaIds filter', async ({ userUuid, query, wa, orgs }) => { + const response = await query({ query: organizationsQuery, operationName: 'organizations', variables: { filter: { excludedAreaIds: { excludes: [muuidToString(wa.metadata.area_id)] } } }, - userUuid, - app + userUuid }) expect(response.statusCode).toBe(200) const dataResult = response.body.data.organizations - expect(dataResult.length).toBe(2) - expect(dataResult.map((o: OrganizationType) => o.orgId).includes(muuidToString(gammaOrg.orgId))).toBeFalsy() + // We want the org that explicitly excludes wa to be absent from this array. + expect(dataResult.map((o: OrganizationType) => o.orgId).includes(muuidToString(orgs[2].orgId))).toBeFalsy() }) }) }) diff --git a/src/graphql/__tests__/ticks.test.ts b/src/graphql/__tests__/ticks.test.ts new file mode 100644 index 00000000..e1dee16b --- /dev/null +++ b/src/graphql/__tests__/ticks.test.ts @@ -0,0 +1,210 @@ +import gql from 'graphql-tag' +import { TickInput, TickType } from '../../db/TickTypes.js' +import { allowableStyleMap, choose } from '../../__tests__/fixtures/data.fixtures.js' +import { gqlTest } from '../../__tests__/fixtures/gql.fixtures.js' + +interface LocalContext { + singleTickData: TickInput + tick: TickType +} + +const it = gqlTest.extend({ + singleTickData: async ({ userUuid, climb }, use) => { + await use({ + name: 'Route One', + notes: 'Nice slab', + climbId: climb._id.toString(), + userId: userUuid, + style: 'Lead', + attemptType: choose(allowableStyleMap.Lead), + dateClimbed: new Date('2016-07-20T17:30:15+05:30'), + grade: '5.8', + source: 'MP' + }) + }, + tick: async ({ ticks, singleTickData }, use) => { + await use(await ticks.addTick(singleTickData)) + } +}) + +describe('ticks API', () => { + describe('queries', () => { + const userQuery = gql` + query userTicks($userId: MUUID, $username: String) { + userTicks(userId: $userId, username: $username) { + _id + name + notes + climbId + style + attemptType + dateClimbed + grade + userId + } + } + ` + const userTickByClimbQuery = gql` + query userTicksByClimbId($userId: String, $climbId: String) { + userTicksByClimbId(userId: $userId, climbId: $climbId) { + _id + name + notes + climbId + style + attemptType + dateClimbed + grade + userId + } + } + ` + + it('queries by userId', async ({ userUuid, profile, tick, query }) => { + const response = await query({ + query: userQuery, + variables: { userId: profile._id.toString() } + }) + + expect(response.statusCode).toBe(200) + const res = response.body.data.userTicks + expect(res).toHaveLength(1) + expect(res[0].name).toBe(tick.name) + }) + + it('queries by username', async ({ userUuid, profile, tick, query }) => { + const response = await query({ + query: userQuery, + variables: { username: profile.username }, + userUuid + }) + expect(response.statusCode, JSON.stringify(response.body.errors)).toBe(200) + const res = response.body.data.userTicks + expect(res).toHaveLength(1) + expect(res[0].name).toBe(tick.name) + }) + + it('queries by userId and climbId', async ({ tick, query, userUuid }) => { + const response = await query({ + query: userTickByClimbQuery, + variables: { userId: userUuid, climbId: tick.climbId }, + userUuid + }) + expect(response.statusCode).toBe(200) + const res = response.body.data.userTicksByClimbId + expect(res).toHaveLength(1) + expect(res[0].name).toBe(tick.name) + }) + }) + + describe('mutations', () => { + const createQuery = ` + mutation ($input: Tick!) { + tick: addTick(input: $input) { + _id + name + notes + climbId + userId + style + attemptType + dateClimbed + grade + source + } + } + ` + const updateQuery = ` + mutation ($input: TickFilter!) { + tick: editTick(input: $input) { + _id + name + notes + climbId + userId + style + attemptType + dateClimbed + grade + source + } + } + ` + + it('creates and updates a tick', async ({ + query, + userUuid, + singleTickData + }) => { + const createResponse = await query({ + query: createQuery, + variables: { input: singleTickData }, + userUuid, + roles: ['user_admin'] + }) + + expect(createResponse.statusCode).toBe(200) + expect(createResponse.body).toBeTruthy() + expect(createResponse.body.data).toBeTruthy() + expect(createResponse.body.data.tick).toBeTruthy() + + const createTickRes = createResponse.body.data.tick + + expect(createTickRes.name).toBe(singleTickData.name) + expect(createTickRes.notes).toBe(singleTickData.notes) + expect(createTickRes.climbId).toBe(singleTickData.climbId) + expect(createTickRes.userId).toBe(singleTickData.userId) + expect(createTickRes.style).toBe(singleTickData.style) + expect(createTickRes.attemptType).toBe(singleTickData.attemptType) + expect(createTickRes.dateClimbed).toBe( + new Date(singleTickData.dateClimbed).getTime() + ) + expect(createTickRes.grade).toBe(singleTickData.grade) + expect(createTickRes.source).toBe(singleTickData.source) + expect(createTickRes._id).toBeTruthy() + + const updateResponse = await query({ + query: updateQuery, + variables: { + input: { + _id: createTickRes._id, + updatedTick: { + name: 'Updated Route One', + climbId: singleTickData.climbId, + userId: userUuid, + dateClimbed: new Date('2022-11-10T12:00:00Z'), + grade: 'new grade', + source: 'OB' + } + } + }, + userUuid, + roles: [] + }) + + expect(updateResponse.statusCode).toBe(200) + expect(updateResponse.body.data.tick.name).toBe('Updated Route One') + }) + + it('verifies date formats correctly', async ({ + singleTickData, + query, + userUuid + }) => { + const validDateTick = { + ...singleTickData, + dateClimbed: new Date('2022-11-10T15:30:00Z').getTime() + } + const validResponse = await query({ + query: createQuery, + variables: { input: validDateTick }, + userUuid, + roles: ['user_admin'] + }) + expect(validResponse.statusCode).toBe(200) + expect(validResponse.body.data.tick.dateClimbed).toBe( + new Date('2022-11-10T15:30:00Z').getTime() + ) + }) + }) +}) diff --git a/src/logger.ts b/src/logger.ts index e5694869..1fcd820a 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -13,5 +13,5 @@ const setupLogFlare = (apiKey?: string, sourceToken?: string): any | undefined = export const logger = pino({ name: 'openbeta-graphql', - level: 'info' + level: process.env.LOG_LEVEL ?? 'info' }, setupLogFlare(process.env.LOGFLARE_API_KEY, process.env.LOGFLARE_SOURCE_TOKEN)) diff --git a/src/model/MutableAreaDataSource.ts b/src/model/MutableAreaDataSource.ts index 0ba2dec2..3a7d0248 100644 --- a/src/model/MutableAreaDataSource.ts +++ b/src/model/MutableAreaDataSource.ts @@ -147,6 +147,7 @@ export default class MutableAreaDataSource extends AreaDataSource { } else { // account for a few new/unofficial countries without lat,lng in the lookup table logger.warn(`Missing lnglat for ${countryName}`) + throw Error(`Missing lnglat for ${countryName}`) } await this.validateUniqueAreaName(countryName, null) diff --git a/src/model/__tests__/AreaHistoryDataSource.test.ts b/src/model/__tests__/AreaHistoryDataSource.test.ts new file mode 100644 index 00000000..d383a6a6 --- /dev/null +++ b/src/model/__tests__/AreaHistoryDataSource.test.ts @@ -0,0 +1,167 @@ +import muuid from 'uuid-mongodb' +import { dataFixtures as it } from '../../__tests__/fixtures/data.fixtures.js' +import { AreaType, OperationType } from '../../db/AreaTypes.js' +import { BaseChangeRecordType } from '../../db/ChangeLogType.js' +import { ok } from 'assert' + +describe('Area history', () => { + it('should properly seperate unrelated histories', async ({ + changeLog, + area, + addArea, + waitForChanges + }) => { + await Promise.all([ + waitForChanges({ document: area }, async () => { + await addArea(undefined, { parent: area }) + }), + waitForChanges({ document: area }, async () => { + await addArea(undefined, { parent: area }) + }) + ]) + + const randomHistory = await changeLog.getAreaChangeSets(muuid.v4()) + expect(randomHistory).toHaveLength(0) + }) + + it('should create history records for new subareas', async ({ + changeLog, + area, + addArea, + waitForChanges, + user + }) => { + await Promise.all([ + waitForChanges({ document: area }, async () => { + await addArea(undefined, { parent: area }) + }), + waitForChanges({ document: area }, async () => { + await addArea(undefined, { parent: area }) + }) + ]) + + const initialHistory = await changeLog.getAreaChangeSets( + area.metadata.area_id + ) + const nvAreaHistory: Array> = + initialHistory[1].changes + + // verify change history linking + expect( + nvAreaHistory[0].fullDocument._change?.historyId.equals( + initialHistory[0]._id + ) + ) // should point to current change + + expect( + nvAreaHistory[0].fullDocument._change?.prevHistoryId + ).not.toBeDefined() // new document -> no previous history + + expect(nvAreaHistory[1].dbOp).toEqual('update') // add area to country.children[] + expect(nvAreaHistory[1].fullDocument.area_name).toEqual(area?.area_name) + + // verify change history linking + // 2nd change record: parent (country) + expect( + nvAreaHistory[1].fullDocument._change?.historyId.equals( + initialHistory[0]._id + ) + ) // should point to current change + expect( + nvAreaHistory[1].fullDocument._change?.prevHistoryId?.equals( + initialHistory[1]._id + ) + ) // should point to previous Add new area + + // Verify parent history + const parentHistory = await changeLog.getAreaChangeSets(area.metadata.area_id) + // We expect the last two operations for the parent to be the two + // 'add area' events + expect(parentHistory[0].operation).toEqual('addArea') + expect(parentHistory[1].operation).toEqual('addArea') + }) + + it('should record multiple Areas.setDestination() calls ', async ({ + user, + areas, + changeLog, + country, + waitForChanges, + area + }) => { + const areaUuid = area.metadata.area_id + await expect( + areas.setDestinationFlag(user, muuid.v4(), true) + ).rejects.toThrow() // non-existent area id. Trx won't be recorded + + await waitForChanges({ document: area, operation: OperationType.updateDestination }, async () => await areas.setDestinationFlag(user, areaUuid, true)) + await waitForChanges({ document: area, operation: OperationType.updateDestination }, async () => await areas.setDestinationFlag(user, areaUuid, false)) + const changset = await changeLog.getAreaChangeSets(areaUuid) + + expect(changset[0].operation).toEqual('updateDestination') + expect(changset[1].operation).toEqual('updateDestination') + expect(changset[2].operation).toEqual('addArea') + + expect( + changset[0].changes[0].fullDocument.metadata.isDestination + ).toStrictEqual(false) + expect( + changset[1].changes[0].fullDocument.metadata.isDestination + ).toStrictEqual(true) + expect( + changset[2].changes[0].fullDocument.metadata.isDestination + ).toStrictEqual(false) // default + }) + + it('should record an Areas.deleteArea() call', async ({ + user, + areas, + changeLog, + area, + waitForChanges + }) => { + await waitForChanges({ document: area, operation: OperationType.deleteArea }, async () => { + await areas.deleteArea(user, area.metadata.area_id) + }) + + const history = await changeLog.getAreaChangeSets(area.metadata.area_id) + + expect(history.map(i => i.operation)).toContain(OperationType.addArea) + expect(history.map(i => i.operation)).toContain(OperationType.deleteArea) + + const addRef = history.find(i => i.operation === OperationType.addArea) + const deleteRef = history.find(i => i.operation === OperationType.deleteArea) + + ok(addRef !== undefined) + ok(deleteRef !== undefined) + + expect(history.indexOf(addRef)).toBeGreaterThan(history.indexOf(deleteRef)) + expect(history[0].changes[0].fullDocument._id).toEqual(area._id) + }) + + it('should not record a failed Areas.deleteArea() call', async ({ + user, + area, + areas, + addArea, + changeLog + }) => { + const child = await addArea(undefined, { parent: area }) + // by giving this child its own child, we can create a vioalation condition if someone were + // to try and delete + await addArea(undefined, { parent: child }) + + await expect( + async () => await areas.deleteArea(user, child.metadata.area_id) + ).rejects.toThrow() + + const history = await changeLog.getAreaChangeSets(area.metadata.area_id) + + // should only have 2 entries: + // 1. Add child + // 2. Add child to that child + expect(history).toHaveLength(2) + expect(history[0].operation).toEqual('addArea') + expect(history[1].operation).toEqual('addArea') + }) +}) diff --git a/src/model/__tests__/AreaHistoryDataSource.ts b/src/model/__tests__/AreaHistoryDataSource.ts deleted file mode 100644 index 424490c4..00000000 --- a/src/model/__tests__/AreaHistoryDataSource.ts +++ /dev/null @@ -1,169 +0,0 @@ -import muuid from 'uuid-mongodb' - -import MutableAreaDataSource from '../MutableAreaDataSource.js' -import ChangeLogDataSource from '../ChangeLogDataSource.js' -import { OperationType } from '../../db/AreaTypes.js' -import inMemoryDB from '../../utils/inMemoryDB.js' -import waitForExpect from 'wait-for-expect' -import jest from 'jest-mock' - -describe('Area history', () => { - let areas: MutableAreaDataSource - let onChange: jest.Mock - const testUser = muuid.v4() - - beforeAll(async () => { - onChange = jest.fn() - await inMemoryDB.connect(onChange) - await ChangeLogDataSource.getInstance()._testRemoveAll() - - areas = MutableAreaDataSource.getInstance() - }) - - afterAll(async () => { - try { - await inMemoryDB.close() - } catch (e) { - console.log('closing mongoose', e) - } - }) - - beforeEach(async () => { - await ChangeLogDataSource.getInstance()._testRemoveAll() - onChange.mockClear() - }) - - it('should create history records for new subareas', async () => { - const usa = await areas.addCountry('usa') - const newArea = await areas.findOneAreaByUUID(usa.metadata.area_id) - expect(newArea.area_name).toEqual(usa.area_name) - - const or = await areas.addArea(testUser, 'oregon', usa.metadata.area_id) - const nv = await areas.addArea(testUser, 'nevada', usa.metadata.area_id) - - expect(nv?._id).toBeTruthy() - expect(or?._id).toBeTruthy() - - await waitForExpect(() => expect(onChange).toHaveBeenCalledTimes(5)) - const areaHistory = await ChangeLogDataSource.getInstance().getAreaChangeSets() - - expect(areaHistory).toHaveLength(2) - // verify changes in most recent order - expect(areaHistory[0].operation).toEqual(OperationType.addArea) - expect(areaHistory[1].operation).toEqual(OperationType.addArea) - - // Verify NV history - const nvAreaHistory = areaHistory[0].changes - expect(nvAreaHistory).toHaveLength(2) - - // history is shown most recent first - expect(nvAreaHistory[0].dbOp).toEqual('insert') // insert new area - expect(nvAreaHistory[0].fullDocument.area_name).toEqual(nv?.area_name) // area added to the right parent? - - // verify change history linking - expect(nvAreaHistory[0].fullDocument._change?.historyId).toEqual(areaHistory[0]._id) // should point to current change - expect(nvAreaHistory[0].fullDocument._change?.prevHistoryId).not.toBeDefined() // new document -> no previous history - - expect(nvAreaHistory[1].dbOp).toEqual('update') // add area to country.children[] - expect(nvAreaHistory[1].fullDocument.area_name).toEqual(usa?.area_name) - - expect(nvAreaHistory[1].fullDocument.children).toHaveLength(2) - expect(nvAreaHistory[1].fullDocument.children[1]).toEqual(nv?._id) // area added to parent.children[]? - - // verify change history linking - // 2nd change record: parent (country) - expect(nvAreaHistory[1].fullDocument._change?.historyId).toEqual(areaHistory[0]._id) // should point to current change - expect(nvAreaHistory[1].fullDocument._change?.prevHistoryId).toEqual(areaHistory[1]._id) // should point to previous Add new area - - // Verify OR history - const orAreaHistory = areaHistory[1].changes - expect(orAreaHistory).toHaveLength(2) - - const randomHistory = await ChangeLogDataSource.getInstance().getAreaChangeSets(muuid.v4()) - expect(randomHistory).toHaveLength(0) - - // Verify USA history - const usaHistory = await ChangeLogDataSource.getInstance().getAreaChangeSets(usa.metadata.area_id) - expect(usaHistory).toHaveLength(2) - expect(usaHistory[0].operation).toEqual('addArea') - expect(usaHistory[1].operation).toEqual('addArea') - - // Verify USA history links - expect(usaHistory[0].changes[0]) - }) - - it('should record multiple Areas.setDestination() calls ', async () => { - const canada = await areas.addCountry('can') - const squamish = await areas.addArea(testUser, 'squamish', canada.metadata.area_id) - - expect(squamish?._id).toBeTruthy() - - if (squamish != null) { - const areaUuid = squamish.metadata.area_id - await expect(areas.setDestinationFlag(testUser, muuid.v4(), true)).rejects.toThrow() // non-existent area id. Trx won't be recorded - - await areas.setDestinationFlag(testUser, areaUuid, true) - await areas.setDestinationFlag(testUser, areaUuid, false) - - await waitForExpect(() => expect(onChange).toHaveBeenCalledTimes(5)) - const changset = await ChangeLogDataSource.getInstance().getAreaChangeSets(areaUuid) - - expect(changset).toHaveLength(3) - expect(changset[0].operation).toEqual('updateDestination') - expect(changset[1].operation).toEqual('updateDestination') - expect(changset[2].operation).toEqual('addArea') - - expect(changset[0].changes[0].fullDocument.metadata.isDestination).toStrictEqual(false) - expect(changset[1].changes[0].fullDocument.metadata.isDestination).toStrictEqual(true) - expect(changset[2].changes[0].fullDocument.metadata.isDestination).toStrictEqual(false) // default - } - }) - - it('should record an Areas.deleteArea() call', async () => { - const greece = await areas.addCountry('grc') - const leonidio = await areas.addArea(testUser, 'Leonidio', greece.metadata.area_id) - - if (leonidio == null) fail() - - await areas.deleteArea(testUser, leonidio.metadata.area_id) - - await waitForExpect(() => expect(onChange).toHaveBeenCalledTimes(5)) - const history = await ChangeLogDataSource.getInstance().getAreaChangeSets(leonidio.metadata.area_id) - - expect(history).toHaveLength(2) - expect(history[0].operation).toEqual('deleteArea') - expect(history[1].operation).toEqual('addArea') - - expect(history[0].changes[0].fullDocument._id).toEqual(leonidio._id) - }) - - it('should not record a failed Areas.deleteArea() call', async () => { - const spain = await areas.addCountry('esp') - const margalef = await areas.addArea(testUser, 'margalef', spain.metadata.area_id) - - if (margalef == null) fail() - - const newChild = await areas.addArea(testUser, 'One', margalef.metadata.area_id) - - if (newChild == null) fail() - - let deleted = false - try { - await areas.deleteArea(testUser, margalef.metadata.area_id) - fail('Shouldn\'t allow deletion when the area still has subareas') - } catch (e) { - deleted = true - } - - expect(deleted).toBeTruthy() - - await waitForExpect(() => expect(onChange).toHaveBeenCalledTimes(5)) - const history = await ChangeLogDataSource.getInstance().getAreaChangeSets(spain.metadata.area_id) - - // should only have 2 entries: - // 1. Add country - // 2. Add child to country - expect(history).toHaveLength(1) - expect(history[0].operation).toEqual('addArea') - }) -}) diff --git a/src/model/__tests__/AreaUtils.ts b/src/model/__tests__/AreaUtils.test.ts similarity index 73% rename from src/model/__tests__/AreaUtils.ts rename to src/model/__tests__/AreaUtils.test.ts index 0f30849b..306c87eb 100644 --- a/src/model/__tests__/AreaUtils.ts +++ b/src/model/__tests__/AreaUtils.test.ts @@ -1,4 +1,4 @@ -describe('Test area utilities', () => { +describe.todo('Test area utilities', () => { test.todo('The name comparison code unit') test.todo('The name-uniqueness system with other side-effects stripped out') }) diff --git a/src/model/__tests__/BulkDataSource.test.ts b/src/model/__tests__/BulkDataSource.test.ts index c9e8744f..91f606dc 100644 --- a/src/model/__tests__/BulkDataSource.test.ts +++ b/src/model/__tests__/BulkDataSource.test.ts @@ -1,233 +1,196 @@ -import {ChangeStream} from 'mongodb'; -import muuid from 'uuid-mongodb'; -import ChangeLogDataSource from '../ChangeLogDataSource.js'; -import MutableClimbDataSource from '../MutableClimbDataSource.js'; -import {AreaType} from '../../db/AreaTypes.js'; -import {ClimbType} from '../../db/ClimbTypes.js'; -import streamListener from '../../db/edit/streamListener.js'; -import inMemoryDB from "../../utils/inMemoryDB.js"; -import {isFulfilled} from "../../utils/testUtils.js"; -import BulkImportDataSource from "../BulkImportDataSource.js"; -import {BulkImportAreaInputType, BulkImportResultType} from "../../db/BulkImportTypes.js"; - -describe('bulk import e2e', () => { - let bulkImport: BulkImportDataSource; - let climbs: MutableClimbDataSource; - let stream: ChangeStream; - const testUser = muuid.v4(); - - const assertBulkImport = async (...input: BulkImportAreaInputType[]): Promise => { - const result = await bulkImport.bulkImport({ - user: testUser, - input: {areas: input}, - climbs - }); - - const addedAreas = await Promise.allSettled( - result.addedAreas.map((area) => - bulkImport.findOneAreaByUUID(area.metadata.area_id) +import { AreaType } from '../../db/AreaTypes.js' +import { ClimbType } from '../../db/ClimbTypes.js' +import { isFulfilled } from '../../utils/testUtils.js' +import { BulkImportAreaInputType, BulkImportResultType } from '../../db/BulkImportTypes.js' +import { dataFixtures } from '../../__tests__/fixtures/data.fixtures.js' + +interface LocalContext { + assertBulkImport: (...input: BulkImportAreaInputType[]) => Promise +} + +const it = dataFixtures.extend({ + assertBulkImport: async ({ climbs, user, bulkImport }, use) => { + const assertBulkImport = async (...input: BulkImportAreaInputType[]): Promise => { + const result = await bulkImport.bulkImport({ + user, + input: { areas: input }, + climbs + }) + + const addedAreas = await Promise.allSettled( + result.addedAreas.map(async (area) => + await bulkImport.findOneAreaByUUID(area.metadata.area_id) + ) ) - ); - const updatedAreas = await Promise.allSettled( - result.updatedAreas.map((area) => - bulkImport.findOneAreaByUUID(area.metadata.area_id) + const updatedAreas = await Promise.allSettled( + result.updatedAreas.map(async (area) => + await bulkImport.findOneAreaByUUID(area.metadata.area_id) + ) + ) + const addedOrUpdatedClimbs = await Promise.allSettled( + result.addedOrUpdatedClimbs.map(async (climb) => await climbs.findOneClimbByMUUID(climb._id)) ) - ); - const addedOrUpdatedClimbs = await Promise.allSettled( - result.addedOrUpdatedClimbs.map((climb) => climbs.findOneClimbByMUUID(climb._id)) - ); - - return { - addedAreas: addedAreas.filter(isFulfilled).map((p) => p.value), - updatedAreas: updatedAreas.filter(isFulfilled).map((p) => p.value), - addedOrUpdatedClimbs: addedOrUpdatedClimbs.filter(isFulfilled).map((p) => p.value as ClimbType), - }; - }; - - beforeAll(async () => { - await inMemoryDB.connect() - stream = await streamListener(); - }); - afterAll(async () => { - try { - await stream.close(); - await inMemoryDB.close() - } catch (e) { - console.log('error closing mongoose', e); + return { + addedAreas: addedAreas.filter(isFulfilled).map((p) => p.value), + updatedAreas: updatedAreas.filter(isFulfilled).map((p) => p.value), + addedOrUpdatedClimbs: addedOrUpdatedClimbs.filter(isFulfilled).map((p) => p.value as ClimbType) + } } - }); - - beforeEach(async () => { - bulkImport = BulkImportDataSource.getInstance(); - climbs = MutableClimbDataSource.getInstance(); - - await bulkImport.addCountry('us'); - }); - - afterEach(async () => { - await ChangeLogDataSource.getInstance()._testRemoveAll(); - await inMemoryDB.clear() - }); + await use(assertBulkImport) + } +}) +describe('bulk import e2e', () => { describe('adding new areas and climbs', () => { - it('should commit a new minimal area to the database', async () => { + it('should commit a new minimal area to the database', async ({ assertBulkImport, country }) => { await expect( assertBulkImport({ areaName: 'Minimal Area', - countryCode: 'us', + countryCode: country.shortCode }) ).resolves.toMatchObject({ addedAreas: [ { area_name: 'Minimal Area', - gradeContext: 'US', + gradeContext: country.gradeContext, metadata: { leaf: false, - isBoulder: false, - }, - }, - ], - }); - }); + isBoulder: false + } + } + ] + }) + }) - it('should rollback when one of the areas fails to import', async () => { + it('should rollback when one of the areas fails to import', async ({ assertBulkImport, country }) => { await expect( assertBulkImport( { areaName: 'Test Area', - countryCode: 'us', + countryCode: country.shortCode }, { - areaName: 'Test Area 2', + areaName: 'Test Area 2' } ) - ).rejects.toThrowError("Must provide parent Id or country code"); - }); + ).rejects.toThrowError('Must provide parent Id or country code') + }) - it('should import nested areas with children', async () => { + it('should import nested areas with children', async ({ assertBulkImport, country }) => { await expect( assertBulkImport({ areaName: 'Parent Area', - countryCode: 'us', + countryCode: country.shortCode, children: [ { - areaName: 'Child Area 2', - }, - ], + areaName: 'Child Area 2' + } + ] }) ).resolves.toMatchObject({ addedAreas: [ - {area_name: 'Parent Area', gradeContext: 'US'}, - {area_name: 'Child Area 2', gradeContext: 'US'}, - ] as Partial[], - }); - }); + { area_name: 'Parent Area', gradeContext: country.gradeContext }, + { area_name: 'Child Area 2', gradeContext: country.gradeContext } + ] as Array> + }) + }) - it('should import nested areas with children and grandchildren', async () => { + it('should import nested areas with children and grandchildren', async ({ assertBulkImport, country }) => { await expect( assertBulkImport({ areaName: 'Test Area', - countryCode: 'us', + countryCode: country.shortCode, children: [ { areaName: 'Test Area 2', children: [ { - areaName: 'Test Area 3', - }, - ], - }, - ], + areaName: 'Test Area 3' + } + ] + } + ] }) ).resolves.toMatchObject({ addedAreas: [ { area_name: 'Test Area', - pathTokens: ['United States of America', 'Test Area'], + pathTokens: [country.area_name, 'Test Area'] }, { area_name: 'Test Area 2', pathTokens: [ - 'United States of America', + country.area_name, 'Test Area', - 'Test Area 2', - ], + 'Test Area 2' + ] }, { area_name: 'Test Area 3', pathTokens: [ - 'United States of America', + country.area_name, 'Test Area', 'Test Area 2', - 'Test Area 3', - ], - }, - ] as Partial[], - }); - }); + 'Test Area 3' + ] + } + ] as Array> + }) + }) - it('should import leaf areas with climbs', async () => { + it('should import leaf areas with climbs', async ({ assertBulkImport, country }) => { await expect( assertBulkImport({ areaName: 'Test Area', - countryCode: 'us', + countryCode: country.shortCode, climbs: [ { name: 'Test Climb', grade: '5.10a', - disciplines: {sport: true}, - }, - ], + disciplines: { sport: true } + } + ] }) ).resolves.toMatchObject({ addedAreas: [ { area_name: 'Test Area', - gradeContext: 'US', + gradeContext: country.gradeContext, metadata: { leaf: true, - isBoulder: false, + isBoulder: false }, climbs: [{ name: 'Test Climb', grades: { - yds: '5.10a', - }, - }], - }, + yds: '5.10a' + } + }] + } ], addedOrUpdatedClimbs: [ { name: 'Test Climb', grades: { - yds: '5.10a', - }, - }, - ], - }); - }); - }); + yds: '5.10a' + } + } + ] + }) + }) + }) describe('updating existing areas', () => { - let area: AreaType; - beforeEach(async () => { - const result = await assertBulkImport({ - areaName: 'Existing Area', - countryCode: 'us', - }); - area = result.addedAreas[0] as AreaType; - }); - - it('should update an existing area', async () => { + it('should update an existing area', async ({ assertBulkImport, area }) => { await expect( assertBulkImport({ uuid: area.metadata.area_id, - areaName: 'New Name', + areaName: 'New Name' }) ).resolves.toMatchObject({ - updatedAreas: [{area_name: 'New Name'}], - }); - }); - }); -}); + updatedAreas: [{ area_name: 'New Name' }] + }) + }) + }) +}) diff --git a/src/model/__tests__/ChangeLogDS.test.ts b/src/model/__tests__/ChangeLogDS.test.ts new file mode 100644 index 00000000..5b6055a0 --- /dev/null +++ b/src/model/__tests__/ChangeLogDS.test.ts @@ -0,0 +1,20 @@ +import muuid from 'uuid-mongodb' +import { getChangeLogModel } from '../../db/index.js' +import { OpType } from '../../db/ChangeLogType.js' +import { OperationType } from '../../db/AreaTypes.js' +import { dbTest as it } from '../../__tests__/fixtures/mongo.fixtures.js' + +describe('Area history', () => { + it('should create a change record', async ({ changeLog }) => { + const userId = muuid.v4() + const op: OpType = OperationType.addCountry + + const session = await getChangeLogModel().startSession() + const ret = await changeLog.create(session, userId, op) + + expect(ret._id).toBeDefined() + expect(ret.editedBy).toEqual(userId) + expect(ret.operation).toEqual(op) + expect(ret.changes).toHaveLength(0) + }) +}) diff --git a/src/model/__tests__/ChangeLogDS.ts b/src/model/__tests__/ChangeLogDS.ts deleted file mode 100644 index e722867e..00000000 --- a/src/model/__tests__/ChangeLogDS.ts +++ /dev/null @@ -1,42 +0,0 @@ -import muuid from 'uuid-mongodb' -import { getAreaModel, getChangeLogModel } from '../../db/index.js' -import ChangeLogDataSource from '../ChangeLogDataSource.js' -import { OpType } from '../../db/ChangeLogType.js' -import { OperationType } from '../../db/AreaTypes.js' - -import { logger } from '../../logger.js' -import inMemoryDB from '../../utils/inMemoryDB.js' - -describe('Area history', () => { - let changeLog: ChangeLogDataSource - - beforeAll(async () => { - await inMemoryDB.connect() - - try { - await getAreaModel().collection.drop() - await getChangeLogModel().collection.drop() - } catch (e) { - logger.info('Expected exception') - } - - changeLog = ChangeLogDataSource.getInstance() - }) - - afterAll(async () => { - await inMemoryDB.close() - }) - - it('should create a change record', async () => { - const userId = muuid.v4() - const op: OpType = OperationType.addCountry - - const session = await getChangeLogModel().startSession() - const ret = await changeLog.create(session, userId, op) - - expect(ret._id).toBeDefined() - expect(ret.editedBy).toEqual(userId) - expect(ret.operation).toEqual(op) - expect(ret.changes).toHaveLength(0) - }) -}) diff --git a/src/model/__tests__/MediaDataSource.ts b/src/model/__tests__/MediaDataSource.test.ts similarity index 58% rename from src/model/__tests__/MediaDataSource.ts rename to src/model/__tests__/MediaDataSource.test.ts index a99dea54..640d0617 100644 --- a/src/model/__tests__/MediaDataSource.ts +++ b/src/model/__tests__/MediaDataSource.test.ts @@ -1,10 +1,5 @@ import mongoose from 'mongoose' -import muuid, { MUUID } from 'uuid-mongodb' -import MutableMediaDataSource from '../MutableMediaDataSource.js' -import AreaDataSource from '../MutableAreaDataSource.js' -import ClimbDataSource from '../MutableClimbDataSource.js' - -import { createIndexes } from '../../db/index.js' +import muuid from 'uuid-mongodb' import { AreaType } from '../../db/AreaTypes.js' import { AddTagEntityInput, @@ -15,119 +10,76 @@ import { AreaMediaQueryInput, ClimbMediaQueryInput } from '../../db/MediaObjectTypes.js' -import { newSportClimb1 } from './MutableClimbDataSource.js' -import inMemoryDB from '../../utils/inMemoryDB.js' - -const TEST_MEDIA: MediaObjectGQLInput = { - userUuid: 'a2eb6353-65d1-445f-912c-53c6301404bd', - mediaUrl: '/u/a2eb6353-65d1-445f-912c-53c6301404bd/photo1.jpg', - width: 800, - height: 600, - format: 'jpeg', - size: 45000 -} - -describe('MediaDataSource', () => { - let media: MutableMediaDataSource - let areas: AreaDataSource - let climbs: ClimbDataSource - - let areaForTagging1: AreaType - let areaForTagging2: AreaType - let areaForTagging3: AreaType - let climbIdForTagging: MUUID +import { gqlTest } from '../../__tests__/fixtures/gql.fixtures.js' +import { muuidToString } from '../../utils/helpers.js' - let areaTag1: AddTagEntityInput - let areaTag2: AddTagEntityInput - let climbTag: AddTagEntityInput - - let testMediaObject: MediaObject - - beforeAll(async () => { - await inMemoryDB.connect() - - areas = AreaDataSource.getInstance() - climbs = ClimbDataSource.getInstance() - media = MutableMediaDataSource.getInstance() - }) +interface LocalContext { + mediaInput: MediaObjectGQLInput + mediaObject: MediaObject + otherArea: AreaType - beforeEach(async () => { - try { - await areas.areaModel.collection.drop() - await climbs.climbModel.collection.drop() - await media.mediaObjectModel.collection.drop() - } catch (e) { - console.log('Cleaning up db before test') - } - - await createIndexes() - - await areas.addCountry('USA') - areaForTagging1 = await areas.addArea(muuid.v4(), 'Yosemite NP', null, 'USA') - areaForTagging2 = await areas.addArea(muuid.v4(), 'Lake Tahoe', null, 'USA') - areaForTagging3 = await areas.addArea(muuid.v4(), 'Shelf Road', null, 'USA') - if (areaForTagging1 == null || areaForTagging2 == null || areaForTagging3 == null) fail('Fail to pre-seed test areas') - - const rs = await climbs.addOrUpdateClimbs(muuid.v4(), areaForTagging1.metadata.area_id, [newSportClimb1]) - if (rs == null) fail('Fail to pre-seed test climbs') - climbIdForTagging = muuid.from(rs[0]) - - const rs2 = await media.addMediaObjects([TEST_MEDIA]) - testMediaObject = rs2[0] - if (testMediaObject == null) { - fail('Fail to create test media') - } - - areaTag1 = { - mediaId: testMediaObject._id, - entityType: 1, - entityUuid: areaForTagging1.metadata.area_id - } - - areaTag2 = { - mediaId: testMediaObject._id, - entityType: 1, - entityUuid: areaForTagging2.metadata.area_id, - topoData: { name: 'AA', value: '1234' } - } - - climbTag = { - mediaId: testMediaObject._id, - entityType: 0, - entityUuid: climbIdForTagging - } - }) + areaTag1: AddTagEntityInput + areaTag2: AddTagEntityInput + climbTag: AddTagEntityInput +} - afterAll(async () => { - await inMemoryDB.close() +const it = gqlTest.extend({ + mediaInput: async ({ task, userUuid }, use) => await use({ + userUuid, + mediaUrl: `/u/${userUuid}/${task.id}.jpg`, + width: 800, + height: 600, + format: 'jpeg', + size: 45000 + }), + otherArea: async ({ addArea }, use) => await use(await addArea()), + mediaObject: async ({ media, mediaInput }, use) => await use((await media.addMediaObjects([mediaInput]))[0]), + + areaTag1: async ({ area, mediaObject }, use) => await use({ + mediaId: mediaObject._id, + entityType: 1, + entityUuid: area.metadata.area_id + }), + + areaTag2: async ({ otherArea, mediaObject }, use) => await use({ + mediaId: mediaObject._id, + entityType: 1, + entityUuid: otherArea.metadata.area_id, + topoData: { name: 'AA', value: '1234' } + }), + + climbTag: async ({ climb, mediaObject }, use) => await use({ + mediaId: mediaObject._id, + entityType: 0, + entityUuid: climb._id }) +}) - it('should not tag a nonexistent area', async () => { +describe('MediaDataSource', () => { + it('should not tag a nonexistent area', async ({ media, mediaObject }) => { const badAreaTag: AddTagEntityInput = { - mediaId: testMediaObject._id, + mediaId: mediaObject._id, entityType: 1, entityUuid: muuid.v4() // some random area } await expect(media.upsertEntityTag(badAreaTag)).rejects.toThrow(/area .* not found/i) }) - it('should not tag a nonexistent *climb*', async () => { + it('should not tag a nonexistent *climb*', async ({ media, mediaObject }) => { const badClimbTag: AddTagEntityInput = { - mediaId: testMediaObject._id, + mediaId: mediaObject._id, entityType: 0, entityUuid: muuid.v4() // some random climb } await expect(media.upsertEntityTag(badClimbTag)).rejects.toThrow(/climb .* not found/i) }) - it('should tag & remove an area tag', async () => { - if (areaForTagging1 == null) fail('Pre-seeded test area not found') - + it('should tag & remove an area tag', async ({ media, mediaInput, areaTag1, climbTag, area, climb }) => { // verify the number tags before test - let mediaObjects = await media.getOneUserMedia(TEST_MEDIA.userUuid, 10) + let mediaObjects = await media.getOneUserMedia(mediaInput.userUuid, 10) expect(mediaObjects[0].entityTags).toHaveLength(0) - // add 1st tag + // add 1climbNamest tag await media.upsertEntityTag(areaTag1) // add 2nd tag @@ -136,14 +88,14 @@ describe('MediaDataSource', () => { expect(tag).toMatchObject>({ targetId: climbTag.entityUuid, type: climbTag.entityType, - areaName: areaForTagging1.area_name, - ancestors: areaForTagging1.ancestors, - climbName: newSportClimb1.name, - lnglat: areaForTagging1.metadata.lnglat + areaName: area.area_name, + ancestors: area.ancestors, + climbName: climb.name, + lnglat: area.metadata.lnglat }) // verify the number tags - mediaObjects = await media.getOneUserMedia(TEST_MEDIA.userUuid, 10) + mediaObjects = await media.getOneUserMedia(mediaInput.userUuid, 10) expect(mediaObjects[0].entityTags).toHaveLength(2) // remove tag @@ -151,14 +103,14 @@ describe('MediaDataSource', () => { expect(res).toBe(true) // verify the number tags - mediaObjects = await media.getOneUserMedia(TEST_MEDIA.userUuid, 10) + mediaObjects = await media.getOneUserMedia(mediaInput.userUuid, 10) expect(mediaObjects[0].entityTags).toHaveLength(1) }) - it('should handle delete tag errors gracefully', async () => { + it('should handle delete tag errors gracefully', async ({ media, mediaObject }) => { // with invalid id format await expect(media.removeEntityTag({ - mediaId: testMediaObject._id, + mediaId: mediaObject._id, // @ts-expect-error tagId: 'abc' // bad ObjectId format })).rejects.toThrowError(/Cast to ObjectId failed/i) @@ -170,28 +122,26 @@ describe('MediaDataSource', () => { })).rejects.toThrowError(/not found/i) }) - it('should not add a duplicate tag', async () => { + it('should not add a duplicate tag', async ({ areaTag2, media }) => { const updating = { ...areaTag2, topoData: { name: 'ZZ' } } const newTag = await media.upsertEntityTag(updating) expect(newTag.targetId).toEqual(areaTag2.entityUuid) expect(newTag.topoData).toEqual(updating.topoData) }) - it('should not add media with the same url', async () => { + it('should not add media with the same url', async ({ mediaInput, media }) => { const mediaObj = { - ...TEST_MEDIA, + ...mediaInput, mediaUrl: 'photoAAA.jpg' } await media.addMediaObjects([mediaObj]) - const rs2 = await expect(media.addMediaObjects([mediaObj])).rejects.toThrowError(/duplicate key error collection/i) - - expect(rs2).toBeUndefined() + await expect(async () => await media.addMediaObjects([mediaObj])).rejects.toThrowError('duplicate key error collection') }) - it('should delete media', async () => { + it('should delete media', async ({ mediaInput, media }) => { const rs = await media.addMediaObjects([{ - ...TEST_MEDIA, + ...mediaInput, mediaUrl: 'u/a0ca9ebb-aa3b-4bb0-8ddd-7c8b2ed228a5/photo100.jpg' }]) @@ -200,24 +150,24 @@ describe('MediaDataSource', () => { const rs2 = await media.deleteMediaObject(rs[0]._id) expect(rs2).toBe(true) - await expect(media.deleteMediaObject(rs[0]._id)).rejects.toThrowError(/not found/i) + await expect(async () => await media.deleteMediaObject(rs[0]._id)).rejects.toThrowError('not found') }) - it('should not delete media with non-empty tags', async () => { + it('should not delete media with non-empty tags', async ({ mediaInput, media, climb }) => { const rs = await media.addMediaObjects([{ - ...TEST_MEDIA, + ...mediaInput, mediaUrl: 'photo101.jpg', - entityTag: { entityType: 0, entityId: climbIdForTagging.toUUID().toString() } + entityTag: { entityType: 0, entityId: climb._id.toUUID().toString() } } ]) - await expect(media.deleteMediaObject(rs[0]._id)).rejects.toThrowError(/Cannot delete media object with non-empty tags./i) + await expect(async () => await media.deleteMediaObject(rs[0]._id)).rejects.toThrowError('Cannot delete media object with non-empty tags.') }) - it('should return paginated user media results', async () => { + it('should return paginated user media results', async ({ mediaInput, media }) => { const ITEMS_PER_PAGE = 3 const MEDIA_TEMPLATE: MediaObjectGQLInput = { - ...TEST_MEDIA, + ...mediaInput, userUuid: 'a0ca9ebb-aa3b-4bb0-8ddd-7c8b2ed228a5' } @@ -233,9 +183,7 @@ describe('MediaDataSource', () => { const expectedMedia = await media.addMediaObjects(newMediaListInput) - if (expectedMedia == null) { - fail('Seeding test media fail') - } + assert(expectedMedia != null, 'seeding test media fail') // reverse because getOneUserMediaPagination() returns most recent first expectedMedia.reverse() @@ -270,10 +218,10 @@ describe('MediaDataSource', () => { verifyPageData(page3, MEDIA_TEMPLATE.userUuid, 'userUuid', expectedMedia.slice(6, 7), mediaCount, 1, false) }) - it('should return paginated area media results', async () => { + it('should return paginated area media results', async ({ mediaInput, area, media }) => { const ITEMS_PER_PAGE = 3 const MEDIA_TEMPLATE: MediaObjectGQLInput = { - ...TEST_MEDIA, + ...mediaInput, userUuid: 'a0ca9ebb-aa3b-4bb0-8ddd-7c8b2ed228a6' } @@ -289,54 +237,52 @@ describe('MediaDataSource', () => { mediaUrl: `/areaPhoto${i}.jpg`, entityTag: { entityType: 1, - entityId: areaForTagging3.metadata.area_id.toUUID().toString() + entityId: area.metadata.area_id.toUUID().toString() } }) } const expectedMedia = await media.addMediaObjects(newMediaListInput) - if (expectedMedia == null) { - fail('Seeding test media fail') - } + assert(expectedMedia !== null) // reverse because getOneAreaMediaPagination() returns most recent first expectedMedia.reverse() const input: AreaMediaQueryInput = { - areaUuid: muuid.from(areaForTagging3.metadata.area_id), + areaUuid: muuid.from(area.metadata.area_id), first: ITEMS_PER_PAGE } const page1 = await media.getOneAreaMediaPagination(input) - verifyPageData(page1, areaForTagging3.metadata.area_id.toString(), 'areaUuid', expectedMedia.slice(0, 3), mediaCount, ITEMS_PER_PAGE, true) + verifyPageData(page1, area.metadata.area_id.toString(), 'areaUuid', expectedMedia.slice(0, 3), mediaCount, ITEMS_PER_PAGE, true) const page1Edges = page1.mediaConnection.edges const input2: AreaMediaQueryInput = { - areaUuid: muuid.from(areaForTagging3.metadata.area_id), + areaUuid: muuid.from(area.metadata.area_id), first: ITEMS_PER_PAGE, after: page1Edges[page1Edges.length - 1].cursor } const page2 = await media.getOneAreaMediaPagination(input2) - verifyPageData(page2, areaForTagging3.metadata.area_id.toString(), 'areaUuid', expectedMedia.slice(3, 6), mediaCount, ITEMS_PER_PAGE, true) + verifyPageData(page2, area.metadata.area_id.toString(), 'areaUuid', expectedMedia.slice(3, 6), mediaCount, ITEMS_PER_PAGE, true) const page2Edges = page2.mediaConnection.edges const input3: AreaMediaQueryInput = { - areaUuid: muuid.from(areaForTagging3.metadata.area_id), + areaUuid: muuid.from(area.metadata.area_id), first: ITEMS_PER_PAGE, after: page2Edges[page2Edges.length - 1].cursor } const page3 = await media.getOneAreaMediaPagination(input3) - verifyPageData(page3, areaForTagging3.metadata.area_id.toString(), 'areaUuid', expectedMedia.slice(6, 7), mediaCount, 1, false) + verifyPageData(page3, area.metadata.area_id.toString(), 'areaUuid', expectedMedia.slice(6, 7), mediaCount, 1, false) }) - it('should return paginated climb media results', async () => { + it('should return paginated climb media results', async ({ mediaInput, climb, media }) => { const ITEMS_PER_PAGE = 4 const MEDIA_TEMPLATE: MediaObjectGQLInput = { - ...TEST_MEDIA, + ...mediaInput, userUuid: 'a0ca9ebb-aa3b-4bb0-8ddd-7c8b2ed228a7' } @@ -352,48 +298,46 @@ describe('MediaDataSource', () => { mediaUrl: `/climbPhoto${i}.jpg`, entityTag: { entityType: 0, - entityId: climbIdForTagging.toUUID().toString() + entityId: climb._id.toUUID().toString() } }) } const expectedMedia = await media.addMediaObjects(newMediaListInput) - if (expectedMedia == null) { - fail('Seeding test media fail') - } + assert(expectedMedia !== null) // reverse because getOneClimbMediaPagination() returns most recent first expectedMedia.reverse() const input: ClimbMediaQueryInput = { - climbUuid: muuid.from(climbIdForTagging), + climbUuid: muuid.from(climb._id), first: ITEMS_PER_PAGE } const page1 = await media.getOneClimbMediaPagination(input) - verifyPageData(page1, climbIdForTagging.toString(), 'climbUuid', expectedMedia.slice(0, 4), mediaCount, ITEMS_PER_PAGE, true) + verifyPageData(page1, muuidToString(climb._id), 'climbUuid', expectedMedia.slice(0, 4), mediaCount, ITEMS_PER_PAGE, true) const page1Edges = page1.mediaConnection.edges const input2: ClimbMediaQueryInput = { - climbUuid: muuid.from(climbIdForTagging), + climbUuid: climb._id, first: ITEMS_PER_PAGE, after: page1Edges[page1Edges.length - 1].cursor } const page2 = await media.getOneClimbMediaPagination(input2) - verifyPageData(page2, climbIdForTagging.toString(), 'climbUuid', expectedMedia.slice(4, 8), mediaCount, ITEMS_PER_PAGE, true) + verifyPageData(page2, muuidToString(climb._id), 'climbUuid', expectedMedia.slice(4, 8), mediaCount, ITEMS_PER_PAGE, true) const page2Edges = page2.mediaConnection.edges const input3: ClimbMediaQueryInput = { - climbUuid: muuid.from(climbIdForTagging), + climbUuid: climb._id, first: ITEMS_PER_PAGE, after: page2Edges[page2Edges.length - 1].cursor } const page3 = await media.getOneClimbMediaPagination(input3) - verifyPageData(page3, climbIdForTagging.toString(), 'climbUuid', expectedMedia.slice(8, 11), mediaCount, 3, false) + verifyPageData(page3, muuidToString(climb._id), 'climbUuid', expectedMedia.slice(8, 11), mediaCount, 3, false) }) }) diff --git a/src/model/__tests__/MutableAreaDataSource.test.ts b/src/model/__tests__/MutableAreaDataSource.test.ts index 3d48c035..5450c37a 100644 --- a/src/model/__tests__/MutableAreaDataSource.test.ts +++ b/src/model/__tests__/MutableAreaDataSource.test.ts @@ -1,189 +1,139 @@ -import { GraphQLError } from "graphql" -import { getAreaModel, createIndexes } from "../../db" -import inMemoryDB from "../../utils/inMemoryDB" -import MutableAreaDataSource from "../MutableAreaDataSource" -import muid, { MUUID } from 'uuid-mongodb' -import { AreaType, OperationType } from "../../db/AreaTypes" -import { ChangeRecordMetadataType } from "../../db/ChangeLogType" - - -describe("Test area mutations", () => { - let areas: MutableAreaDataSource - let rootCountry: AreaType - let areaCounter = 0 - const testUser = muid.v4() - - async function addArea(name?: string, extra?: Partial<{ leaf: boolean, boulder: boolean, parent: MUUID | AreaType}>) { - function isArea(x: any): x is AreaType { - return typeof x.metadata?.area_id !== 'undefined' - } - - areaCounter += 1 - if (name === undefined || name === 'test') { - name = process.uptime().toString() + '-' + areaCounter.toString() - } - - let parent: MUUID | undefined = undefined - if (extra?.parent) { - if (isArea(extra.parent)) { - parent = extra.parent.metadata?.area_id - } else { - parent = extra.parent - } - } - - return areas.addArea( - testUser, - name, - parent ?? rootCountry.metadata.area_id, - undefined, - undefined, - extra?.leaf, - extra?.boulder - ) - } - - beforeAll(async () => { - await inMemoryDB.connect() - await getAreaModel().collection.drop() - await createIndexes() - - areas = MutableAreaDataSource.getInstance() - // We need a root country, and it is beyond the scope of these tests - rootCountry = await areas.addCountry("USA") - }) - - afterAll(inMemoryDB.close) - - describe("Add area param cases", () => { - test("Add a simple area with no specifications using a parent UUID", () => areas - .addArea(testUser, 'Texas2', rootCountry.metadata.area_id) - .then(area => { - expect(area?._change).toMatchObject({ - user: testUser, - operation: OperationType.addArea, - } satisfies Partial) - })) - - test("Add an area with an unknown UUID parent should fail", - async () => await expect(() => areas.addArea(testUser, 'Texas', muid.v4())).rejects.toThrow()) - - test("Add a simple area with no specifications using a country code", () => areas.addArea(testUser, 'Texas part 2', null, 'USA') - .then(texas => areas.addArea(testUser, 'Texas Child', texas.metadata.area_id))) - - test("Add a simple area, then specify a new child one level deep", () => addArea('California') - .then(async parent => { - let child = await addArea('Child', { parent }) - expect(child).toMatchObject({ area_name: 'Child' }) - let parentCheck = await areas.findOneAreaByUUID(parent.metadata.area_id) - expect(parentCheck?.children ?? []).toContainEqual(child._id) - })) - - test("Add a leaf area", () => addArea('Somewhere').then(parent => addArea('Child', { leaf: true, parent })) - .then(async leaf => { - expect(leaf).toMatchObject({ metadata: { leaf: true }}) - let area = await areas.areaModel.findById(leaf._id) - expect(area).toMatchObject({ metadata: { leaf: true }}) - })) - - test("Add a leaf area that is a boulder", () => addArea('Maine') - .then(parent => addArea('Child', {leaf: true, boulder: true, parent} )) - .then(area => { - expect(area).toMatchObject({ - metadata: { - leaf: true, - isBoulder: true, - }, - } satisfies Partial & { metadata: Partial}>) - })) - - test("Add a NON-leaf area that is a boulder", () => addArea('Wisconcin') - .then(texas => addArea('Child', { leaf: false, boulder: true })) - .then(area => { - expect(area).toMatchObject({ - metadata: { - // Even though we specified false to leaf on the input, we expect it to be true - // after write because a boulder cannot contain sub-areas - leaf: true, - isBoulder: true, - }, - } satisfies Partial & { metadata: Partial}>) - })) - - test("Adding a child to a leaf area should cause it to become a normal area", () => addArea() - .then(parent => Promise.all(new Array(5).map(() => addArea('test', { leaf: true, parent } )))) - .then(([leaf]) => leaf) - .then(leaf => addArea('test', { parent: leaf })) - .then(leaf => expect(leaf).toMatchObject({ metadata: { leaf: false }}))) - - test("area names should be unique in their parent context", () => addArea('test').then(async parent => { - await addArea('Big ol boulder', { parent }) - await expect(() => addArea('Big ol boulder', { parent })).rejects.toThrow(GraphQLError) - })) - }) - - test("Delete Area", () => addArea("test").then(area => areas.deleteArea(testUser, area.metadata.area_id)).then(async deleted => { - expect(deleted).toBeDefined() - // TODO: this test fails based on the data returned, which appears to omit the _deleting field. - let d = await areas.areaModel.findById(deleted?._id) - - expect(d).toBeDefined() - expect(d).not.toBeNull() - expect(d?._deleting).toBeDefined() +import { GraphQLError } from 'graphql' +import muid from 'uuid-mongodb' +import { AreaType, OperationType } from '../../db/AreaTypes' +import { ChangeRecordMetadataType } from '../../db/ChangeLogType' +import { dataFixtures as test } from '../../__tests__/fixtures/data.fixtures' +import { ok } from 'assert' + +describe('Test area mutations', () => { + describe('Add area param cases', () => { + test('Add a simple area with no specifications using a parent UUID', async ({ areas, user, country }) => await areas + .addArea(user, 'Texas2', country.metadata.area_id) + .then(area => { + expect(area?._change).toMatchObject({ + user, + operation: OperationType.addArea + } satisfies Partial) })) - test("Delete Area that is already deleted should throw", () => addArea("test") - .then(area => areas.deleteArea(testUser, area.metadata.area_id)) - .then(async area => { - expect(area).not.toBeNull() - await expect(() => areas.deleteArea(testUser, area!.metadata.area_id)).rejects.toThrow() - })) - - - - describe("Area update cases", () => { - test("Updating an area should superficially pass", () => addArea('test').then(area => areas.updateArea(testUser, area.metadata.area_id, { areaName: `New Name! ${process.uptime()}`}))) - test("Updating an area should produce a change entry in the changelog", () => addArea('test') - .then(area => areas.updateArea(testUser, area.metadata.area_id, { areaName: process.uptime().toString() })) - .then(area => { - expect(area?._change).toMatchObject({ - user: testUser, - operation: OperationType.updateArea, - } satisfies Partial) - })) - - test("Area name uniqueness in its current parent context", () => addArea('test').then(async parent => { - let [area, newArea, divorcedArea] = await Promise.all([ - addArea('original', { parent }), - addArea('wannabe', { parent }), - addArea(undefined, { parent: rootCountry }), - ]) - - await Promise.all([ - // Case where an area gets changed to what it already is, which should not throw an error - areas.updateArea(testUser, area.metadata.area_id, { areaName: area.area_name }), - // name-uniqueness should not be global, so this shouldn't throw - areas.updateArea(testUser, divorcedArea.metadata.area_id, { areaName: area.area_name }), - // if we update one of the areas to have a name for which another area already exists, we should expect this to throw. - expect(() => areas.updateArea(testUser, newArea.metadata.area_id, { areaName: area.area_name })).rejects.toThrow(GraphQLError), - ]) - })) - }) - - test("Area name uniqueness should not create a UUID shadow via deletion", () => addArea('test').then(async parent => { - let name = 'Big ol boulder' - let big = await addArea(name, { boulder: true, parent }) - await areas.deleteArea(testUser, big.metadata.area_id) - await addArea(name, { boulder: true, parent }) + test('Add an area with an unknown UUID parent should fail', + async ({ areas, user, country }) => await expect(async () => await areas.addArea(user, 'Texas', muid.v4())).rejects.toThrow()) + + test('Add a simple area with no specifications using a country code', async ({ areas, user, country }) => await areas.addArea(user, 'Texas part 2', null, country.shortCode) + .then(async texas => await areas.addArea(user, 'Texas Child', texas.metadata.area_id))) + + test('Add a simple area, then specify a new child one level deep', async ({ areas, addArea }) => await addArea('California') + .then(async parent => { + const child = await addArea('Child', { parent }) + expect(child).toMatchObject({ area_name: 'Child' }) + const parentCheck = await areas.findOneAreaByUUID(parent.metadata.area_id) + expect(parentCheck?.children ?? []).toContainEqual(child._id) + })) + + test('Add a leaf area', async ({ areas, addArea }) => await addArea('Somewhere').then(async parent => await addArea('Child', { leaf: true, parent })) + .then(async leaf => { + expect(leaf).toMatchObject({ metadata: { leaf: true } }) + const area = await areas.areaModel.findById(leaf._id) + expect(area).toMatchObject({ metadata: { leaf: true } }) + })) + + test('Add a leaf area that is a boulder', async ({ addArea }) => await addArea('Maine') + .then(async parent => await addArea('Child', { leaf: true, boulder: true, parent })) + .then(area => { + expect(area).toMatchObject({ + metadata: { + leaf: true, + isBoulder: true + } + } satisfies Partial & { metadata: Partial }>) + })) + + test('Add a NON-leaf area that is a boulder', async ({ addArea }) => await addArea('Wisconcin') + .then(async texas => await addArea('Child', { leaf: false, boulder: true })) + .then(area => { + expect(area).toMatchObject({ + metadata: { + // Even though we specified false to leaf on the input, we expect it to be true + // after write because a boulder cannot contain sub-areas + leaf: true, + isBoulder: true + } + } satisfies Partial & { metadata: Partial }>) + })) + + test('Adding a child to a leaf area should cause it to become a normal area', async ({ addArea }) => await addArea() + .then(async parent => await Promise.all(new Array(5).map(async () => await addArea('test', { leaf: true, parent })))) + .then(([leaf]) => leaf) + .then(async leaf => await addArea('test', { parent: leaf })) + .then(leaf => expect(leaf).toMatchObject({ metadata: { leaf: false } }))) + + test('area names should be unique in their parent context', async ({ areas, user, country, addArea }) => await addArea('test').then(async parent => { + await addArea('Big ol boulder', { parent }) + await expect(async () => await addArea('Big ol boulder', { parent })).rejects.toThrow(GraphQLError) + })) + }) + + test('Delete Area', async ({ areas, user, addArea }) => await addArea('test').then(async area => await areas.deleteArea(user, area.metadata.area_id)).then(async deleted => { + expect(deleted).toBeDefined() + // TODO: this test fails based on the data returned, which appears to omit the _deleting field. + const d = await areas.areaModel.findById(deleted?._id) + + expect(d).toBeDefined() + expect(d).not.toBeNull() + expect(d?._deleting).toBeDefined() + })) + + test('Delete Area that is already deleted should throw', async ({ areas, user, addArea }) => await addArea('test') + .then(async area => await areas.deleteArea(user, area.metadata.area_id)) + .then(async area => { + expect(area).not.toBeNull() + ok(area !== null) + await expect(async () => await areas.deleteArea(user, area.metadata.area_id)).rejects.toThrow() })) - test("Area name uniqueness should not create a UUID shadow via edit of name", () => addArea('test').then(async parent => { - let nameShadow = 'Big ol boulder 2' - let big = await addArea(nameShadow, { boulder: true, parent }) + describe('Area update cases', () => { + test('Updating an area should superficially pass', async ({ areas, user, addArea }) => await addArea('test').then(async area => await areas.updateArea(user, area.metadata.area_id, { areaName: `New Name! ${process.uptime()}` }))) + test('Updating an area should produce a change entry in the changelog', async ({ areas, user, country, addArea }) => await addArea('test') + .then(async area => await areas.updateArea(user, area.metadata.area_id, { areaName: process.uptime().toString() })) + .then(area => { + expect(area?._change).toMatchObject({ + user, + operation: OperationType.updateArea + } satisfies Partial) + })) - // We change the name of the original owner of the nameshadow, and then try to add a - // name claming the original name in this area structure context - await areas.updateArea(testUser, big.metadata.area_id, { areaName: "Still big ol bolder"}) - await addArea(nameShadow, { boulder: true, parent }) + test('Area name uniqueness in its current parent context', async ({ areas, user, addArea }) => await addArea('test').then(async parent => { + const [area, newArea, divorcedArea] = await Promise.all([ + addArea('original', { parent }), + addArea('wannabe', { parent }), + addArea() + ]) + + await Promise.all([ + // Case where an area gets changed to what it already is, which should not throw an error + areas.updateArea(user, area.metadata.area_id, { areaName: area.area_name }), + // name-uniqueness should not be global, so this shouldn't throw + areas.updateArea(user, divorcedArea.metadata.area_id, { areaName: area.area_name }), + // if we update one of the areas to have a name for which another area already exists, we should expect this to throw. + expect(async () => await areas.updateArea(user, newArea.metadata.area_id, { areaName: area.area_name })).rejects.toThrow(GraphQLError) + ]) })) -}) \ No newline at end of file + }) + + test('Area name uniqueness should not create a UUID shadow via deletion', async ({ areas, user, country, addArea }) => await addArea('test').then(async parent => { + const name = 'Big ol boulder' + const big = await addArea(name, { boulder: true, parent }) + await areas.deleteArea(user, big.metadata.area_id) + await addArea(name, { boulder: true, parent }) + })) + + test('Area name uniqueness should not create a UUID shadow via edit of name', async ({ areas, user, country, addArea }) => await addArea('test').then(async parent => { + const nameShadow = 'Big ol boulder 2' + const big = await addArea(nameShadow, { boulder: true, parent }) + + // We change the name of the original owner of the nameshadow, and then try to add a + // name claming the original name in this area structure context + await areas.updateArea(user, big.metadata.area_id, { areaName: 'Still big ol bolder' }) + await addArea(nameShadow, { boulder: true, parent }) + })) +}) diff --git a/src/model/__tests__/MutableClimbDataSource.ts b/src/model/__tests__/MutableClimbDataSource.test.ts similarity index 73% rename from src/model/__tests__/MutableClimbDataSource.ts rename to src/model/__tests__/MutableClimbDataSource.test.ts index f4d4dbd1..48a8c714 100644 --- a/src/model/__tests__/MutableClimbDataSource.ts +++ b/src/model/__tests__/MutableClimbDataSource.test.ts @@ -1,19 +1,10 @@ import muid from 'uuid-mongodb' -import { ChangeStream } from 'mongodb' - -import MutableClimbDataSource from '../MutableClimbDataSource.js' -import MutableAreaDataSource from '../MutableAreaDataSource.js' - -import { createIndexes, getAreaModel, getClimbModel } from '../../db/index.js' -import { logger } from '../../logger.js' import { ClimbChangeInputType, ClimbType } from '../../db/ClimbTypes.js' +import { dataFixtures as it } from '../../__tests__/fixtures/data.fixtures' import { sanitizeDisciplines, validDisciplines } from '../../GradeUtils.js' -import streamListener from '../../db/edit/streamListener.js' -import ChangeLogDataSource from '../ChangeLogDataSource.js' -import inMemoryDB from '../../utils/inMemoryDB.js' import assert from 'assert' -export const newSportClimb1: ClimbChangeInputType = { +const newSportClimb1: ClimbChangeInputType = { name: 'Cool route 1', disciplines: { sport: true @@ -25,11 +16,6 @@ export const newSportClimb1: ClimbChangeInputType = { } describe('Climb CRUD', () => { - let climbs: MutableClimbDataSource - let areas: MutableAreaDataSource - let stream: ChangeStream - const testUser = muid.v4() - const newClimbsToAdd: ClimbChangeInputType[] = [ { name: 'Sport 1', @@ -140,44 +126,16 @@ describe('Climb CRUD', () => { ] } - beforeAll(async () => { - await inMemoryDB.connect() - stream = await streamListener() - - try { - await getAreaModel().collection.drop() - await getClimbModel().collection.drop() - } catch (e) { - logger.info('Expected exception') - } - - await createIndexes() - - climbs = MutableClimbDataSource.getInstance() - areas = MutableAreaDataSource.getInstance() - await ChangeLogDataSource.getInstance()._testRemoveAll() - await areas.addCountry('fr') - }) - - afterAll(async () => { - try { - await stream.close() - await inMemoryDB.close() - } catch (e) { - console.log('closing mongoose', e) - } - }) - - it('can add new climbs', async () => { + it('can add new climbs', async ({ areas, climbs, user }) => { await areas.addCountry('usa') - const newDestination = await areas.addArea(testUser, 'California', null, 'usa') - if (newDestination == null) fail('Expect new area to be created') + const newDestination = await areas.addArea(user, 'California', null, 'usa') + expect(newDestination).toBeTruthy() - const routesArea = await areas.addArea(testUser, 'Sport & Trad', newDestination.metadata.area_id) + const routesArea = await areas.addArea(user, 'Sport & Trad', newDestination.metadata.area_id) const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, routesArea.metadata.area_id, newClimbsToAdd ) @@ -215,11 +173,11 @@ describe('Climb CRUD', () => { // California contains subareas. Should fail. await expect( - climbs.addOrUpdateClimbs(testUser, newDestination.metadata.area_id, [newBoulderProblem1]) + climbs.addOrUpdateClimbs(user, newDestination.metadata.area_id, [newBoulderProblem1]) ).rejects.toThrowError(/You can only add climbs to a crag/) // Route-only area should accept new boulder problems - const [newBoulderID] = await climbs.addOrUpdateClimbs(testUser, routesArea.metadata.area_id, [newBoulderProblem1]) + const [newBoulderID] = await climbs.addOrUpdateClimbs(user, routesArea.metadata.area_id, [newBoulderProblem1]) // Should come after existing climbs expect(await climbs.findOneClimbByMUUID(muid.from(newBoulderID))).toMatchObject({ metadata: { @@ -228,18 +186,18 @@ describe('Climb CRUD', () => { }) }) - it('can add new boulder problems', async () => { + it('can add new boulder problems', async ({ areas, climbs, user }) => { await areas.addCountry('esp') - const newDestination = await areas.addArea(testUser, 'Valencia', null, 'esp') - if (newDestination == null) fail('Expect new area to be created') + const newDestination = await areas.addArea(user, 'Valencia', null, 'esp') + expect(newDestination).toBeTruthy() - const boulderingArea = await areas.addArea(testUser, 'Bouldering only', newDestination.metadata.area_id) + const boulderingArea = await areas.addArea(user, 'Bouldering only', newDestination.metadata.area_id) expect(boulderingArea.metadata.isBoulder).toBeFalsy() const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, boulderingArea.metadata.area_id, [newBoulderProblem1, newBoulderProblem2]) @@ -247,16 +205,14 @@ describe('Climb CRUD', () => { const newClimb = await climbs.findOneClimbByMUUID(muid.from(newIDs[0])) - if (newClimb == null) fail('Expecting new boulder problem to be added, but didn\'t find one') + assert(newClimb != null) expect(newClimb.name).toBe(newBoulderProblem1.name) }) - it('can delete new boulder problems', async () => { - const newBoulderingArea = await areas.addArea(testUser, 'Bouldering area 1', null, 'fr') - if (newBoulderingArea == null) fail('Expect new area to be created') - + it('can delete new boulder problems', async ({ areas, climbs, user, addArea }) => { + const newBoulderingArea = await addArea('Bouldering area 1') const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newBoulderingArea.metadata.area_id, [newBoulderProblem1, newBoulderProblem2]) @@ -264,20 +220,20 @@ describe('Climb CRUD', () => { // delete a random (non-existing) climb const count0 = await climbs.deleteClimbs( - testUser, + user, newBoulderingArea.metadata.area_id, [muid.v4()]) expect(count0).toEqual(0) // try delete a correct climb and a non-existent one const count1 = await climbs.deleteClimbs( - testUser, + user, newBoulderingArea.metadata.area_id, [muid.from(newIDs[0]), muid.v4()]) // immediately delete a previously deleted climb. Should be a no op. const count2 = await climbs.deleteClimbs( - testUser, + user, newBoulderingArea.metadata.area_id, [muid.from(newIDs[0]), muid.v4()]) @@ -294,7 +250,7 @@ describe('Climb CRUD', () => { // expect one to remain rs = await climbs.findOneClimbByMUUID(muid.from(newIDs[1])) - if (rs == null) fail('Expect climb 2 to exist') + assert(rs != null) expect(rs._id.toUUID().toString()).toEqual(newIDs[1]) const areaRs = await areas.findOneAreaByUUID(newBoulderingArea.metadata.area_id) @@ -302,13 +258,13 @@ describe('Climb CRUD', () => { expect((areaRs.climbs[0] as ClimbType)._id.toUUID().toString()).toEqual(newIDs[1]) }) - it('handles mixed grades and disciplines correctly', async () => { + it('handles mixed grades and disciplines correctly', async ({ areas, climbs, user }) => { await areas.addCountry('can') - const newBoulderingArea = await areas.addArea(testUser, 'Bouldering area 1', null, 'can') - if (newBoulderingArea == null) fail('Expect new area to be created') + const newBoulderingArea = await areas.addArea(user, 'Bouldering area 1', null, 'can') + expect(newBoulderingArea).toBeTruthy() const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newBoulderingArea.metadata.area_id, [{ ...newBoulderProblem1, grade: 'V3' }, // good grade { ...newBoulderProblem2, grade: '5.9' }]) // invalid grade (YDS grade for a boulder problem) @@ -322,13 +278,13 @@ describe('Climb CRUD', () => { expect(climb2?.grades).toEqual(undefined) }) - it('handles Australian grade context correctly', async () => { + it('handles Australian grade context correctly', async ({ areas, climbs, user }) => { await areas.addCountry('aus') { // A roped climbing area - const newClimbingArea = await areas.addArea(testUser, 'Climbing area 1', null, 'aus') - if (newClimbingArea == null) fail('Expect new area to be created') + const newClimbingArea = await areas.addArea(user, 'Climbing area 1', null, 'aus') + expect(newClimbingArea).toBeTruthy() const newclimbs = [ { ...newSportClimb1, grade: '17' }, // good sport grade @@ -339,7 +295,7 @@ describe('Climb CRUD', () => { ] const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newClimbingArea.metadata.area_id, newclimbs ) @@ -374,11 +330,11 @@ describe('Climb CRUD', () => { { // A bouldering area - const newBoulderingArea = await areas.addArea(testUser, 'Bouldering area 1', null, 'aus') - if (newBoulderingArea == null) fail('Expect new area to be created') + const newBoulderingArea = await areas.addArea(user, 'Bouldering area 1', null, 'aus') + expect(newBoulderingArea).toBeTruthy() const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newBoulderingArea.metadata.area_id, [{ ...newBoulderProblem1, grade: 'V3' }, // good grade { ...newBoulderProblem2, grade: '23' }, // bad boulder grade @@ -397,13 +353,13 @@ describe('Climb CRUD', () => { } }) - it('handles Brazilian grade context correctly', async () => { + it('handles Brazilian grade context correctly', async ({ areas, climbs, user }) => { await areas.addCountry('bra') { // A roped climbing area - const newClimbingArea = await areas.addArea(testUser, 'Climbing area in Brazil', null, 'bra') - if (newClimbingArea == null) fail('Expect new area to be created in Brazil') + const newClimbingArea = await areas.addArea(user, 'Climbing area in Brazil', null, 'bra') + expect(newClimbingArea).toBeTruthy() const newclimbs = [ { ...newSportClimb1, grade: 'VIsup' }, // good sport grade @@ -414,7 +370,7 @@ describe('Climb CRUD', () => { ] const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newClimbingArea.metadata.area_id, newclimbs ) @@ -449,11 +405,11 @@ describe('Climb CRUD', () => { { // A bouldering area - const newBoulderingArea = await areas.addArea(testUser, 'Bouldering area 1', null, 'bra') - if (newBoulderingArea == null) fail('Expect new area to be created') + const newBoulderingArea = await areas.addArea(user, 'Bouldering area 1', null, 'bra') + expect(newBoulderingArea).toBeTruthy() const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newBoulderingArea.metadata.area_id, [{ ...newBoulderProblem1, grade: 'V3' }, // good grade { ...newBoulderProblem2, grade: '23' }, // bad boulder grade @@ -472,15 +428,15 @@ describe('Climb CRUD', () => { } }) - it('handles UIAA grades correctly', async () => { + it('handles UIAA grades correctly', async ({ areas, climbs, user }) => { await areas.addCountry('deu') // Assuming Germany since UIAA is dominant grading system // A roped climbing area - const newClimbingArea = await areas.addArea(testUser, 'Climbing area 1', null, 'deu') - if (newClimbingArea == null) fail('Expect new area to be created') + const newClimbingArea = await areas.addArea(user, 'Climbing area 1', null, 'deu') + expect(newClimbingArea).toBeTruthy() const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newClimbingArea.metadata.area_id, [{ ...newSportClimb1, grade: '6+' }, // good UIAA grade { ...newSportClimb2, grade: '7-' }, // good UIAA grade @@ -502,31 +458,28 @@ describe('Climb CRUD', () => { expect(climb4?.grades).toEqual(undefined) }) - it('can update boulder problems', async () => { - const newDestination = await areas.addArea(testUser, 'Bouldering area A100', null, 'fr') - - if (newDestination == null) fail('Expect new area to be created') - + it('can update boulder problems', async ({ climbs, user, area, randomGrade, gradeSystemFor }) => { const newIDs = await climbs.addOrUpdateClimbs( - testUser, - newDestination.metadata.area_id, + user, + area.metadata.area_id, [newBoulderProblem1, newBoulderProblem2]) const actual0 = await climbs.findOneClimbByMUUID(muid.from(newIDs[0])) + assert(actual0 != null) expect(actual0).toMatchObject({ name: newBoulderProblem1.name, type: sanitizeDisciplines(newBoulderProblem1.disciplines) }) - expect(actual0?.createdBy?.toUUID().toString()).toEqual(testUser.toString()) + expect(actual0?.createdBy?.toUUID().toString()).toEqual(user.toString()) expect(actual0?.updatedBy).toBeUndefined() const changes: ClimbChangeInputType[] = [ { id: newIDs[0], name: 'new name A100', - grade: '6b', + grade: randomGrade(actual0), disciplines: sanitizeDisciplines({ bouldering: true }) }, { @@ -536,17 +489,15 @@ describe('Climb CRUD', () => { ] const otherUser = muid.v4() - - const updated = await climbs.addOrUpdateClimbs(otherUser, newDestination.metadata.area_id, changes) + const updated = await climbs.addOrUpdateClimbs(otherUser, area.metadata.area_id, changes) expect(updated).toHaveLength(2) - const actual1 = await climbs.findOneClimbByMUUID(muid.from(newIDs[0])) - - expect(actual1).toMatchObject({ + const climbInDatabase = await climbs.findOneClimbByMUUID(muid.from(newIDs[0])) + expect(climbInDatabase).toMatchObject({ name: changes[0].name, grades: { - font: changes[0].grade + [gradeSystemFor(actual0)]: changes[0].grade }, // Make sure update doesn't touch other fields type: sanitizeDisciplines(changes[0].disciplines), @@ -557,17 +508,15 @@ describe('Climb CRUD', () => { } }) - expect(actual1?.createdBy?.toUUID().toString()).toEqual(testUser.toUUID().toString()) - expect(actual1?.updatedBy?.toUUID().toString()).toEqual(otherUser.toUUID().toString()) + expect(climbInDatabase?.createdBy?.toUUID().toString()).toEqual(user.toUUID().toString()) + expect(climbInDatabase?.updatedBy?.toUUID().toString()).toEqual(otherUser.toUUID().toString()) }) - it('can update climb length, boltsCount & fa', async () => { - const newDestination = await areas.addArea(testUser, 'Sport area Z100', null, 'fr') - - if (newDestination == null) fail('Expect new area to be created') + it('can update climb length, boltsCount & fa', async ({ areas, climbs, user, addArea }) => { + const newDestination = await addArea('Sport area Z100') const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newDestination.metadata.area_id, newClimbsToAdd ) @@ -579,9 +528,10 @@ describe('Climb CRUD', () => { boltsCount: 5 } - await climbs.addOrUpdateClimbs(testUser, + await climbs.addOrUpdateClimbs(user, newDestination.metadata.area_id, - [change]) + [change] + ) const actual = await climbs.findOneClimbByMUUID(muid.from(newIDs[0])) @@ -596,17 +546,17 @@ describe('Climb CRUD', () => { }) }) - it('can add multi-pitch climbs', async () => { + it('can add multi-pitch climbs', async ({ areas, climbs, user }) => { await areas.addCountry('aut') - const newDestination = await areas.addArea(testUser, 'Some Location with Multi-Pitch Climbs', null, 'aut') - if (newDestination == null) fail('Expect new area to be created') + const newDestination = await areas.addArea(user, 'Some Location with Multi-Pitch Climbs', null, 'aut') + expect(newDestination).toBeTruthy() - const routesArea = await areas.addArea(testUser, 'Sport & Trad Multi-Pitches', newDestination.metadata.area_id) + const routesArea = await areas.addArea(user, 'Sport & Trad Multi-Pitches', newDestination.metadata.area_id) // create new climb with individual pitches const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, routesArea.metadata.area_id, [newClimbWithPitches] ) @@ -626,25 +576,23 @@ describe('Climb CRUD', () => { }, pitches: newClimbWithPitches.pitches }) - // Validate each pitch - if (climb?.pitches != null) { - climb.pitches.forEach((pitch) => { - expect(pitch).toHaveProperty('_id') - expect(pitch).toHaveProperty('parentId') - expect(pitch).toHaveProperty('pitchNumber') - }) - } else { - fail('Pitches are missing either of required attributes id, parentId, pitchNumber') - } + + assert(climb?.pitches != null) + + climb.pitches.forEach((pitch) => { + expect(pitch).toHaveProperty('_id') + expect(pitch).toHaveProperty('parentId') + expect(pitch).toHaveProperty('pitchNumber') + }) }) - it('can update multi-pitch problems', async () => { - const newDestination = await areas.addArea(testUser, 'Some Multi-Pitch Area to be Updated', null, 'deu') + it('can update multi-pitch problems', async ({ areas, climbs, user }) => { + const newDestination = await areas.addArea(user, 'Some Multi-Pitch Area to be Updated', null, 'deu') - if (newDestination == null) fail('Expect new area to be created') + expect(newDestination).toBeTruthy() const newIDs = await climbs.addOrUpdateClimbs( - testUser, + user, newDestination.metadata.area_id, [newClimbWithPitches] ) @@ -652,11 +600,9 @@ describe('Climb CRUD', () => { // Fetch the original climb const original = await climbs.findOneClimbByMUUID(muid.from(newIDs[0])) - // Check if 'original' is not null before accessing its properties - if ((original == null) || (original.pitches == null) || original.pitches.length < 2) { - fail('Original climb is null or does not have at least two pitches (as defined in the test case)') - return - } + assert(original !== null) + assert(original.pitches !== undefined) + expect(original.pitches.length).not.toBeLessThan(2) // Store original pitch IDs and parent IDs const originalPitch1ID = original.pitches[0]._id.toUUID().toString() @@ -695,7 +641,7 @@ describe('Climb CRUD', () => { ] // update climb - await climbs.addOrUpdateClimbs(testUser, newDestination.metadata.area_id, changes) + await climbs.addOrUpdateClimbs(user, newDestination.metadata.area_id, changes) // Fetch the updated climb const updatedClimb = await climbs.findOneClimbByMUUID(muid.from(newIDs[0])) @@ -724,12 +670,11 @@ describe('Climb CRUD', () => { } // Check that the createdBy and updatedBy fields are not undefined before accessing their properties - if ((updatedClimb.createdBy != null) && (updatedClimb.updatedBy != null)) { - expect(updatedClimb.createdBy.toUUID().toString()).toEqual(testUser.toString()) - expect(updatedClimb.updatedBy.toUUID().toString()).toEqual(testUser.toString()) - } else { - fail('createdBy or updatedBy is undefined') - } + assert(updatedClimb.createdBy !== undefined) + assert(updatedClimb.updatedBy !== undefined) + + expect(updatedClimb.createdBy.toUUID().toString()).toEqual(user.toString()) + expect(updatedClimb.updatedBy.toUUID().toString()).toEqual(user.toString()) } }) }) diff --git a/src/model/__tests__/MutableOrganizationDataSource.test.ts b/src/model/__tests__/MutableOrganizationDataSource.test.ts new file mode 100644 index 00000000..0c371886 --- /dev/null +++ b/src/model/__tests__/MutableOrganizationDataSource.test.ts @@ -0,0 +1,143 @@ +import muuid from 'uuid-mongodb' + +import { OrganizationEditableFieldsType, OrganizationType, OrgType } from '../../db/OrganizationTypes.js' +import { AreaType } from '../../db/AreaTypes.js' +import { muuidToString } from '../../utils/helpers.js' +import { dataFixtures } from '../../__tests__/fixtures/data.fixtures.js' + +interface LocalContext { + excludedArea: AreaType + orgData: OrganizationEditableFieldsType + organization: OrganizationType + emptyOrg: OrganizationEditableFieldsType +} + +const it = dataFixtures.extend({ + excludedArea: async ({ addArea }, use) => { await use(await addArea()) }, + + orgData: async ({ country, excludedArea, area, task }, use) => { + await use({ + associatedAreaIds: [country.metadata.area_id], + excludedAreaIds: [excludedArea.metadata.area_id, area.metadata.area_id], + displayName: task.name, + website: `https://www.${task.id}.com`, + email: `admin@${task.id}.com`, + donationLink: `https://www.${task.id}.com/donate`, + instagramLink: `https://www.instagram.com/${task.id}`, + facebookLink: `https://www.facebook.com/${task.id}`, + hardwareReportLink: `https://www.${task.id}.com/reporthardware`, + description: `We are ${task.id}.\nWe are a 503(B) corporation.` + }) + }, + + organization: async ({ organizations, orgData, user }, use) => { + const org = await organizations.addOrganization(user, OrgType.localClimbingOrganization, orgData) + await use(org) + await organizations.deleteFromCacheById(org._id) + }, + + emptyOrg: async ({ task }, use) => { + await use({ + displayName: `Foes of ${task.id}` + }) + } +}) + +describe('Organization', () => { + it('should successfully create a document when passed valid input', async ({ organizations, orgData, user, country }) => { + const newOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, orgData) + const document = { ...orgData } + expect(newOrg.displayName).toBe(document.displayName) + expect(newOrg.content?.website).toBe(document.website) + expect(newOrg.content?.email).toBe(document.email) + expect(newOrg.content?.donationLink).toBe(document.donationLink) + expect(newOrg.content?.instagramLink).toBe(document.instagramLink) + expect(newOrg.content?.facebookLink).toBe(document.facebookLink) + expect(newOrg.content?.hardwareReportLink).toBe(document.hardwareReportLink) + expect(newOrg.content?.description).toBe(document.description) + expect(newOrg.associatedAreaIds.map(muuidToString)).toEqual([muuidToString(country.metadata.area_id)]) + expect(newOrg._change?.operation).toBe('addOrganization') + expect(newOrg._change?.seq).toBe(0) + + const orgIdSearchRes = await organizations.findOneOrganizationByOrgId(newOrg.orgId) + expect(orgIdSearchRes._id).toEqual(newOrg._id) + }) + + describe('should retrieve documents based on displayName', () => { + it('Should be case insensitive', async ({ organization, organizations }) => { + // Match should be case-insensitive. + const displayNameSearchCursor = await organizations.findOrganizationsByFilter({ + displayName: { + match: organization.displayName.toLocaleUpperCase(), + exactMatch: false + } + }) + const displayNameSearchRes = await displayNameSearchCursor.toArray() + expect(displayNameSearchRes).toHaveLength(1) + expect(displayNameSearchRes[0]._id).toEqual(organization._id) + }) + + it('Should match against a partial string', async ({ organization, organizations }) => { + // Match should be case-insensitive. + const displayNameSearchCursor = await organizations.findOrganizationsByFilter({ + displayName: { + match: organization.displayName.toLocaleUpperCase().slice(10, 20), + exactMatch: false + } + }) + const displayNameSearchRes = await displayNameSearchCursor.toArray() + expect(displayNameSearchRes).toHaveLength(1) + expect(displayNameSearchRes[0]._id).toEqual(organization._id) + }) + }) + + it('should retrieve documents based on associatedAreaIds', async ({ organizations, orgData, user, excludedArea, area }) => { + const newOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, orgData) + const document = { + associatedAreaIds: [excludedArea.metadata.area_id, area.metadata.area_id] + } + await organizations.updateOrganization(user, newOrg.orgId, document) + const areaIdSearchCursor = await organizations.findOrganizationsByFilter({ associatedAreaIds: { includes: [excludedArea.metadata.area_id] } }) + const areaIdSearchRes = await areaIdSearchCursor.toArray() + expect(areaIdSearchRes).toHaveLength(1) + expect(areaIdSearchRes[0]._id).toEqual(newOrg._id) + }) + + describe('update', () => { + it('should succeed on valid input', async ({ organizations, emptyOrg, user, orgData }) => { + const newOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, emptyOrg) + const document = { ...orgData } + const updatedOrg = await organizations.updateOrganization(user, newOrg.orgId, document) + + expect(updatedOrg).toBeDefined() + assert(updatedOrg != null) + + expect(updatedOrg.associatedAreaIds.map(muuidToString).sort()) + .toStrictEqual(document?.associatedAreaIds?.map(muuidToString).sort()) + expect(updatedOrg.excludedAreaIds.map(muuidToString).sort()) + .toStrictEqual(document?.excludedAreaIds?.map(muuidToString).sort()) + expect(updatedOrg.displayName).toBe(document.displayName) + expect(updatedOrg.content?.website).toBe(document.website) + expect(updatedOrg.content?.email).toBe(document.email) + expect(updatedOrg.content?.donationLink).toBe(document.donationLink) + expect(updatedOrg.content?.instagramLink).toBe(document.instagramLink) + expect(updatedOrg.content?.facebookLink).toBe(document.facebookLink) + expect(updatedOrg.content?.hardwareReportLink).toBe(document.hardwareReportLink) + expect(updatedOrg.content?.description).toBe(document.description) + expect(updatedOrg._change?.operation).toBe('updateOrganization') + expect(updatedOrg._change?.seq).toBe(0) + expect(updatedOrg.updatedAt?.getTime()).toBeGreaterThan(updatedOrg.createdAt.getTime()) + }) + + it('should throw when an invalid area is supplied', async ({ orgData, emptyOrg, user, organizations }) => { + const newOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, emptyOrg) + const document = { + ...orgData, + associatedAreaIds: [muuid.v4()] + } + await expect(organizations.updateOrganization(user, newOrg.orgId, document)) + .rejects + .toThrow(/Organization update error. Reason: Associated areas not found: /) + }) + }) +}) diff --git a/src/model/__tests__/MutableOrganizationDataSource.ts b/src/model/__tests__/MutableOrganizationDataSource.ts deleted file mode 100644 index 651198b5..00000000 --- a/src/model/__tests__/MutableOrganizationDataSource.ts +++ /dev/null @@ -1,146 +0,0 @@ -import muuid from 'uuid-mongodb' - -import MutableOrganizationDataSource from '../MutableOrganizationDataSource.js' -import MutableAreaDataSource from '../MutableAreaDataSource.js' -import { createIndexes, getAreaModel, getOrganizationModel } from '../../db/index.js' -import { OrganizationEditableFieldsType, OrgType } from '../../db/OrganizationTypes.js' -import { AreaType } from '../../db/AreaTypes.js' -import { muuidToString } from '../../utils/helpers.js' -import inMemoryDB from '../../utils/inMemoryDB.js' - -describe('Organization', () => { - let organizations: MutableOrganizationDataSource - let areas: MutableAreaDataSource - let usa: AreaType - let ca: AreaType - let wa: AreaType - let fullOrg: OrganizationEditableFieldsType - let emptyOrg: OrganizationEditableFieldsType - const testUser = muuid.v4() - - beforeAll(async () => { - await inMemoryDB.connect() - try { // Use the same fixed areas for testing so no need to drop and re-create on each test. - await getAreaModel().collection.drop() - } catch (e) { - console.log('Cleaning up area model before test', e) - } - organizations = MutableOrganizationDataSource.getInstance() - areas = MutableAreaDataSource.getInstance() - usa = await areas.addCountry('usa') - ca = await areas.addArea(testUser, 'CA', usa.metadata.area_id) - wa = await areas.addArea(testUser, 'WA', usa.metadata.area_id) - fullOrg = { - associatedAreaIds: [usa.metadata.area_id], - excludedAreaIds: [ca.metadata.area_id, wa.metadata.area_id], - displayName: 'Friends of Openbeta', - website: 'https://www.friendsofopenbeta.com', - email: 'admin@friendsofopenbeta.com', - donationLink: 'https://www.friendsofopenbeta.com/donate', - instagramLink: 'https://www.instagram.com/friendsofopenbeta', - facebookLink: 'https://www.facebook.com/friendsofopenbeta', - hardwareReportLink: 'https://www.friendsofopenbeta.com/reporthardware', - description: 'We are friends of openbeta.\nWe are a 503(B) corporation.' - } - emptyOrg = { - displayName: 'Foes of Openbeta' - } - }) - - beforeEach(async () => { - try { - await getOrganizationModel().collection.drop() - } catch (e) { - console.log('Cleaning up organization model before test', e) - } - await createIndexes() - }) - - afterAll(async () => { - await inMemoryDB.close() - }) - - it('should successfully create a document when passed valid input', async () => { - const newOrg = await organizations.addOrganization(testUser, OrgType.localClimbingOrganization, fullOrg) - const document = { ...fullOrg } - expect(newOrg.displayName).toBe(document.displayName) - expect(newOrg.content?.website).toBe(document.website) - expect(newOrg.content?.email).toBe(document.email) - expect(newOrg.content?.donationLink).toBe(document.donationLink) - expect(newOrg.content?.instagramLink).toBe(document.instagramLink) - expect(newOrg.content?.facebookLink).toBe(document.facebookLink) - expect(newOrg.content?.hardwareReportLink).toBe(document.hardwareReportLink) - expect(newOrg.content?.description).toBe(document.description) - expect(newOrg.associatedAreaIds.map(muuidToString)).toEqual([muuidToString(usa.metadata.area_id)]) - expect(newOrg._change?.operation).toBe('addOrganization') - expect(newOrg._change?.seq).toBe(0) - - const orgIdSearchRes = await organizations.findOneOrganizationByOrgId(newOrg.orgId) - expect(orgIdSearchRes._id).toEqual(newOrg._id) - }) - - it('should retrieve documents based on displayName', async () => { - const newOrg = await organizations.addOrganization(testUser, OrgType.localClimbingOrganization, fullOrg) - // Match should be case-insensitive. - const displayNameSearchCursor = await organizations.findOrganizationsByFilter({ - displayName: { - match: 'openbeta', - exactMatch: false - } - }) - const displayNameSearchRes = await displayNameSearchCursor.toArray() - expect(displayNameSearchRes).toHaveLength(1) - expect(displayNameSearchRes[0]._id).toEqual(newOrg._id) - }) - - it('should retrieve documents based on associatedAreaIds', async () => { - const newOrg = await organizations.addOrganization(testUser, OrgType.localClimbingOrganization, fullOrg) - const document = { - associatedAreaIds: [ca.metadata.area_id, wa.metadata.area_id] - } - await organizations.updateOrganization(testUser, newOrg.orgId, document) - const areaIdSearchCursor = await organizations.findOrganizationsByFilter({ associatedAreaIds: { includes: [ca.metadata.area_id] } }) - const areaIdSearchRes = await areaIdSearchCursor.toArray() - expect(areaIdSearchRes).toHaveLength(1) - expect(areaIdSearchRes[0]._id).toEqual(newOrg._id) - }) - - describe('update', () => { - it('should succeed on valid input', async () => { - const newOrg = await organizations.addOrganization(testUser, OrgType.localClimbingOrganization, emptyOrg) - const document = { ...fullOrg } - const updatedOrg = await organizations.updateOrganization(testUser, newOrg.orgId, document) - - expect(updatedOrg).toBeDefined() - if (updatedOrg == null) { - fail('should not reach here.') - } - expect(updatedOrg.associatedAreaIds.map(muuidToString).sort()) - .toStrictEqual(document?.associatedAreaIds?.map(muuidToString).sort()) - expect(updatedOrg.excludedAreaIds.map(muuidToString).sort()) - .toStrictEqual(document?.excludedAreaIds?.map(muuidToString).sort()) - expect(updatedOrg.displayName).toBe(document.displayName) - expect(updatedOrg.content?.website).toBe(document.website) - expect(updatedOrg.content?.email).toBe(document.email) - expect(updatedOrg.content?.donationLink).toBe(document.donationLink) - expect(updatedOrg.content?.instagramLink).toBe(document.instagramLink) - expect(updatedOrg.content?.facebookLink).toBe(document.facebookLink) - expect(updatedOrg.content?.hardwareReportLink).toBe(document.hardwareReportLink) - expect(updatedOrg.content?.description).toBe(document.description) - expect(updatedOrg._change?.operation).toBe('updateOrganization') - expect(updatedOrg._change?.seq).toBe(0) - expect(updatedOrg.updatedAt?.getTime()).toBeGreaterThan(updatedOrg.createdAt.getTime()) - }) - - it('should throw when an invalid area is supplied', async () => { - const newOrg = await organizations.addOrganization(testUser, OrgType.localClimbingOrganization, emptyOrg) - const document = { - ...fullOrg, - associatedAreaIds: [muuid.v4()] - } - await expect(organizations.updateOrganization(testUser, newOrg.orgId, document)) - .rejects - .toThrow(/Organization update error. Reason: Associated areas not found: /) - }) - }) -}) diff --git a/src/model/__tests__/UserDataSource.ts b/src/model/__tests__/UserDataSource.test.ts similarity index 82% rename from src/model/__tests__/UserDataSource.ts rename to src/model/__tests__/UserDataSource.test.ts index 9d6cc57c..fcd25008 100644 --- a/src/model/__tests__/UserDataSource.ts +++ b/src/model/__tests__/UserDataSource.test.ts @@ -1,36 +1,14 @@ -import mongoose from 'mongoose' import muuid from 'uuid-mongodb' -import { jest } from '@jest/globals' - -import { getUserModel } from '../../db/index.js' import UserDataSource from '../UserDataSource.js' import { UpdateProfileGQLInput } from '../../db/UserTypes.js' -import inMemoryDB from '../../utils/inMemoryDB.js' +import { dataFixtures as it } from '../../__tests__/fixtures/data.fixtures.js' describe('UserDataSource', () => { - let users: UserDataSource - - beforeAll(async () => { - await inMemoryDB.connect() - const userModel = getUserModel() - try { - await userModel.collection.drop() - } catch (e) { - console.log('Cleaning up db before test') - } - await userModel.ensureIndexes() - users = new UserDataSource({ modelOrCollection: mongoose.connection.db.collection('users') }) - }) - - afterAll(async () => { - await inMemoryDB.close() - }) - afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) - it('should create a new user with just username', async () => { + it('should create a new user with just username', async ({ users }) => { const userUuid = muuid.v4() const updater = muuid.v4() const input: UpdateProfileGQLInput = { @@ -53,7 +31,7 @@ describe('UserDataSource', () => { expect(u?.updatedAt.getTime()).toBeLessThan(Date.now()) }) - it('should create a new user from username and other updatable fields', async () => { + it('should create a new user from username and other updatable fields', async ({ users }) => { const updater = muuid.v4() const userUuid = muuid.v4() const username = 'new-test-profile' @@ -100,7 +78,7 @@ describe('UserDataSource', () => { }) }) - it('should require an email when creating new profile', async () => { + it('should require an email when creating new profile', async ({ users }) => { const updater = muuid.v4() const userUuid = muuid.v4() const input: UpdateProfileGQLInput = { @@ -113,7 +91,7 @@ describe('UserDataSource', () => { ).rejects.toThrowError(/Email is required/i) }) - it('should enforce a waiting period for username update', async () => { + it('should enforce a waiting period for username update', async ({ users }) => { const updater = muuid.v4() const userUuid = muuid.v4() const input: UpdateProfileGQLInput = { @@ -134,7 +112,7 @@ describe('UserDataSource', () => { // Mock implementation with a function // that correctly tracks call count let callCount = 0 - jest.spyOn(UserDataSource, 'calculateLastUpdatedInDays') + vitest.spyOn(UserDataSource, 'calculateLastUpdatedInDays') .mockImplementation(() => { // For both calls in the third update operation: // First call (account age) - return 15 days (older than 14 days) @@ -158,7 +136,7 @@ describe('UserDataSource', () => { expect(errorThrown).toBe(true) }) - it('should reject invalid website url', async () => { + it('should reject invalid website url', async ({ users }) => { const updater = muuid.v4() const userUuid = muuid.v4() const input: UpdateProfileGQLInput = { diff --git a/src/model/__tests__/tickValidation.test.ts b/src/model/__tests__/tickValidation.test.ts new file mode 100644 index 00000000..9e3c2904 --- /dev/null +++ b/src/model/__tests__/tickValidation.test.ts @@ -0,0 +1,148 @@ +import { allowableStyleMap, choose, dataFixtures } from '../../__tests__/fixtures/data.fixtures.js' +import { muuidToString } from '../../utils/helpers.js' +import { ClimbChangeInputType } from '../../db/ClimbTypes.js' +import { + TickInput, + TickStyleValues +} from '../../db/TickTypes.js' + +interface LocalContext { + tick: ( + props?: Partial & { + climb?: Partial + }, + ) => Promise +} + +const it = dataFixtures.extend({ + tick: async ({ userUuid, addClimb }, use) => + await use(async (props) => { + const { climb, ...tick } = props ?? {} + + const reifiedClimb = await addClimb(climb) + const style = tick.style ?? choose(TickStyleValues) + const attemptType = tick.attemptType ?? choose(allowableStyleMap[style]) + return { + name: reifiedClimb.name, + notes: 'Sandbagged', + climbId: muuidToString(reifiedClimb._id), + userId: userUuid, + style, + attemptType, + dateClimbed: new Date('2012-12-12'), + grade: '5.7', + source: 'MP', + ...tick + } + }) +}) + +describe('Tick Validation', () => { + it('should validate tick for sport climb', async ({ ticks, tick }) => { + const tickData = await tick({ style: 'Lead' }) + await expect(ticks.addTick(tickData)).resolves.not.toThrow() + }) + + it('should validate tick for deep water solo climb', async ({ + ticks, + tick + }) => { + const tickData = await tick({ + climb: { disciplines: { deepwatersolo: true } }, + attemptType: 'Send', + style: undefined + }) + + await expect(ticks.addTick(tickData)).resolves.not.toThrow() + }) + + it('should throw error for invalid style for deep water solo climb', async ({ + ticks, + tick + }) => { + const invalidDwsTick: TickInput = await tick({ + style: 'Lead', + attemptType: 'Send', + climb: { disciplines: { deepwatersolo: true } } + }) + + await expect(ticks.addTick(invalidDwsTick)).rejects.toThrow( + 'Invalid style Lead for climb type' + ) + }) + + it('should validate tick for top rope climb', async ({ ticks, tick }) => { + const tickData = await tick({ + style: 'TR', + climb: { disciplines: { tr: true } } + }) + await expect(ticks.addTick(tickData)).resolves.not.toThrow() + }) + + it('should throw error for invalid attempt type for top rope climb', async ({ + ticks, + tick + }) => { + const invalidTrTick = await tick({ + attemptType: 'Pinkpoint', + style: 'TR', + climb: { + disciplines: { tr: true } + } + }) + + await expect(ticks.addTick(invalidTrTick)).rejects.toThrow( + 'Invalid attempt type Pinkpoint for TR/Follow/Aid style' + ) + }) + + it('should validate tick for aid climb', async ({ ticks, tick }) => { + const tickData = await tick({ + style: 'Aid', + climb: { disciplines: { aid: true } } + }) + await expect(ticks.addTick(tickData)).resolves.not.toThrow() + }) + + it('should throw error for invalid attempt type for aid climb', async ({ + ticks, + tick + }) => { + const invalidAidTick = await tick({ + style: 'Aid', + attemptType: 'Flash', + climb: { disciplines: { aid: true } } + }) + + await expect(ticks.addTick(invalidAidTick)).rejects.toThrow( + 'Invalid attempt type Flash for TR/Follow/Aid style' + ) + }) + + it('should throw error for invalid style for aid climb', async ({ + ticks, + tick + }) => { + const invalidAidTick: TickInput = await tick({ + style: 'Lead', + attemptType: 'Send', + grade: 'A2', + climb: { disciplines: { aid: true } } + }) + + await expect(ticks.addTick(invalidAidTick)).rejects.toThrow( + 'Invalid style Lead for climb type' + ) + }) + + it('should validate tick with no attempt type', async ({ ticks, tick }) => { + const noAttemptTypeTick: TickInput = { + ...(await tick({ + style: 'Lead', + climb: { disciplines: { sport: true } } + })), + attemptType: undefined + } + await expect(ticks.addTick(noAttemptTypeTick)).resolves.not.toThrow() + }) +}) diff --git a/src/model/__tests__/tickValidation.ts b/src/model/__tests__/tickValidation.ts deleted file mode 100644 index 98599479..00000000 --- a/src/model/__tests__/tickValidation.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { produce } from 'immer' -import TickDataSource from '../TickDataSource.js' -import { getTickModel, getUserModel } from '../../db/index.js' -import { TickInput } from '../../db/TickTypes.js' -import muuid from 'uuid-mongodb' -import inMemoryDB from '../../utils/inMemoryDB.js' -import { ClimbChangeInputType } from '../../db/ClimbTypes.js' -import MutableClimbDataSource from '../MutableClimbDataSource.js' -import MutableAreaDataSource from '../MutableAreaDataSource.js' - -const userId = muuid.v4() -const newClimbsToAdd: ClimbChangeInputType[] = [ - { - name: 'Sport 1', - disciplines: { - sport: true - }, - description: 'The best climb', - location: '5m left of the big tree', - protection: '5 quickdraws' - }, - { - name: 'Deep water 1', - disciplines: { - deepwatersolo: true - } - }, - { - name: 'Boulder 1', - disciplines: { - bouldering: true - } - }, - { - name: 'Top Rope 1', - disciplines: { - tr: true - } - }, - { - name: 'Aid 1', - disciplines: { - aid: true - } - } -] - -const toTestSport: TickInput = { - name: 'Small Dog', - notes: 'Sandbagged', - climbId: 'tbd', // need to create a climb for tick validation - userId: userId.toUUID().toString(), - style: 'Lead', - attemptType: 'Onsight', - dateClimbed: new Date('2012-12-12'), - grade: '5.7', - source: 'MP' -} - -const toTestDWS: TickInput = { - name: 'Sloppy Peaches', - notes: 'v sloppy', - climbId: 'tbd', - userId: userId.toUUID().toString(), - attemptType: 'Flash', - dateClimbed: new Date('2012-10-15'), - grade: '5.10', - source: 'MP' -} - -const toTestBoulder: TickInput = { - name: 'Boulder or DWS', - notes: 'wet!', - climbId: 'tbd', - userId: userId.toUUID().toString(), - style: 'Boulder', - attemptType: 'Flash', - dateClimbed: new Date('2012-10-15'), - grade: 'v4', - source: 'OB' -} - -const toTestTR: TickInput = { - name: 'Top Rope Climb', - notes: 'Nice climb', - climbId: 'tbd', - userId: userId.toUUID().toString(), - style: 'TR', - attemptType: 'Send', - dateClimbed: new Date('2012-10-15'), - grade: '5.10', - source: 'OB' -} - -const toTestAid: TickInput = { - name: 'Aid Climb', - notes: 'Challenging', - climbId: 'tbd', - userId: userId.toUUID().toString(), - style: 'Aid', - attemptType: 'Send', - dateClimbed: new Date('2012-10-15'), - grade: 'A2', - source: 'OB' -} - -let tickUpdate: TickInput = produce(toTestSport, draft => { - draft.notes = 'Not sandbagged' - draft.attemptType = 'Flash' - draft.source = 'OB' -}) - -describe('Tick Validation', () => { - let ticks: TickDataSource - let climbs: MutableClimbDataSource - let areas: MutableAreaDataSource - const tickModel = getTickModel() - - beforeAll(async () => { - console.log('#BeforeAll Tick Validation') - await inMemoryDB.connect() - - try { - await getTickModel().collection.drop() - await getUserModel().collection.drop() - } catch (e) { - console.log('Cleaning db') - } - - ticks = TickDataSource.getInstance() - climbs = MutableClimbDataSource.getInstance() - areas = MutableAreaDataSource.getInstance() - // Add climbs because add/update tick requires type validation - await areas.addCountry('usa') - const newDestination = await areas.addArea(userId, 'California', null, 'usa') - if (newDestination == null) fail('Expect new area to be created') - - const routesArea = await areas.addArea(userId, 'Sport & Trad', newDestination.metadata.area_id) - - const newIDs = await climbs.addOrUpdateClimbs(userId, routesArea.metadata.area_id, newClimbsToAdd) - - // Update tick inputs with generated climb IDs - toTestSport.climbId = newIDs[0] - toTestDWS.climbId = newIDs[1] - toTestBoulder.climbId = newIDs[2] - toTestTR.climbId = newIDs[3] - toTestAid.climbId = newIDs[4] - tickUpdate = { ...tickUpdate, climbId: newIDs[0] } // Ensure tickUpdate has the correct climbId - }) - - afterAll(async () => { - await inMemoryDB.close() - }) - - afterEach(async () => { - await getTickModel().collection.drop() - await tickModel.ensureIndexes() - }) - - it('should validate tick for sport climb', async () => { - await expect(ticks.addTick(toTestSport)).resolves.not.toThrow() - }) - - it('should validate tick for deep water solo climb', async () => { - const dwsTick: TickInput = { - ...toTestDWS, - attemptType: 'Send' - } - await expect(ticks.addTick(dwsTick)).resolves.not.toThrow() - }) - - it('should throw error for invalid style for deep water solo climb', async () => { - const invalidDwsTick: TickInput = { - ...toTestDWS, - style: 'Lead', - attemptType: 'Send' - } - await expect(ticks.addTick(invalidDwsTick)).rejects.toThrow('Invalid style Lead for climb type') - }) - - it('should validate tick for top rope climb', async () => { - await expect(ticks.addTick(toTestTR)).resolves.not.toThrow() - }) - - it('should throw error for invalid attempt type for top rope climb', async () => { - const invalidTrTick: TickInput = { - ...toTestTR, - attemptType: 'Pinkpoint' - } - await expect(ticks.addTick(invalidTrTick)).rejects.toThrow('Invalid attempt type Pinkpoint for TR/Follow/Aid style') - }) - - it('should validate tick for aid climb', async () => { - await expect(ticks.addTick(toTestAid)).resolves.not.toThrow() - }) - - it('should throw error for invalid attempt type for aid climb', async () => { - const invalidAidTick: TickInput = { - ...toTestAid, - attemptType: 'Flash' - } - await expect(ticks.addTick(invalidAidTick)).rejects.toThrow('Invalid attempt type Flash for TR/Follow/Aid style') - }) - - it('should throw error for invalid style for aid climb', async () => { - const invalidAidTick: TickInput = { - ...toTestAid, - style: 'Lead', - attemptType: 'Send' - } - await expect(ticks.addTick(invalidAidTick)).rejects.toThrow('Invalid style Lead for climb type') - }) - - it('should validate tick with no attempt type', async () => { - const noAttemptTypeTick: TickInput = { - ...toTestSport, - attemptType: undefined - } - await expect(ticks.addTick(noAttemptTypeTick)).resolves.not.toThrow() - }) -}) diff --git a/src/model/__tests__/ticks.test.ts b/src/model/__tests__/ticks.test.ts new file mode 100644 index 00000000..04ff236c --- /dev/null +++ b/src/model/__tests__/ticks.test.ts @@ -0,0 +1,136 @@ +import { produce } from 'immer' +import { TickInput, TickType } from '../../db/TickTypes.js' +import { allowableStyleMap, dataFixtures } from '../../__tests__/fixtures/data.fixtures.js' +import { muuidToString } from '../../utils/helpers.js' +import muuid from 'uuid-mongodb' + +interface LocalContext { + tickImportData: TickInput[] + tickData: TickInput + tickUpdateData: TickInput + tick: TickType +} + +function choose (arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)] +} + +const it = dataFixtures.extend({ + tickImportData: async ({ task, userUuid, climb }, use) => await use( + Array.from({ length: 20 }).map((_, idx) => ( + { + name: `${task.id}-${idx}`, + notes: 'Sandbagged', + climbId: muuidToString(climb._id), + userId: userUuid, + style: 'Lead', + attemptType: choose(allowableStyleMap.Lead), + dateClimbed: new Date(), + grade: '5.7', + source: 'MP' + } + ))), + tickData: async ({ userUuid, climb }, use) => await use({ + name: 'Small Dog', + notes: 'Sandbagged', + climbId: muuidToString(climb._id), + userId: userUuid, + style: 'Lead', + attemptType: choose(allowableStyleMap.Lead), + dateClimbed: new Date('2012-12-12'), + grade: '5.7', + source: 'MP' + }), + + tickUpdateData: async ({ tickData }, use) => await use(produce(tickData, draft => { + draft.notes = 'Not sandbagged' + draft.attemptType = choose(allowableStyleMap[tickData.style ?? 'Lead']) + draft.source = 'OB' + })), + tick: async ({ ticks, tickData }, use) => await use(await ticks.addTick(tickData)) +}) + +describe('Ticks', () => { + // test adding tick + it('should create a new tick for the associated climb', async ({ ticks, tickData }) => { + const tick = await ticks.addTick(tickData) + const newTick = await ticks.tickModel.findOne({ userId: tickData.userId }) + expect(newTick?._id).toEqual(tick._id) + }) + + // test updating tick + it('should update a tick and return the proper information', async ({ ticks, tick, tickUpdateData }) => { + const newTick = await ticks.editTick({ _id: tick._id }, tickUpdateData) + + expect(newTick).not.toBeNull() + + expect(newTick?._id).toEqual(tick._id) + expect(newTick?.notes).toEqual(tickUpdateData.notes) + expect(newTick?.attemptType).toEqual(tickUpdateData.attemptType) + }) + + // test removing tick + it('should remove a tick', async ({ ticks, tick }) => { + await ticks.deleteTick(tick._id) + const newTick = await ticks.tickModel.findOne({ _id: tick._id }) + expect(newTick).toBeNull() + }) + + // test importing ticks + it('should add an array of ticks', async ({ ticks, tickImportData }) => { + const newTicks = await ticks.importTicks(tickImportData) + + expect(newTicks).not.toBeNull() + expect(newTicks).toHaveLength(tickImportData.length) + + const tick1 = await ticks.tickModel.findOne({ _id: newTicks[0]._id }) + expect(tick1?._id).toEqual(newTicks[0]._id) + + const tick2 = await ticks.tickModel.findOne({ _id: newTicks[1]._id }) + expect(tick2?._id).toEqual(newTicks[1]._id) + + const tick3 = await ticks.tickModel.findOne({ _id: newTicks[2]._id }) + expect(tick3?._id).toEqual(newTicks[2]._id) + }) + + it('should grab all ticks by userId', async ({ ticks, tick, userUuid, profile }) => { + const newTicks = await ticks.ticksByUser({ userId: profile._id }) + newTicks.forEach(tick => expect(tick.userId).toEqual(userUuid)) + expect(newTicks[0]._id.equals(tick._id)) + }) + + it('should grab all ticks by userId and climbId', async ({ ticks, tickImportData, climb, userUuid }) => { + await Promise.all([ + ticks.addTick(tickImportData[0]), + ticks.addTick(tickImportData[1]), + ticks.addTick({ ...tickImportData[1], userId: muuidToString(muuid.v4()) }) + ]) + + const userClimbTicks = await ticks.ticksByUserIdAndClimb(muuidToString(climb._id), userUuid) + expect(userClimbTicks).toHaveLength(2) + }) + + it('should delete all ticks with the specified userId', async ({ ticks, tickImportData, userUuid }) => { + const newTicks = await ticks.importTicks(tickImportData) + + expect(newTicks).not.toBeNull() + expect(newTicks).toHaveLength(tickImportData.length) + + await ticks.deleteAllTicks(userUuid) + const newTick = await ticks.tickModel.findOne({ userId: userUuid }) + expect(newTick).toBeNull() + }) + + it('should only delete MP imports', async ({ ticks, tickData, tickUpdateData, userUuid }) => { + const MPTick = await ticks.addTick(tickData) + const OBTick = await ticks.addTick(tickUpdateData) + + expect(MPTick).not.toBeNull() + expect(OBTick).not.toBeNull() + + await ticks.deleteImportedTicks(userUuid) + const newTick = await ticks.tickModel.findOne({ _id: OBTick._id }) + expect(newTick?._id).toEqual(OBTick._id) + expect(newTick?.notes).toEqual('Not sandbagged') + }) +}) diff --git a/src/model/__tests__/ticks.ts b/src/model/__tests__/ticks.ts deleted file mode 100644 index 96baf716..00000000 --- a/src/model/__tests__/ticks.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { produce } from 'immer' -import TickDataSource from '../TickDataSource.js' -import { getTickModel, getUserModel } from '../../db/index.js' -import { TickInput } from '../../db/TickTypes.js' -import muuid from 'uuid-mongodb' -import UserDataSource from '../UserDataSource.js' -import { UpdateProfileGQLInput } from '../../db/UserTypes.js' -import inMemoryDB from '../../utils/inMemoryDB.js' -import { ClimbChangeInputType } from '../../db/ClimbTypes.js' -import MutableClimbDataSource from '../MutableClimbDataSource.js' -import MutableAreaDataSource from '../MutableAreaDataSource.js' - -const userId = muuid.v4() -const newClimbsToAdd: ClimbChangeInputType[] = [ - { - name: 'Sport 1', - // Intentionally disable TS check to make sure input is sanitized - disciplines: { - sport: true - }, - description: 'The best climb', - location: '5m left of the big tree', - protection: '5 quickdraws' - }, - { - name: 'Deep water 1', - disciplines: { - deepwatersolo: true - } - } -] - -const toTest: TickInput = { - name: 'Small Dog', - notes: 'Sandbagged', - climbId: 'tbd', // need to create a climb for tick validation - userId: userId.toUUID().toString(), - style: 'Lead', - attemptType: 'Onsight', - dateClimbed: new Date('2012-12-12'), - grade: '5.7', - source: 'MP' -} - -const toTest2: TickInput = { - name: 'Sloppy Peaches', - notes: 'v sloppy', - climbId: 'tbd', - userId: userId.toUUID().toString(), - attemptType: 'Flash', - dateClimbed: new Date('2012-10-15'), - grade: '5.10', - source: 'MP' -} - -let tickUpdate: TickInput = produce(toTest, draft => { - draft.notes = 'Not sandbagged' - draft.attemptType = 'Flash' - draft.source = 'OB' -}) - -const testImport: TickInput[] = [ - toTest, toTest2, tickUpdate -] - -describe('Ticks', () => { - let ticks: TickDataSource - let climbs: MutableClimbDataSource - let areas: MutableAreaDataSource - const tickModel = getTickModel() - - let users: UserDataSource - - beforeAll(async () => { - console.log('#BeforeAll Ticks') - await inMemoryDB.connect() - - try { - await getTickModel().collection.drop() - await getUserModel().collection.drop() - } catch (e) { - console.log('Cleaning db') - } - - ticks = TickDataSource.getInstance() - climbs = MutableClimbDataSource.getInstance() - users = UserDataSource.getInstance() - areas = MutableAreaDataSource.getInstance() - // Add climbs because add/update tick requires type validation - await areas.addCountry('usa') - const newDestination = await areas.addArea(userId, 'California', null, 'usa') - if (newDestination == null) fail('Expect new area to be created') - - const routesArea = await areas.addArea(userId, 'Sport & Trad', newDestination.metadata.area_id) - - const newIDs = await climbs.addOrUpdateClimbs(userId, routesArea.metadata.area_id, newClimbsToAdd) - - // Update tick inputs with generated climb IDs - toTest.climbId = newIDs[0] - toTest2.climbId = newIDs[1] - tickUpdate = { ...tickUpdate, climbId: newIDs[0] } // Ensure tickUpdate has the correct climbId - }) - - afterAll(async () => { - await inMemoryDB.close() - }) - - afterEach(async () => { - await getTickModel().collection.drop() - await tickModel.ensureIndexes() - }) - - // test adding tick - it('should create a new tick for the associated climb', async () => { - const tick = await ticks.addTick(toTest) - const newTick = await tickModel.findOne({ userId: toTest.userId }) - expect(newTick?._id).toEqual(tick._id) - }) - - // test updating tick - it('should update a tick and return the proper information', async () => { - const tick = await ticks.addTick(toTest) - - if (tick == null) { - fail('Tick should not be null') - } - const newTick = await ticks.editTick({ _id: tick._id }, tickUpdate) - - if (newTick == null) { - fail('The new tick should not be null') - } - expect(newTick?._id).toEqual(tick._id) - expect(newTick?.notes).toEqual(tickUpdate.notes) - expect(newTick?.attemptType).toEqual(tickUpdate.attemptType) - }) - - // test removing tick - it('should remove a tick', async () => { - const tick = await ticks.addTick(toTest) - - if (tick == null) { - fail('Tick should not be null') - } - await ticks.deleteTick(tick._id) - - const newTick = await tickModel.findOne({ _id: tick._id }) - - expect(newTick).toBeNull() - }) - - // test importing ticks - it('should add an array of ticks', async () => { - const newTicks = await ticks.importTicks(testImport) - - if (newTicks == null) { - fail(`Should add ${testImport.length} new ticks`) - } - expect(newTicks?.length).toEqual(testImport.length) - - const tick1 = await tickModel.findOne({ _id: newTicks[0]._id }) - expect(tick1?._id).toEqual(newTicks[0]._id) - - const tick2 = await tickModel.findOne({ _id: newTicks[1]._id }) - expect(tick2?._id).toEqual(newTicks[1]._id) - - const tick3 = await tickModel.findOne({ _id: newTicks[2]._id }) - expect(tick3?._id).toEqual(newTicks[2]._id) - }) - - it('should grab all ticks by userId', async () => { - const userProfileInput: UpdateProfileGQLInput = { - userUuid: userId.toUUID().toString(), - username: 'cat.dog', - email: 'cat@example.com' - } - await users.createOrUpdateUserProfile(userId, userProfileInput) - const tick = await ticks.addTick(toTest) - - if (tick == null) { - fail('Should add a new tick') - } - - const newTicks = await ticks.ticksByUser({ userId }) - - expect(newTicks.length).toEqual(1) - }) - - it('should grab all ticks by userId and climbId', async () => { - const climbId = toTest.climbId - const tick = await ticks.addTick(toTest) - const tick2 = await ticks.addTick(toTest2) - - if (tick == null || tick2 == null) { - fail('Should add a new tick') - } - const userClimbTicks = await ticks.ticksByUserIdAndClimb(climbId, userId.toUUID().toString()) - expect(userClimbTicks.length).toEqual(1) - }) - - it('should delete all ticks with the specified userId', async () => { - const newTicks = await ticks.importTicks(testImport) - - if (newTicks == null) { - fail('Should add 3 new ticks') - } - - await ticks.deleteAllTicks(userId.toUUID().toString()) - const newTick = await tickModel.findOne({ userId }) - expect(newTick).toBeNull() - }) - - it('should only delete MP imports', async () => { - const MPTick = await ticks.addTick(toTest) - const OBTick = await ticks.addTick(tickUpdate) - if (MPTick == null || OBTick == null) { - fail('Should add two new ticks') - } - - await ticks.deleteImportedTicks(userId.toUUID().toString()) - const newTick = await tickModel.findOne({ _id: OBTick._id }) - expect(newTick?._id).toEqual(OBTick._id) - expect(newTick?.notes).toEqual('Not sandbagged') - }) -}) diff --git a/src/model/__tests__/updateAreas.ts b/src/model/__tests__/updateAreas.test.ts similarity index 50% rename from src/model/__tests__/updateAreas.ts rename to src/model/__tests__/updateAreas.test.ts index b17df811..91109f91 100644 --- a/src/model/__tests__/updateAreas.ts +++ b/src/model/__tests__/updateAreas.test.ts @@ -1,132 +1,104 @@ import muuid from 'uuid-mongodb' import { geometry } from '@turf/helpers' - -import MutableAreaDataSource from '../MutableAreaDataSource.js' -import MutableClimbDataSource from '../MutableClimbDataSource.js' -import { createIndexes, getAreaModel, getClimbModel } from '../../db/index.js' +import countries from 'i18n-iso-countries' import { AreaEditableFieldsType, UpdateSortingOrderType } from '../../db/AreaTypes.js' -import inMemoryDB from '../../utils/inMemoryDB.js' +import { dataFixtures as it } from '../../__tests__/fixtures/data.fixtures' +import { gradeContextToGradeScales } from '../../GradeUtils.js' describe('Areas', () => { - let areas: MutableAreaDataSource - let climbs: MutableClimbDataSource - const testUser = muuid.v4() - - beforeAll(async () => { - await inMemoryDB.connect() - - try { - await getAreaModel().collection.drop() - await getClimbModel().collection.drop() - } catch (e) { - console.log('Cleaning up db before test', e) - } - await createIndexes() - areas = MutableAreaDataSource.getInstance() - climbs = MutableClimbDataSource.getInstance() + it('should create a country by Alpha-3 country code', async ({ areas, countryCode }) => { + const country = await areas.addCountry(countryCode.toLocaleLowerCase()) + const newArea = await areas.findOneAreaByUUID(country.metadata.area_id) + expect(newArea.area_name).toEqual(countries.getName(countryCode, 'en')) + expect(newArea.shortCode).toEqual(countryCode) }) - afterAll(async () => { - await inMemoryDB.close() + it('should create a country by Alpha-2 country code', async ({ areas, countryCode }) => { + const alpha2 = countries.alpha3ToAlpha2(countryCode) + assert(alpha2) + const country = await areas.addCountry(alpha2) + expect(country.area_name).toEqual(countries.getName(countryCode, 'en')) + // should be expanded to the long country code + expect(country.shortCode).toEqual(countryCode) }) - it('should create a country by Alpha-3 country code', async () => { - const spain = await areas.addCountry('esP') - const newArea = await areas.findOneAreaByUUID(spain.metadata.area_id) - expect(newArea.area_name).toEqual('Spain') - expect(newArea.shortCode).toEqual('ESP') - }) - - it('should create a country by Alpha-2 country code', async () => { - const country = await areas.addCountry('ch') - expect(country.area_name).toEqual('Switzerland') - expect(country.shortCode).toEqual('CHE') - }) - - it('should create a country and 2 subareas', async () => { - const canada = await areas.addCountry('can') + it('should create a country and 2 subareas', async ({ areas, user, countryCode }) => { + const country = await areas.addCountry(countryCode) // Add 1st area to the country - const bc = await areas.addArea(testUser, 'British Columbia', canada.metadata.area_id) + const district = await areas.addArea(user, 'British Columbia', country.metadata.area_id) + assert(district != null) + assert(country != null) - if (bc == null || canada == null) { - fail() - } - expect(canada.metadata.lnglat).not.toMatchObject(geometry('Point', [0, 0])) - expect(bc.area_name).toEqual('British Columbia') + expect(country.metadata.lnglat).not.toMatchObject(geometry('Point', [0, 0])) + expect(district.area_name).toEqual('British Columbia') - expect(bc.metadata.lnglat).toEqual(canada.metadata.lnglat) + expect(district.metadata.lnglat).toEqual(country.metadata.lnglat) - let canadaInDb = await areas.findOneAreaByUUID(canada.metadata.area_id) + let countryInDB = await areas.findOneAreaByUUID(country.metadata.area_id) - expect(canadaInDb.children.length).toEqual(1) - expect(canadaInDb.children[0]).toEqual(bc?._id) + expect(countryInDB.children.length).toEqual(1) + expect(countryInDB.children[0]).toEqual(district?._id) // Add another area to the country - const theBug = await areas.addArea(testUser, 'The Bugaboos', canada.metadata.area_id) + const province = await areas.addArea(user, 'The Bugaboos', country.metadata.area_id) - canadaInDb = await areas.findOneAreaByUUID(canada.metadata.area_id) - expect(canadaInDb.children.length).toEqual(2) - expect(canadaInDb.children[1]).toEqual(theBug?._id) + countryInDB = await areas.findOneAreaByUUID(country.metadata.area_id) + expect(countryInDB.children.length).toEqual(2) + expect(countryInDB.children[1]).toEqual(province?._id) // Verify paths and ancestors - if (theBug != null) { // make TS happy - expect(theBug.ancestors) - .toEqual(`${canada.metadata.area_id.toUUID().toString()},${theBug?.metadata.area_id.toUUID().toString()}`) - expect(theBug.pathTokens) - .toEqual([canada.area_name, theBug.area_name]) - } + assert(province !== null) + + expect(province.ancestors) + .toEqual(`${country.metadata.area_id.toUUID().toString()},${province?.metadata.area_id.toUUID().toString()}`) + expect(province.pathTokens) + .toEqual([country.area_name, province.area_name]) }) - it('should allow adding child areas to empty leaf area', async () => { - let parent = await areas.addArea(testUser, 'My house', null, 'can') - await areas.updateArea(testUser, parent.metadata.area_id, { isLeaf: true, isBoulder: true }) + it('should allow adding child areas to empty leaf area', async ({ areas, user, climbs, country, area }) => { + await areas.updateArea(user, area.metadata.area_id, { isLeaf: true, isBoulder: true }) - const newClimb = await climbs.addOrUpdateClimbs(testUser, parent.metadata.area_id, [{ name: 'Big Mac' }]) + gradeContextToGradeScales[country.gradeContext] = gradeContextToGradeScales.US + const newClimb = await climbs.addOrUpdateClimbs(user, area.metadata.area_id, [{ name: 'Big Mac' }]) // Try to add a new area when there's already a climb - await expect(areas.addArea(testUser, 'Kitchen', parent.metadata.area_id)).rejects.toThrow(/Adding new areas to a leaf or boulder area is not allowed/) + await expect(areas.addArea(user, 'Kitchen', area.metadata.area_id)).rejects.toThrow('Adding new areas to a leaf or boulder area is not allowed') // Now remove the climb to see if we can add the area + await climbs.deleteClimbs(user, area.metadata.area_id, [muuid.from(newClimb[0])]) + await areas.addArea(user, 'Kitchen', area.metadata.area_id) - await climbs.deleteClimbs(testUser, parent.metadata.area_id, [muuid.from(newClimb[0])]) - await areas.addArea(testUser, 'Kitchen', parent.metadata.area_id) + // Reload the parent area + area = await areas.findOneAreaByUUID(area.metadata.area_id) - // Reload the parent - parent = await areas.findOneAreaByUUID(parent.metadata.area_id) - expect(parent.climbs).toHaveLength(0) - expect(parent.children).toHaveLength(1) + expect(area.climbs).toHaveLength(0) + expect(area.children).toHaveLength(1) // make sure leaf and boulder flag are cleared - expect(parent.metadata.leaf).toBeFalsy() - expect(parent.metadata.isBoulder).toBeFalsy() + expect(area.metadata.leaf).toBeFalsy() + expect(area.metadata.isBoulder).toBeFalsy() }) - it('should create an area using only country code (without parent id)', async () => { - const country = await areas.addCountry('za') - const area = await areas.addArea(testUser, 'Table mountain', null, 'zaf') + it('should create an area using only country code (without parent id)', async ({ areas, user, countryCode }) => { + const country = await areas.addCountry(countryCode) + const area = await areas.addArea(user, 'Table mountain', null, countryCode) const countryInDb = await areas.findOneAreaByUUID(country.metadata.area_id) expect(countryInDb.children.length).toEqual(1) expect(countryInDb.children[0]).toEqual(area?._id) }) - it('should set crag/boulder attribute when adding new areas', async () => { - let parent = await areas.addArea(testUser, 'Boulder A', null, 'can', undefined, false, true) + it('should set crag/boulder attribute when adding new areas', async ({ areas, user, country }) => { + let parent = await areas.addArea(user, 'Boulder A', country.metadata.area_id, undefined, undefined, false, true) expect(parent.metadata.isBoulder).toBe(true) expect(parent.metadata.leaf).toBe(true) - parent = await areas.addArea(testUser, 'Sport A', null, 'can', undefined, true, undefined) + parent = await areas.addArea(user, 'Sport A', country.metadata.area_id, undefined, undefined, true, undefined) expect(parent.metadata.isBoulder).toBe(false) expect(parent.metadata.leaf).toBe(true) }) - it('should update multiple fields', async () => { - await areas.addCountry('au') - const a1 = await areas.addArea(testUser, 'One', null, 'au') - - if (a1 == null) { - fail() - } - // for testing area description and location is sanitized + it('should update multiple fields', async ({ areas, user, area }) => { + // for testing area desccription is sanitized const iframeStr = '' const doc1: AreaEditableFieldsType = { areaName: '1', @@ -135,7 +107,7 @@ describe('Areas', () => { areaLocation: `This is a cool area location with some malicious code.${iframeStr}`, isDestination: true } - let a1Updated = await areas.updateArea(testUser, a1?.metadata.area_id, doc1) + let a1Updated = await areas.updateArea(user, area?.metadata.area_id, doc1) expect(a1Updated?.area_name).toEqual(doc1.areaName) expect(a1Updated?.shortCode).toEqual(doc1.shortCode) @@ -150,36 +122,28 @@ describe('Areas', () => { lat: 46.433333, lng: 11.85 } - a1Updated = await areas.updateArea(testUser, a1?.metadata.area_id, doc2) + a1Updated = await areas.updateArea(user, area?.metadata.area_id, doc2) expect(a1Updated?.metadata.lnglat).toEqual(geometry('Point', [doc2.lng, doc2.lat])) expect(a1Updated?.metadata.isDestination).toEqual(doc2.isDestination) }) - it('should not update country name and code', async () => { - const country = await areas.addCountry('lao') - if (country == null) fail() - await expect(areas.updateArea(testUser, country.metadata.area_id, { areaName: 'Foo' })).rejects.toThrowError() - - // eslint-disable-next-line - await new Promise(res => setTimeout(res, 2000)) - - await expect(areas.updateArea(testUser, country.metadata.area_id, { shortCode: 'Foo' })).rejects.toThrowError() + it('should not update country name and code', async ({ areas, user, country }) => { + await expect(areas.updateArea(user, country.metadata.area_id, { areaName: 'Foo' })).rejects.toThrowError() }) - it('should delete a subarea', async () => { - const usa = await areas.addCountry('usa') - const ca = await areas.addArea(testUser, 'CA', usa.metadata.area_id) - const or = await areas.addArea(testUser, 'OR', usa.metadata.area_id) - const wa = await areas.addArea(testUser, 'WA', usa.metadata.area_id) + it('should delete a subarea', async ({ areas, user, country }) => { + const ca = await areas.addArea(user, 'CA', country.metadata.area_id) + const or = await areas.addArea(user, 'OR', country.metadata.area_id) + const wa = await areas.addArea(user, 'WA', country.metadata.area_id) - if (ca == null || or == null || wa == null) { - fail('Child area is null') - } + assert(ca != null, 'child area is null') + assert(or != null, 'child area is null') + assert(wa != null, 'child area is null') - // eslint-disable-next-line - await new Promise(res => setTimeout(res, 3000)) + // + // await new Promise(res => setTimeout(res, 3000)) - let usaInDB = await areas.findOneAreaByUUID(usa.metadata.area_id) + let usaInDB = await areas.findOneAreaByUUID(country.metadata.area_id) // verify number of child areas in parent expect(usaInDB.children as any[]).toHaveLength(3) @@ -190,9 +154,9 @@ describe('Areas', () => { wa._id ]) - await areas.deleteArea(testUser, ca.metadata.area_id) + await areas.deleteArea(user, ca.metadata.area_id) - usaInDB = await areas.findOneAreaByUUID(usa.metadata.area_id) + usaInDB = await areas.findOneAreaByUUID(country.metadata.area_id) // verify child area IDs (one less than before) expect(usaInDB.children as any[]).toHaveLength(2) @@ -204,60 +168,60 @@ describe('Areas', () => { await expect(areas.findOneAreaByUUID(ca.metadata.area_id)).rejects.toThrow(/Area.*not found/) }) - it('should not delete a subarea containing children', async () => { - const gr = await areas.addCountry('grc') - const kali = await areas.addArea(testUser, 'Kalymnos', gr.metadata.area_id) + it('should not delete a subarea containing children', async ({ areas, user, countryCode }) => { + const country = await areas.addCountry(countryCode) + const province = await areas.addArea(user, 'Kalymnos', country.metadata.area_id) - if (kali == null) fail() + assert(province != null) - const arhi = await areas.addArea(testUser, 'Arhi', kali.metadata.area_id) + const arhi = await areas.addArea(user, 'Arhi', province.metadata.area_id) - if (arhi == null) fail() + assert(arhi != null) // Try to delete 'Arhi' (expecting exception) - await expect(areas.deleteArea(testUser, kali.metadata.area_id)).rejects.toThrow('subareas not empty') + await expect(areas.deleteArea(user, province.metadata.area_id)).rejects.toThrow('subareas not empty') const arhiInDb = await areas.findOneAreaByUUID(arhi.metadata.area_id) expect(arhiInDb._id).toEqual(arhi._id) }) - it('should not create duplicate countries', async () => { - await areas.addCountry('ita') + it('should not create duplicate countries', async ({ areas, user, countryCode }) => { + await areas.addCountry(countryCode) // eslint-disable-next-line await new Promise(res => setTimeout(res, 2000)) - await expect(areas.addCountry('ita')).rejects.toThrowError('This name already exists for some other area in this parent') + await expect(areas.addCountry(countryCode)).rejects.toThrowError('This name already exists for some other area in this parent') }) - it('should not create duplicate sub-areas', async () => { - const fr = await areas.addCountry('fra') - await areas.addArea(testUser, 'Verdon Gorge', fr.metadata.area_id) - await expect(areas.addArea(testUser, 'Verdon Gorge', fr.metadata.area_id)) + it('should not create duplicate sub-areas', async ({ areas, user, countryCode }) => { + const country = await areas.addCountry(countryCode) + await areas.addArea(user, 'Verdon Gorge', country.metadata.area_id) + await expect(areas.addArea(user, 'Verdon Gorge', country.metadata.area_id)) .rejects.toThrowError('This name already exists for some other area in this parent') }) - it('should fail when adding without a parent country', async () => { - await expect(areas.addArea(testUser, 'Peak District ', null, 'GB')) + it('should fail when adding without a parent country', async ({ areas, user, climbs }) => { + await expect(areas.addArea(user, 'Peak District ', null, 'GB')) .rejects.toThrowError() }) - it('should fail when adding with a non-existent parent id', async () => { + it('should fail when adding with a non-existent parent id', async ({ areas, user, climbs }) => { const notInDb = muuid.from('abf6cb8b-8461-45c3-b46b-5997444be867') - await expect(areas.addArea(testUser, 'Land\'s End ', notInDb)) + await expect(areas.addArea(user, 'Land\'s End ', notInDb)) .rejects.toThrowError() }) - it('should fail when adding with null parents', async () => { - await expect(areas.addArea(testUser, 'Land\'s End ', null, '1q1')) + it('should fail when adding with null parents', async ({ areas, user, climbs }) => { + await expect(areas.addArea(user, 'Land\'s End ', null, '1q1')) .rejects.toThrowError() }) - it('should update areas sorting order', async () => { + it('should update areas sorting order', async ({ areas, user, climbs }) => { // Setup await areas.addCountry('MX') - const a1 = await areas.addArea(testUser, 'A1', null, 'MX') - const a2 = await areas.addArea(testUser, 'A2', null, 'MX') + const a1 = await areas.addArea(user, 'A1', null, 'MX') + const a2 = await areas.addArea(user, 'A2', null, 'MX') const change1: UpdateSortingOrderType = { areaId: a1.metadata.area_id.toUUID().toString(), @@ -269,7 +233,7 @@ describe('Areas', () => { } // Update - await areas.updateSortingOrder(testUser, [change1, change2]) + await areas.updateSortingOrder(user, [change1, change2]) // Verify const a1Actual = await areas.findOneAreaByUUID(a1.metadata.area_id) @@ -291,15 +255,15 @@ describe('Areas', () => { })) }) - it('should update self and childrens pathTokens', async () => { + it('should update self and childrens pathTokens', async ({ areas, user, climbs }) => { await areas.addCountry('JP') - const a1 = await areas.addArea(testUser, 'Parent', null, 'JP') - const b1 = await areas.addArea(testUser, 'B1', a1.metadata.area_id) - const b2 = await areas.addArea(testUser, 'B2', a1.metadata.area_id) - const c1 = await areas.addArea(testUser, 'C1', b1.metadata.area_id) - const c2 = await areas.addArea(testUser, 'C2', b1.metadata.area_id) - const c3 = await areas.addArea(testUser, 'C3', b2.metadata.area_id) - const e1 = await areas.addArea(testUser, 'E1', c3.metadata.area_id) + const a1 = await areas.addArea(user, 'Parent', null, 'JP') + const b1 = await areas.addArea(user, 'B1', a1.metadata.area_id) + const b2 = await areas.addArea(user, 'B2', a1.metadata.area_id) + const c1 = await areas.addArea(user, 'C1', b1.metadata.area_id) + const c2 = await areas.addArea(user, 'C2', b1.metadata.area_id) + const c3 = await areas.addArea(user, 'C3', b2.metadata.area_id) + const e1 = await areas.addArea(user, 'E1', c3.metadata.area_id) let a1Actual = await areas.findOneAreaByUUID(a1.metadata.area_id) expect(a1Actual).toEqual( @@ -348,7 +312,7 @@ describe('Areas', () => { const doc1: AreaEditableFieldsType = { areaName: 'Test Name' } - await areas.updateArea(testUser, a1?.metadata.area_id, doc1) + await areas.updateArea(user, a1?.metadata.area_id, doc1) // Verify a1Actual = await areas.findOneAreaByUUID(a1.metadata.area_id) diff --git a/src/utils/inMemoryDB.ts b/src/utils/inMemoryDB.ts deleted file mode 100644 index f30e8eef..00000000 --- a/src/utils/inMemoryDB.ts +++ /dev/null @@ -1,84 +0,0 @@ -import mongoose, { ConnectOptions } from 'mongoose' -import { ChangeStream, ChangeStreamDocument, MongoClient } from 'mongodb' -import { MongoMemoryReplSet } from 'mongodb-memory-server' -import { checkVar, defaultPostConnect } from '../db/index.js' -import { logger } from '../logger.js' -import { testStreamListener } from '../db/edit/streamListener' - -/** - * In-memory Mongo replset used for testing. - * More portable than requiring user to set up Mongo in a background Docker process. - * Need a replset to faciliate transactions. - */ -let mongod: MongoMemoryReplSet -let stream: ChangeStream | undefined - -/** - * Connect to the in-memory database. - */ -export const connect = async (onChange?: (change: ChangeStreamDocument) => void): Promise => { - mongod = await MongoMemoryReplSet.create({ - // Stream listener listens on DB denoted by 'MONGO_DBNAME' env var. - replSet: { count: 1, storageEngine: 'wiredTiger', dbName: checkVar('MONGO_DBNAME') } - }) - const uri = await mongod.getUri(checkVar('MONGO_DBNAME')) - logger.info(`Connecting to in-memory database ${uri}`) - const mongooseOpts: ConnectOptions = { - autoIndex: false // Create indices using defaultPostConnect instead. - } - - await mongoose.connect(uri, mongooseOpts) - mongoose.set('debug', false) // Set to 'true' to enable verbose mode - stream = await defaultPostConnect(async () => await testStreamListener(onChange)) -} - -/** - * Drop database, close the connection and stop mongod. - */ -export const close = async (): Promise => { - await stream?.close() - await mongoose.connection.dropDatabase() - await mongoose.connection.close() - await mongod.stop() -} - -/** - * Remove all the data for all db collections. - */ -export const clear = async (): Promise => { - const collections = mongoose.connection.collections - - for (const key in collections) { - const collection = collections[key] - await collection.deleteMany({}) - } -} - -/** - * Bypass Mongoose to insert data directly into Mongo. - * Useful for inserting data that is incompatible with Mongoose schemas for migration testing. - * @param collection Name of collection for documents to be inserted into. - * @param docs Documents to be inserted into collection. - */ -const insertDirectly = async (collection: string, documents: any[]): Promise => { - const uri = await mongod.getUri(checkVar('MONGO_DBNAME')) - const client = new MongoClient(uri) - try { - const database = client.db(checkVar('MONGO_DBNAME')) - const mCollection = database.collection(collection) - const result = await mCollection.insertMany(documents) - - console.log(`${result.insertedCount} documents were inserted directly into MongoDB`) - } finally { - void client.close() - } -} - -export interface InMemoryDB { - connect: () => Promise - close: () => Promise - clear: () => Promise - insertDirectly: (collection: string, documents: any[]) => Promise -} - -export default { connect, close, clear, insertDirectly, stream } diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts index 6006cccf..e49da079 100644 --- a/src/utils/testUtils.ts +++ b/src/utils/testUtils.ts @@ -1,79 +1,3 @@ -import jwt from 'jsonwebtoken' -import { jest } from '@jest/globals' -import request from 'supertest' -import { ApolloServer } from '@apollo/server' -import express from 'express' - -import type { InMemoryDB } from './inMemoryDB.js' -import inMemoryDB from './inMemoryDB.js' -import { createServer } from '../server.js' - -const PORT = 4000 - -export interface QueryAPIProps { - query?: string - operationName?: string - variables?: any - userUuid?: string - roles?: string[] - port?: number - endpoint?: string - app?: express.Application - body?: any -} - -/* - * Helper function for querying the locally-served API. It mocks JWT verification - * so we can pretend to have an role we want when calling the API. - */ -export const queryAPI = async ({ - query, - operationName, - variables, - userUuid = '', - roles = [], - app, - endpoint = '/', - port = PORT -}: QueryAPIProps): Promise => { - // Avoid needing to pass in actual signed tokens. - const jwtSpy = jest.spyOn(jwt, 'verify') - jwtSpy.mockImplementation(() => { - return { - // Roles defined at https://manage.auth0.com/dashboard/us/dev-fmjy7n5n/roles - 'https://tacos.openbeta.io/roles': roles, - 'https://tacos.openbeta.io/uuid': userUuid - } - }) - - const queryObj = { query, operationName, variables } - let req = request(app ?? `http://localhost:${port}`) - .post(endpoint) - .send(queryObj) - - if (userUuid != null) { - req = req.set('Authorization', 'Bearer placeholder-jwt-see-SpyOn') - } - - return await req -} - -export interface SetUpServerReturnType { - server: ApolloServer - app: express.Application - inMemoryDB: InMemoryDB -} - -/* - * Starts Apollo server and has Mongo inMemory replset connect to it. -*/ -export const setUpServer = async (): Promise => { - await inMemoryDB.connect() - - const { app, server } = await createServer() - return { app, server, inMemoryDB } -} - export const isFulfilled = ( p: PromiseSettledResult ): p is PromiseFulfilledResult => p.status === 'fulfilled' diff --git a/tsconfig.json b/tsconfig.json index 3b503295..e8439192 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "types": [ "node", - "jest", + "vitest/globals", ], "allowJs": true, "skipLibCheck": true, @@ -19,5 +19,6 @@ }, "include": [ "src/**/*.ts", + "vite.config.ts" ] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..464d56af --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + globals: true, + globalSetup: './src/__tests__/setup.ts', + environment: 'node', + + pool: 'threads', + poolOptions: { + threads: { + isolate: false + } + } + } +}) diff --git a/yarn.lock b/yarn.lock index c19a69ad..805ab331 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,6 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - "@apollo/cache-control-types@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz#5da62cf64c3b4419dabfef4536b57a40c8ff0b47" @@ -168,234 +160,6 @@ resolved "https://registry.yarnpkg.com/@apollo/utils.withrequired/-/utils.withrequired-2.0.1.tgz#e72bc512582a6f26af150439f7eb7473b46ba874" integrity sha512-YBDiuAX9i1lLc6GeTy1m7DGLFn/gMnvXqlalOIMjM7DeOgIacEjjfwPqb0M1CQ2v11HhR15d1NmxJoRCfrNqcA== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== - dependencies: - "@babel/helper-validator-identifier" "^7.25.9" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/compat-data@^7.25.9": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" - integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== - -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" - integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.26.0" - "@babel/generator" "^7.26.0" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helpers" "^7.26.0" - "@babel/parser" "^7.26.0" - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.26.0" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.25.9", "@babel/generator@^7.26.0", "@babel/generator@^7.7.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" - integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== - dependencies: - "@babel/parser" "^7.26.2" - "@babel/types" "^7.26.0" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^3.0.2" - -"@babel/helper-compilation-targets@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" - integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== - dependencies: - "@babel/compat-data" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - browserslist "^4.24.0" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-module-imports@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.8.0": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" - integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== - -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/helper-validator-option@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" - integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== - -"@babel/helpers@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" - integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== - dependencies: - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" - integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== - dependencies: - "@babel/types" "^7.26.0" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-import-attributes@^7.24.7": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" - integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.7.2": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" - integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.7.2": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" - integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.21.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" @@ -403,40 +167,130 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.25.9", "@babel/template@^7.3.3": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" - integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/traverse@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" - integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/template" "^7.25.9" - "@babel/types" "^7.25.9" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.3.3": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" - integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@esbuild/aix-ppc64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437" + integrity sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ== + +"@esbuild/android-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz#649e47e04ddb24a27dc05c395724bc5f4c55cbfe" + integrity sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ== + +"@esbuild/android-arm@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.3.tgz#8a0f719c8dc28a4a6567ef7328c36ea85f568ff4" + integrity sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A== + +"@esbuild/android-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.3.tgz#e2ab182d1fd06da9bef0784a13c28a7602d78009" + integrity sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ== + +"@esbuild/darwin-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz#c7f3166fcece4d158a73dcfe71b2672ca0b1668b" + integrity sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w== + +"@esbuild/darwin-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz#d8c5342ec1a4bf4b1915643dfe031ba4b173a87a" + integrity sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A== + +"@esbuild/freebsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz#9f7d789e2eb7747d4868817417cc968ffa84f35b" + integrity sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw== + +"@esbuild/freebsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz#8ad35c51d084184a8e9e76bb4356e95350a64709" + integrity sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q== + +"@esbuild/linux-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz#3af0da3d9186092a9edd4e28fa342f57d9e3cd30" + integrity sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A== + +"@esbuild/linux-arm@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz#e91cafa95e4474b3ae3d54da12e006b782e57225" + integrity sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ== + +"@esbuild/linux-ia32@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz#81025732d85b68ee510161b94acdf7e3007ea177" + integrity sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw== + +"@esbuild/linux-loong64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz#3c744e4c8d5e1148cbe60a71a11b58ed8ee5deb8" + integrity sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g== + +"@esbuild/linux-mips64el@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz#1dfe2a5d63702db9034cc6b10b3087cc0424ec26" + integrity sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag== + +"@esbuild/linux-ppc64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz#2e85d9764c04a1ebb346dc0813ea05952c9a5c56" + integrity sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg== + +"@esbuild/linux-riscv64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz#a9ea3334556b09f85ccbfead58c803d305092415" + integrity sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA== + +"@esbuild/linux-s390x@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz#f6a7cb67969222b200974de58f105dfe8e99448d" + integrity sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ== + +"@esbuild/linux-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz#a237d3578ecdd184a3066b1f425e314ade0f8033" + integrity sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA== + +"@esbuild/netbsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz#4c15c68d8149614ddb6a56f9c85ae62ccca08259" + integrity sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA== + +"@esbuild/netbsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz#12f6856f8c54c2d7d0a8a64a9711c01a743878d5" + integrity sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g== + +"@esbuild/openbsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz#ca078dad4a34df192c60233b058db2ca3d94bc5c" + integrity sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ== + +"@esbuild/openbsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz#c9178adb60e140e03a881d0791248489c79f95b2" + integrity sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w== + +"@esbuild/sunos-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz#03765eb6d4214ff27e5230af779e80790d1ee09f" + integrity sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA== + +"@esbuild/win32-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz#f1c867bd1730a9b8dfc461785ec6462e349411ea" + integrity sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ== + +"@esbuild/win32-ia32@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz#77491f59ef6c9ddf41df70670d5678beb3acc322" + integrity sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew== + +"@esbuild/win32-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz#b17a2171f9074df9e91bfb07ef99a892ac06412a" + integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg== "@eslint-community/eslint-utils@^4.2.0": version "4.4.1" @@ -621,246 +475,11 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - dependencies: - camelcase "^5.3.1" - find-up "^4.1.0" - get-package-type "^0.1.0" - js-yaml "^3.13.1" - resolve-from "^5.0.0" - -"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/console@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" - integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - -"@jest/core@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" - integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== - dependencies: - "@jest/console" "^29.7.0" - "@jest/reporters" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - ci-info "^3.2.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-changed-files "^29.7.0" - jest-config "^29.7.0" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-resolve-dependencies "^29.7.0" - jest-runner "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - jest-watcher "^29.7.0" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - -"@jest/environment@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" - integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== - dependencies: - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - -"@jest/expect-utils@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" - integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== - dependencies: - jest-get-type "^29.6.3" - -"@jest/expect@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" - integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== - dependencies: - expect "^29.7.0" - jest-snapshot "^29.7.0" - -"@jest/fake-timers@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" - integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== - dependencies: - "@jest/types" "^29.6.3" - "@sinonjs/fake-timers" "^10.0.2" - "@types/node" "*" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-util "^29.7.0" - -"@jest/globals@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" - integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/types" "^29.6.3" - jest-mock "^29.7.0" - -"@jest/reporters@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" - integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - "@types/node" "*" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^6.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.1.3" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - jest-worker "^29.7.0" - slash "^3.0.0" - string-length "^4.0.1" - strip-ansi "^6.0.0" - v8-to-istanbul "^9.0.1" - -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - -"@jest/source-map@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" - integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.18" - callsites "^3.0.0" - graceful-fs "^4.2.9" - -"@jest/test-result@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" - integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== - dependencies: - "@jest/console" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-sequencer@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" - integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== - dependencies: - "@jest/test-result" "^29.7.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - slash "^3.0.0" - -"@jest/transform@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" - integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.2" - -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== - dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - "@mongodb-js/saslprep@^1.1.0", "@mongodb-js/saslprep@^1.1.9": version "1.1.9" resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz#e974bab8eca9faa88677d4ea4da8d09a52069004" @@ -957,30 +576,111 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@rollup/rollup-android-arm-eabi@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz#d964ee8ce4d18acf9358f96adc408689b6e27fe3" + integrity sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg== + +"@rollup/rollup-android-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz#9b5e130ecc32a5fc1e96c09ff371743ee71a62d3" + integrity sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w== + +"@rollup/rollup-darwin-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz#ef439182c739b20b3c4398cfc03e3c1249ac8903" + integrity sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ== + +"@rollup/rollup-darwin-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz#d7380c1531ab0420ca3be16f17018ef72dd3d504" + integrity sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA== + +"@rollup/rollup-freebsd-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz#cbcbd7248823c6b430ce543c59906dd3c6df0936" + integrity sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg== + +"@rollup/rollup-freebsd-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz#96bf6ff875bab5219c3472c95fa6eb992586a93b" + integrity sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw== + +"@rollup/rollup-linux-arm-gnueabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz#d80cd62ce6d40f8e611008d8dbf03b5e6bbf009c" + integrity sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA== + +"@rollup/rollup-linux-arm-musleabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz#75440cfc1e8d0f87a239b4c31dfeaf4719b656b7" + integrity sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg== + +"@rollup/rollup-linux-arm64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz#ac527485ecbb619247fb08253ec8c551a0712e7c" + integrity sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg== + +"@rollup/rollup-linux-arm64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz#74d2b5cb11cf714cd7d1682e7c8b39140e908552" + integrity sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ== + +"@rollup/rollup-linux-loongarch64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz#a0a310e51da0b5fea0e944b0abd4be899819aef6" + integrity sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg== + +"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz#4077e2862b0ac9f61916d6b474d988171bd43b83" + integrity sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw== + +"@rollup/rollup-linux-riscv64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz#5812a1a7a2f9581cbe12597307cc7ba3321cf2f3" + integrity sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA== + +"@rollup/rollup-linux-riscv64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz#973aaaf4adef4531375c36616de4e01647f90039" + integrity sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ== + +"@rollup/rollup-linux-s390x-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz#9bad59e907ba5bfcf3e9dbd0247dfe583112f70b" + integrity sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw== + +"@rollup/rollup-linux-x64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz#68b045a720bd9b4d905f462b997590c2190a6de0" + integrity sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ== + +"@rollup/rollup-linux-x64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz#8e703e2c2ad19ba7b2cb3d8c3a4ad11d4ee3a282" + integrity sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw== + +"@rollup/rollup-win32-arm64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz#c5bee19fa670ff5da5f066be6a58b4568e9c650b" + integrity sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ== + +"@rollup/rollup-win32-ia32-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz#846e02c17044bd922f6f483a3b4d36aac6e2b921" + integrity sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA== + +"@rollup/rollup-win32-x64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76" + integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ== + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - -"@sinonjs/commons@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" - integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== - dependencies: - type-detect "4.0.8" - -"@sinonjs/fake-timers@^10.0.2": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" - integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== - dependencies: - "@sinonjs/commons" "^3.0.0" - "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -1058,39 +758,6 @@ resolved "https://registry.yarnpkg.com/@types/auth0/-/auth0-3.3.10.tgz#6c483d7ebe6e3eb1fb44d5c27aecbe49ad0b200d" integrity sha512-9tS0Y2igWxw+Dx5uCHkIUCu6tG0oRkwpE322dOJPwZMLXQMx49n/gDmUz7YJSe1iVjrWW+ffVYmlPShVIEwjkg== -"@types/babel__core@^7.1.14": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.8" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" - integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" - integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" - integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== - dependencies: - "@babel/types" "^7.20.7" - "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -1111,6 +778,11 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== +"@types/estree@1.0.7", "@types/estree@^1.0.0": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@^4.17.30", "@types/express-serve-static-core@^4.17.33": version "4.19.6" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" @@ -1131,45 +803,11 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/graceful-fs@^4.1.3": - version "4.1.9" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" - integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== - dependencies: - "@types/node" "*" - "@types/http-errors@*": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" - integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== - -"@types/istanbul-lib-report@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" - integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" - integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/jest@^29.4.0": - version "29.5.14" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" - integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== - dependencies: - expect "^29.0.0" - pretty-format "^29.0.0" - "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1268,11 +906,6 @@ "@types/node" "*" "@types/send" "*" -"@types/stack-utils@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" - integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== - "@types/superagent@*": version "8.1.9" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" @@ -1320,18 +953,6 @@ "@types/node" "*" "@types/webidl-conversions" "*" -"@types/yargs-parser@*": - version "21.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" - integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== - -"@types/yargs@^17.0.8": - version "17.0.33" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" - integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== - dependencies: - "@types/yargs-parser" "*" - "@types/yup@0.29.13": version "0.29.13" resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.13.tgz#21b137ba60841307a3c8a1050d3bf4e63ad561e9" @@ -1426,6 +1047,65 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@vitest/expect@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.1.2.tgz#b203a7ad2efa6af96c85f6c116216bda259d2bc8" + integrity sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA== + dependencies: + "@vitest/spy" "3.1.2" + "@vitest/utils" "3.1.2" + chai "^5.2.0" + tinyrainbow "^2.0.0" + +"@vitest/mocker@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.1.2.tgz#1ff239036072feb543ab56825ada09b12a075af2" + integrity sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw== + dependencies: + "@vitest/spy" "3.1.2" + estree-walker "^3.0.3" + magic-string "^0.30.17" + +"@vitest/pretty-format@3.1.2", "@vitest/pretty-format@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.1.2.tgz#689b0604c0b73fdccb144f11b64d70c9233b23b8" + integrity sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w== + dependencies: + tinyrainbow "^2.0.0" + +"@vitest/runner@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.1.2.tgz#ffeba74618046221e944e94f09b565af772170cf" + integrity sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g== + dependencies: + "@vitest/utils" "3.1.2" + pathe "^2.0.3" + +"@vitest/snapshot@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.1.2.tgz#46c52a417afbf1fe94fba0a5735cbedf9cfc60f6" + integrity sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q== + dependencies: + "@vitest/pretty-format" "3.1.2" + magic-string "^0.30.17" + pathe "^2.0.3" + +"@vitest/spy@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.1.2.tgz#3a5be04d71c4a458c8d6859503626e2aed61bcbf" + integrity sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA== + dependencies: + tinyspy "^3.0.2" + +"@vitest/utils@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.1.2.tgz#f3ae55b3a205c88c346a2a8dcde7c89210364932" + integrity sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg== + dependencies: + "@vitest/pretty-format" "3.1.2" + loupe "^3.1.3" + tinyrainbow "^2.0.0" + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -1475,13 +1155,6 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -1499,24 +1172,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@^3.0.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - apollo-datasource-mongodb@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/apollo-datasource-mongodb/-/apollo-datasource-mongodb-0.6.0.tgz#643e3311cff1861a11cc967eff7259d0a84342e3" @@ -1526,13 +1186,6 @@ apollo-datasource-mongodb@^0.6.0: bson "^5.4.0" dataloader "^1.4.0" -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1647,6 +1300,11 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + async-mutex@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.5.0.tgz#353c69a0b9e75250971a64ac203b0ebfddd75482" @@ -1661,11 +1319,6 @@ async-retry@^1.2.1, async-retry@^1.3.3: dependencies: retry "0.13.1" -async@^3.2.3: - version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1711,69 +1364,6 @@ b4a@^1.6.4: resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== -babel-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" - integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== - dependencies: - "@jest/transform" "^29.7.0" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.6.3" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - -babel-plugin-istanbul@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" - integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^5.0.4" - test-exclude "^6.0.0" - -babel-plugin-jest-hoist@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" - integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.1.14" - "@types/babel__traverse" "^7.0.6" - -babel-preset-current-node-syntax@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" - integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - -babel-preset-jest@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" - integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== - dependencies: - babel-plugin-jest-hoist "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1878,30 +1468,6 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0: - version "4.24.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" - integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== - dependencies: - caniuse-lite "^1.0.30001669" - electron-to-chromium "^1.5.41" - node-releases "^2.0.18" - update-browserslist-db "^1.1.1" - -bs-logger@^0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" - integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== - dependencies: - fast-json-stable-stringify "2.x" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - bson@^5.4.0, bson@^5.5.0: version "5.5.1" resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.1.tgz#f5849d405711a7f23acdda9a442375df858e6833" @@ -1922,11 +1488,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -1947,6 +1508,11 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -1971,22 +1537,23 @@ camel-case@^1.1.1: sentence-case "^1.1.1" upper-case "^1.1.1" -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.2.0, camelcase@^6.3.0: +camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001669: - version "1.0.30001684" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz#0eca437bab7d5f03452ff0ef9de8299be6b08e16" - integrity sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ== +chai@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.2.0.tgz#1358ee106763624114addf84ab02697e411c9c05" + integrity sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" -chalk@^4.0.0, chalk@^4.0.2: +chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2016,45 +1583,16 @@ change-case@^2.3.0: upper-case "^1.1.1" upper-case-first "^1.1.0" -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -cjs-module-lexer@^1.0.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" - integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - -collect-v8-coverage@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" - integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -2152,11 +1690,6 @@ content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -2180,19 +1713,6 @@ cors@^2.8.5: object-assign "^4" vary "^1" -create-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" - integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-config "^29.7.0" - jest-util "^29.7.0" - prompts "^2.0.1" - cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -2200,7 +1720,7 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -2260,7 +1780,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.x, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: +debug@4, debug@4.x, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -2274,6 +1794,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.7, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -2281,10 +1808,10 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -dedent@^1.0.0: - version "1.5.3" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" - integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== deep-extend@^0.6.0: version "0.6.0" @@ -2344,11 +1871,6 @@ detect-libc@^2.0.0, detect-libc@^2.0.2: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== -detect-newline@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - dezalgo@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -2362,11 +1884,6 @@ diacritics@1.3.0: resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== -diff-sequences@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" - integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2465,23 +1982,6 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -ejs@^3.1.10: - version "3.1.10" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" - integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== - dependencies: - jake "^10.8.5" - -electron-to-chromium@^1.5.41: - version "1.5.66" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.66.tgz#1e9b4bc7638ac02d3551eea1dbaeb0101ec5823f" - integrity sha512-pI2QF6+i+zjPbqRzJwkMvtvkdI7MjVbSh2g8dlMguDJIXEPw+kwasS1Jl+YGPEBfGVxsVgGUratAKymPdPo2vQ== - -emittery@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" - integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2613,6 +2113,11 @@ es-iterator-helpers@^1.1.0: iterator.prototype "^1.1.3" safe-array-concat "^1.1.2" +es-module-lexer@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -2645,21 +2150,42 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.5" is-symbol "^1.0.4" -escalade@^3.1.1, escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== +esbuild@^0.25.0: + version "0.25.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.3.tgz#371f7cb41283e5b2191a96047a7a89562965a285" + integrity sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.3" + "@esbuild/android-arm" "0.25.3" + "@esbuild/android-arm64" "0.25.3" + "@esbuild/android-x64" "0.25.3" + "@esbuild/darwin-arm64" "0.25.3" + "@esbuild/darwin-x64" "0.25.3" + "@esbuild/freebsd-arm64" "0.25.3" + "@esbuild/freebsd-x64" "0.25.3" + "@esbuild/linux-arm" "0.25.3" + "@esbuild/linux-arm64" "0.25.3" + "@esbuild/linux-ia32" "0.25.3" + "@esbuild/linux-loong64" "0.25.3" + "@esbuild/linux-mips64el" "0.25.3" + "@esbuild/linux-ppc64" "0.25.3" + "@esbuild/linux-riscv64" "0.25.3" + "@esbuild/linux-s390x" "0.25.3" + "@esbuild/linux-x64" "0.25.3" + "@esbuild/netbsd-arm64" "0.25.3" + "@esbuild/netbsd-x64" "0.25.3" + "@esbuild/openbsd-arm64" "0.25.3" + "@esbuild/openbsd-x64" "0.25.3" + "@esbuild/sunos-x64" "0.25.3" + "@esbuild/win32-arm64" "0.25.3" + "@esbuild/win32-ia32" "0.25.3" + "@esbuild/win32-x64" "0.25.3" escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -2873,11 +2399,6 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - esquery@^1.4.2: version "1.6.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" @@ -2902,6 +2423,13 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -2917,41 +2445,15 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expect@^29.0.0, expect@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" - integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== - dependencies: - "@jest/expect-utils" "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" +expect-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.1.tgz#af76d8b357cf5fa76c41c09dafb79c549e75f71f" + integrity sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw== express@^4.18.2, express@^4.21.1: version "4.21.1" @@ -3021,7 +2523,7 @@ fast-json-parse@^1.0.3: resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" integrity sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw== -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3060,12 +2562,10 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" +fdir@^6.4.3, fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== file-entry-cache@^6.0.1: version "6.0.1" @@ -3074,13 +2574,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -filelist@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" - integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== - dependencies: - minimatch "^5.0.1" - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -3117,7 +2610,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.0.0, find-up@^4.1.0: +find-up@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -3228,7 +2721,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3271,16 +2764,6 @@ gcp-metadata@^5.3.0: gaxios "^5.0.0" json-bigint "^1.0.0" -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" @@ -3292,21 +2775,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - get-stdin@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -3347,7 +2820,7 @@ glob@^10.2.2: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -3359,11 +2832,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -3420,7 +2888,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.15, graceful-fs@^4.2.9: +graceful-fs@^4.1.15: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3520,11 +2988,6 @@ hexoid@^1.0.0: resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - htmlparser2@^8.0.0: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" @@ -3571,11 +3034,6 @@ https-proxy-agent@^7.0.5: agent-base "^7.0.2" debug "4" -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - husky@^8.0.1: version "8.0.3" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" @@ -3618,14 +3076,6 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-local@^3.0.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" - integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -3754,11 +3204,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - is-generator-function@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" @@ -3885,68 +3330,15 @@ is-weakset@^2.0.3: call-bind "^1.0.7" get-intrinsic "^1.2.4" -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" - integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== - -istanbul-lib-instrument@^5.0.4: - version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== - dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.2.0" - semver "^6.3.0" - -istanbul-lib-instrument@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" - integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== - dependencies: - "@babel/core" "^7.23.9" - "@babel/parser" "^7.23.9" - "@istanbuljs/schema" "^0.1.3" - istanbul-lib-coverage "^3.2.0" - semver "^7.5.4" - -istanbul-lib-report@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" - integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^4.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== -istanbul-reports@^3.1.3: - version "3.1.7" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" - integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== iterator.prototype@^1.1.3: version "1.1.3" @@ -3968,382 +3360,6 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jake@^10.8.5: - version "10.9.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" - integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== - dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.4" - minimatch "^3.1.2" - -jest-changed-files@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" - integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== - dependencies: - execa "^5.0.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - -jest-circus@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" - integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^1.0.0" - is-generator-fn "^2.0.0" - jest-each "^29.7.0" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - pretty-format "^29.7.0" - pure-rand "^6.0.0" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-cli@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" - integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== - dependencies: - "@jest/core" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - chalk "^4.0.0" - create-jest "^29.7.0" - exit "^0.1.2" - import-local "^3.0.2" - jest-config "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - yargs "^17.3.1" - -jest-config@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" - integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== - dependencies: - "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.7.0" - "@jest/types" "^29.6.3" - babel-jest "^29.7.0" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-circus "^29.7.0" - jest-environment-node "^29.7.0" - jest-get-type "^29.6.3" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-runner "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - micromatch "^4.0.4" - parse-json "^5.2.0" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-json-comments "^3.1.1" - -jest-diff@^29.0.0, jest-diff@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" - integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== - dependencies: - chalk "^4.0.0" - diff-sequences "^29.6.3" - jest-get-type "^29.6.3" - pretty-format "^29.7.0" - -jest-docblock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" - integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== - dependencies: - detect-newline "^3.0.0" - -jest-each@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" - integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - jest-get-type "^29.6.3" - jest-util "^29.7.0" - pretty-format "^29.7.0" - -jest-environment-node@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" - integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - jest-util "^29.7.0" - -jest-extended@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-4.0.2.tgz#d23b52e687cedf66694e6b2d77f65e211e99e021" - integrity sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog== - dependencies: - jest-diff "^29.0.0" - jest-get-type "^29.0.0" - -jest-get-type@^29.0.0, jest-get-type@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" - integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== - -jest-haste-map@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" - integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== - dependencies: - "@jest/types" "^29.6.3" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - jest-worker "^29.7.0" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - -jest-leak-detector@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" - integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== - dependencies: - jest-get-type "^29.6.3" - pretty-format "^29.7.0" - -jest-matcher-utils@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" - integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== - dependencies: - chalk "^4.0.0" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - pretty-format "^29.7.0" - -jest-message-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" - integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.6.3" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-mock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" - integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-util "^29.7.0" - -jest-pnp-resolver@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== - -jest-regex-util@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" - integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== - -jest-resolve-dependencies@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" - integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== - dependencies: - jest-regex-util "^29.6.3" - jest-snapshot "^29.7.0" - -jest-resolve@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" - integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== - dependencies: - chalk "^4.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-pnp-resolver "^1.2.2" - jest-util "^29.7.0" - jest-validate "^29.7.0" - resolve "^1.20.0" - resolve.exports "^2.0.0" - slash "^3.0.0" - -jest-runner@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" - integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== - dependencies: - "@jest/console" "^29.7.0" - "@jest/environment" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.13.1" - graceful-fs "^4.2.9" - jest-docblock "^29.7.0" - jest-environment-node "^29.7.0" - jest-haste-map "^29.7.0" - jest-leak-detector "^29.7.0" - jest-message-util "^29.7.0" - jest-resolve "^29.7.0" - jest-runtime "^29.7.0" - jest-util "^29.7.0" - jest-watcher "^29.7.0" - jest-worker "^29.7.0" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runtime@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" - integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/globals" "^29.7.0" - "@jest/source-map" "^29.6.3" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-snapshot@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" - integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== - dependencies: - "@babel/core" "^7.11.6" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-jsx" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^29.7.0" - graceful-fs "^4.2.9" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - natural-compare "^1.4.0" - pretty-format "^29.7.0" - semver "^7.5.3" - -jest-util@^29.0.0, jest-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" - integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-validate@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" - integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== - dependencies: - "@jest/types" "^29.6.3" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^29.6.3" - leven "^3.1.0" - pretty-format "^29.7.0" - -jest-watcher@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" - integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== - dependencies: - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.13.1" - jest-util "^29.7.0" - string-length "^4.0.1" - -jest-worker@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" - integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== - dependencies: - "@types/node" "*" - jest-util "^29.7.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" - integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== - dependencies: - "@jest/core" "^29.7.0" - "@jest/types" "^29.6.3" - import-local "^3.0.2" - jest-cli "^29.7.0" - jose@^2.0.6: version "2.0.7" resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.7.tgz#3aabbaec70bff313c108b9406498a163737b16ba" @@ -4356,19 +3372,11 @@ jose@^4.14.6: resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -4381,11 +3389,6 @@ jsbn@1.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== -jsesc@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - json-bigint@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" @@ -4403,11 +3406,6 @@ json-parse-better-errors@^1.0.1: resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -4430,11 +3428,6 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -4547,16 +3540,6 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -4570,11 +3553,6 @@ limiter@^1.1.5: resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - load-json-file@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3" @@ -4665,11 +3643,6 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -4712,6 +3685,11 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +loupe@^3.1.0, loupe@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" + integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== + lower-case-first@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" @@ -4736,13 +3714,6 @@ lru-cache@^10.0.0, lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - lru-cache@^7.10.1, lru-cache@^7.14.1: version "7.18.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" @@ -4756,6 +3727,13 @@ lru-memoizer@^2.1.4, lru-memoizer@^2.2.0: lodash.clonedeep "^4.5.0" lru-cache "6.0.0" +magic-string@^0.30.17: + version "0.30.17" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4763,25 +3741,6 @@ make-dir@^3.0.2: dependencies: semver "^6.0.0" -make-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" - integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== - dependencies: - semver "^7.5.3" - -make-error@^1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -makeerror@1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" - integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== - dependencies: - tmpl "1.0.5" - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -4797,11 +3756,6 @@ merge-descriptors@1.0.3: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -4852,30 +3806,18 @@ mime@^3.0.0: resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" @@ -5012,10 +3954,10 @@ nanoclone@^0.2.1: resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== -nanoid@^3.3.7: - version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== +nanoid@^3.3.7, nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== napi-build-utils@^1.0.1: version "1.0.2" @@ -5087,28 +4029,6 @@ node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== - -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5195,13 +4115,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -5221,7 +4134,7 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.1, p-limit@^3.0.2, p-limit@^3.1.0: +p-limit@^3.0.1, p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -5295,16 +4208,6 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - parse-srcset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" @@ -5350,7 +4253,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -5378,21 +4281,36 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -5459,11 +4377,6 @@ pino@^9.5.0: sonic-boom "^4.0.1" thread-stream "^3.0.0" -pirates@^4.0.4: - version "4.0.6" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - pkg-conf@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-3.1.0.tgz#d9f9c75ea1bae0e77938cde045b276dac7cc69ae" @@ -5480,7 +4393,7 @@ pkg-conf@^4.0.0: find-up "^6.0.0" load-json-file "^7.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -5506,6 +4419,15 @@ postcss@^8.3.11: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.3: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prebuild-install@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" @@ -5529,15 +4451,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -pretty-format@^29.0.0, pretty-format@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - process-warning@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" @@ -5548,14 +4461,6 @@ process-warning@^4.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.0.tgz#581e3a7a1fb456c5f4fd239f76bce75897682d5a" integrity sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw== -prompts@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -5615,11 +4520,6 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -pure-rand@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" - integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== - qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -5691,11 +4591,6 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - "readable-stream@2 || 3", readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -5743,34 +4638,12 @@ regexpp@^3.0.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve.exports@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" - integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== - -resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.4: +resolve@^1.22.1, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -5828,6 +4701,35 @@ robust-predicates@^2.0.4: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-2.0.4.tgz#0a2367a93abd99676d075981707f29cfb402248b" integrity sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg== +rollup@^4.34.9: + version "4.40.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.0.tgz#13742a615f423ccba457554f006873d5a4de1920" + integrity sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w== + dependencies: + "@types/estree" "1.0.7" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.40.0" + "@rollup/rollup-android-arm64" "4.40.0" + "@rollup/rollup-darwin-arm64" "4.40.0" + "@rollup/rollup-darwin-x64" "4.40.0" + "@rollup/rollup-freebsd-arm64" "4.40.0" + "@rollup/rollup-freebsd-x64" "4.40.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.40.0" + "@rollup/rollup-linux-arm-musleabihf" "4.40.0" + "@rollup/rollup-linux-arm64-gnu" "4.40.0" + "@rollup/rollup-linux-arm64-musl" "4.40.0" + "@rollup/rollup-linux-loongarch64-gnu" "4.40.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-musl" "4.40.0" + "@rollup/rollup-linux-s390x-gnu" "4.40.0" + "@rollup/rollup-linux-x64-gnu" "4.40.0" + "@rollup/rollup-linux-x64-musl" "4.40.0" + "@rollup/rollup-win32-arm64-msvc" "4.40.0" + "@rollup/rollup-win32-ia32-msvc" "4.40.0" + "@rollup/rollup-win32-x64-msvc" "4.40.0" + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -5886,12 +4788,12 @@ semver@^5.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: +semver@^7.0.0, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4, semver@^7.6.3: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -6008,10 +4910,10 @@ sift@16.0.1: resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== -signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== signal-exit@^4.0.1: version "4.1.0" @@ -6039,11 +4941,6 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -6089,19 +4986,6 @@ source-map-js@^1.2.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== -source-map-support@0.5.13: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - sparse-bitfield@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" @@ -6126,17 +5010,10 @@ sprintf-js@^1.1.3: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -stack-utils@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" - integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== - dependencies: - escape-string-regexp "^2.0.0" +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== standard-engine@^15.0.0: version "15.1.0" @@ -6153,6 +5030,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +std-env@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" + integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== + stream-events@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" @@ -6176,14 +5058,6 @@ streamx@^2.15.0, streamx@^2.20.0: optionalDependencies: bare-events "^2.2.0" -string-length@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -6193,7 +5067,7 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6298,16 +5172,6 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -6376,13 +5240,6 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -6448,15 +5305,6 @@ teeny-request@^8.0.0: stream-events "^1.0.5" uuid "^9.0.0" -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - text-decoder@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.1.tgz#e173f5121d97bfa3ff8723429ad5ba92e1ead67e" @@ -6487,11 +5335,44 @@ tiny-case@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinyglobby@^0.2.12, tinyglobby@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + +tinypool@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + tinyqueue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + title-case@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/title-case/-/title-case-1.1.2.tgz#fae4a6ae546bfa22d083a0eea910a40d12ed4f5a" @@ -6500,11 +5381,6 @@ title-case@^1.1.0: sentence-case "^1.1.1" upper-case "^1.0.3" -tmpl@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" - integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -6541,21 +5417,6 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -ts-jest@^29.2.5: - version "29.2.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63" - integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA== - dependencies: - bs-logger "^0.2.6" - ejs "^3.1.10" - fast-json-stable-stringify "^2.1.0" - jest-util "^29.0.0" - json5 "^2.2.3" - lodash.memoize "^4.1.2" - make-error "^1.3.6" - semver "^7.6.3" - yargs-parser "^21.1.1" - ts-standard@^12.0.0: version "12.0.2" resolved "https://registry.yarnpkg.com/ts-standard/-/ts-standard-12.0.2.tgz#883db655106f9bde374348fc81c89e8a30414e1b" @@ -6620,21 +5481,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - type-fest@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -6741,14 +5592,6 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" - integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.0" - upper-case-first@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" @@ -6800,15 +5643,6 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -v8-to-istanbul@^9.0.1: - version "9.3.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" - integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== - dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^2.0.0" - value-or-promise@1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140" @@ -6824,17 +5658,57 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -wait-for-expect@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.2.tgz#d2f14b2f7b778c9b82144109c8fa89ceaadaa463" - integrity sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag== +vite-node@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.1.2.tgz#b17869a12307f5260b20ba4b58cf493afee70aa7" + integrity sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA== + dependencies: + cac "^6.7.14" + debug "^4.4.0" + es-module-lexer "^1.6.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0" + +"vite@^5.0.0 || ^6.0.0": + version "6.3.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.2.tgz#4c1bb01b1cea853686a191657bbc14272a038f0a" + integrity sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.3" + picomatch "^4.0.2" + postcss "^8.5.3" + rollup "^4.34.9" + tinyglobby "^0.2.12" + optionalDependencies: + fsevents "~2.3.3" -walker@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" - integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== - dependencies: - makeerror "1.0.12" +vitest@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.1.2.tgz#63afc16b6da3bea6e39f5387d80719e70634ba66" + integrity sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ== + dependencies: + "@vitest/expect" "3.1.2" + "@vitest/mocker" "3.1.2" + "@vitest/pretty-format" "^3.1.2" + "@vitest/runner" "3.1.2" + "@vitest/snapshot" "3.1.2" + "@vitest/spy" "3.1.2" + "@vitest/utils" "3.1.2" + chai "^5.2.0" + debug "^4.4.0" + expect-type "^1.2.1" + magic-string "^0.30.17" + pathe "^2.0.3" + std-env "^3.9.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinyglobby "^0.2.13" + tinypool "^1.0.2" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0" + vite-node "3.1.2" + why-is-node-running "^2.3.0" webidl-conversions@^3.0.0: version "3.0.1" @@ -6933,6 +5807,14 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" @@ -6947,15 +5829,6 @@ word-wrap@^1.2.5: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -6970,52 +5843,16 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^3.0.7" - xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.3.1: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yauzl@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-3.2.0.tgz#7b6cb548f09a48a6177ea0be8ece48deb7da45c0"