From c68ada1e479a3276422f7e056b8378914207d6a4 Mon Sep 17 00:00:00 2001 From: Viet Nguyen <3805254+vnugent@users.noreply.github.com> Date: Tue, 9 May 2023 02:28:04 +0200 Subject: [PATCH] Migrate photo storage to GCloud storage (#273) * chore: extract & save image width/height during migration * chore: resolve username in the backend instead of frontend * chore: return image metadata to tag queries * chore: climb/area/user profile media share the same type * refactor: embed tags in media object collection * refactor: update find-tags-by-area-id to work with embedded tags * refactor: move leaderboard api to new embedded tags model --- .env | 1 + .vscode/launch.json | 31 +- package.json | 6 + src/db/AreaTypes.ts | 7 +- src/db/MediaObjectSchema.ts | 54 ++ src/db/MediaObjectTypes.ts | 58 ++ src/db/MediaSchema.ts | 4 + src/db/MediaTypes.ts | 35 +- src/db/XMediaSchema.ts | 5 + src/db/index.ts | 5 +- src/db/utils/index.ts | 8 + .../migration/CreateMediaMetaCollection.ts | 77 +++ .../migration/MigrateTagsToMediaCollection.ts | 150 +++++ src/db/utils/jobs/migration/SirvClient.ts | 78 +++ src/db/utils/jobs/migration/Tests.ts | 92 +++ src/graphql/history/HistoryFieldResolvers.ts | 15 +- src/graphql/media/MediaResolvers.ts | 49 +- src/graphql/media/queries.ts | 20 +- src/graphql/resolvers.ts | 82 +-- src/graphql/schema/Area.gql | 8 +- src/graphql/schema/Climb.gql | 8 +- src/graphql/schema/History.gql | 13 +- src/graphql/schema/Media.gql | 119 ++-- src/model/AreaDataSource.ts | 110 +--- src/model/MediaDataSource.ts | 188 ++++-- src/model/__tests__/MediaDataSource.ts | 13 +- src/types.ts | 7 + src/utils/helpers.ts | 46 +- yarn.lock | 563 +++++++++++++++++- 29 files changed, 1527 insertions(+), 325 deletions(-) create mode 100644 src/db/MediaObjectSchema.ts create mode 100644 src/db/MediaObjectTypes.ts create mode 100644 src/db/utils/index.ts create mode 100644 src/db/utils/jobs/migration/CreateMediaMetaCollection.ts create mode 100644 src/db/utils/jobs/migration/MigrateTagsToMediaCollection.ts create mode 100644 src/db/utils/jobs/migration/SirvClient.ts create mode 100644 src/db/utils/jobs/migration/Tests.ts diff --git a/.env b/.env index ce27ebd7..217c1146 100644 --- a/.env +++ b/.env @@ -9,6 +9,7 @@ MONGO_READ_PREFERENCE=primary MONGO_REPLICA_SET_NAME=rs0 CONTENT_BASEDIR=./tmp DEPLOYMENT_ENV=development +CDN_URL=https://storage.googleapis.com/openbeta-staging # Typesense TYPESENSE_NODE=4wknoyspjq6l7c9fp-1.a1.typesense.net diff --git a/.vscode/launch.json b/.vscode/launch.json index ce7d65ac..8201d40f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -54,7 +54,8 @@ "preLaunchTask": "tsc: build - tsconfig.json", "outFiles": [ "${workspaceFolder}/build/**/*.js" - ] + ], + "console": "integratedTerminal", }, { "name": "Debug Jest Tests", @@ -71,6 +72,32 @@ ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - } + }, + { + "type": "node", + "request": "launch", + "name": "Create MediaObjects collection", + "program": "${workspaceFolder}/src/db/utils/jobs/migration/CreateMediaMetaCollection.ts", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": [ + "${workspaceFolder}/build/**/*.js" + ], + "skipFiles": [ + "/**" + ], + }, + { + "type": "node", + "request": "launch", + "name": "Migrate old tags to MediaObjects collection", + "program": "${workspaceFolder}/src/db/utils/jobs/migration/MigrateTagsToMediaCollection.ts", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": [ + "${workspaceFolder}/build/**/*.js" + ], + "skipFiles": [ + "/**" + ], + }, ] } \ No newline at end of file diff --git a/package.json b/package.json index bf666977..be3f992f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@babel/runtime": "^7.17.2", + "@google-cloud/storage": "^6.9.5", "@graphql-tools/schema": "^8.3.1", "@openbeta/sandbag": "^0.0.37", "@turf/area": "^6.5.0", @@ -34,9 +35,12 @@ "@types/uuid": "^8.3.3", "apollo-datasource-mongodb": "^0.5.4", "apollo-server": "^3.9.0", + "axios": "^1.3.6", "cors": "^2.8.5", "dot-object": "^2.1.4", "dotenv": "^10.0.0", + "expiry-map": "^2.0.0", + "glob": "^10.2.2", "graphql": "^16.5.0", "graphql-middleware": "^6.1.31", "graphql-shield": "^7.5.0", @@ -48,8 +52,10 @@ "mongoose-lean-virtuals": "0.9.1", "node-fetch": "2", "p-limit": "^4.0.0", + "p-memoize": "^7.1.1", "pino": "^8.2.0", "sanitize-html": "^2.7.2", + "sharp": "^0.32.0", "typesense": "^1.2.1", "underscore": "^1.13.2", "uuid": "^8.3.2", diff --git a/src/db/AreaTypes.ts b/src/db/AreaTypes.ts index 141114c9..37bb9240 100644 --- a/src/db/AreaTypes.ts +++ b/src/db/AreaTypes.ts @@ -6,6 +6,7 @@ import { ClimbType } from './ClimbTypes.js' import { ChangeRecordMetadataType } from './ChangeLogType.js' import { GradeContexts } from '../GradeUtils.js' import { ExperimentalAuthorType } from './UserTypes.js' +import { AuthorMetadata } from '../types.js' /** * Areas are a grouping mechanism in the OpenBeta data model that allow @@ -32,7 +33,7 @@ export type AreaType = IAreaProps & { * See AreaType for the reified version of this object, and always use it * if you are working with data that exists inside the database. */ -export interface IAreaProps { +export interface IAreaProps extends AuthorMetadata { _id: mongoose.Types.ObjectId /** * ShortCodes are short, globally uniqe codes that identify significant climbing areas @@ -99,10 +100,6 @@ export interface IAreaProps { _change?: ChangeRecordMetadataType /** Used to delete an area. See https://www.mongodb.com/docs/manual/core/index-ttl/ */ _deleting?: Date - createdAt?: Date - updatedAt?: Date - updatedBy?: MUUID - createdBy?: MUUID } export interface IAreaMetadata { diff --git a/src/db/MediaObjectSchema.ts b/src/db/MediaObjectSchema.ts new file mode 100644 index 00000000..3795f3df --- /dev/null +++ b/src/db/MediaObjectSchema.ts @@ -0,0 +1,54 @@ +import mongoose from 'mongoose' + +import { MediaObject, EntityTag } from './MediaObjectTypes.js' +import { PointSchema } from './ClimbSchema.js' + +const { Schema } = mongoose + +const UUID_TYPE = { + type: 'object', value: { type: 'Buffer' } +} + +const EntitySchema = new Schema({ + targetId: { ...UUID_TYPE, index: true }, + climbName: { type: Schema.Types.String }, + areaName: { type: Schema.Types.String, required: true }, + type: { type: Schema.Types.Number, required: true }, + ancestors: { type: Schema.Types.String, required: true, index: true }, + lnglat: { + type: PointSchema, + index: '2dsphere' + } +}, { _id: true }) + +const schema = new Schema({ + userUuid: { ...UUID_TYPE, index: true }, + mediaUrl: { type: Schema.Types.String, unique: true, index: true }, + width: { type: Schema.Types.Number, required: true }, + height: { type: Schema.Types.Number, required: true }, + size: { type: Schema.Types.Number, required: true }, + format: { type: Schema.Types.String, required: true }, + entityTags: [EntitySchema] +}, { _id: true, timestamps: true }) + +/** + * Additional indices + */ +schema.index({ + /** + * For filtering media objects with/without tags + */ + entityTags: 1, + /** + * For sorting media objects by insertion order + */ + createdAt: -1 // ascending, more recent first +}) + +/** + * Get media object model + * @returns MediaObjectType + */ +export const getMediaObjectModel = (): mongoose.Model => { + return mongoose.model('media_objects', schema) +} diff --git a/src/db/MediaObjectTypes.ts b/src/db/MediaObjectTypes.ts new file mode 100644 index 00000000..92981ec6 --- /dev/null +++ b/src/db/MediaObjectTypes.ts @@ -0,0 +1,58 @@ +import { ObjectId } from 'mongoose' +import { MUUID } from 'uuid-mongodb' +import { Point } from '@turf/helpers' + +export type ImageFormatType = 'jpeg' | 'png' | 'webp' | 'avif' + +export interface MediaObject { + _id: ObjectId + userUuid: MUUID + mediaUrl: string + width: number + height: number + format: ImageFormatType + createdAt: Date + size: number + entityTags: EntityTag[] +} + +export interface EntityTag { + _id: ObjectId + targetId: MUUID + type: number + ancestors: string + climbName?: string + areaName: string + lnglat: Point +} + +export interface MediaByUsers { + username: string + userUuid: MUUID + mediaWithTags: MediaObject[] +} +export interface MediaForFeedInput { + uuidStr?: string + maxUsers?: number + maxFiles?: number + includesNoEntityTags?: boolean +} + +export interface TagByUser { + username?: string + userUuid: MUUID + total: number +} + +export interface AllTimeTagStats { + totalMediaWithTags: number + byUsers: TagByUser[] +} +export interface TagsLeaderboardType { + allTime: AllTimeTagStats +} + +export interface UserMediaQueryInput { + userUuid: string + maxFiles?: number +} diff --git a/src/db/MediaSchema.ts b/src/db/MediaSchema.ts index fc9af31d..28ac5104 100644 --- a/src/db/MediaSchema.ts +++ b/src/db/MediaSchema.ts @@ -55,6 +55,10 @@ MediaSchema.virtual('area', { MediaSchema.plugin(mongooseLeanVirtuals) MediaSchema.index({ mediaUuid: 1, destinationId: 1 }, { unique: true }) +/** + * @deprecated Superseded by MediaObjectSchema + * @param name + */ export const getMediaModel = (name: string = 'media'): mongoose.Model => { return mongoose.model(name, MediaSchema) } diff --git a/src/db/MediaTypes.ts b/src/db/MediaTypes.ts index d9592ad1..2fb2e9f3 100644 --- a/src/db/MediaTypes.ts +++ b/src/db/MediaTypes.ts @@ -3,8 +3,11 @@ import { MUUID } from 'uuid-mongodb' import { AreaType } from './AreaTypes.js' import { ClimbType } from './ClimbTypes.js' +import { MediaObject } from './MediaObjectTypes.js' -// Type for 'Media' collection schema +/** + * @deprecated to be removed in favor of MediaObject type + */ export interface MediaType { _id?: mongoose.Types.ObjectId mediaUuid: MUUID @@ -20,11 +23,21 @@ export enum RefModelType { areas = 'areas' } -export interface MediaListByAuthorType { - _id: string - tagList: MediaType[] +/** + * A tag with media metadata + */ +export type BaseTagType = MediaType & MediaObject + +export interface CompleteAreaTag extends BaseTagType { + area: AreaType +} + +export interface CompleteClimbTag extends BaseTagType { + climb: ClimbType } +export type TagType = CompleteAreaTag | CompleteClimbTag + export interface MediaInputType { mediaUuid: MUUID mediaUrl: string @@ -33,7 +46,10 @@ export interface MediaInputType { destType: number } -interface BaseTagType { +/** + * TODO: consolidate this type with BaseTagType + */ +interface LegacyBaseTagType { _id: mongoose.Types.ObjectId mediaUuid: MUUID mediaUrl: string @@ -42,11 +58,11 @@ interface BaseTagType { onModel: RefModelType } -export interface AreaTagType extends BaseTagType { +export interface AreaTagType extends LegacyBaseTagType { area: AreaType } -export interface ClimbTagType extends BaseTagType { +export interface ClimbTagType extends LegacyBaseTagType { climb: ClimbType } @@ -58,8 +74,3 @@ export interface DeleteTagResult { destType: number destinationId: string } - -export interface TagsLeaderboardType { - userUuid: string - total: number -} diff --git a/src/db/XMediaSchema.ts b/src/db/XMediaSchema.ts index 8cf3ae70..92617ad9 100644 --- a/src/db/XMediaSchema.ts +++ b/src/db/XMediaSchema.ts @@ -38,6 +38,11 @@ export const XMediaSchema = new Schema({ XMediaSchema.plugin(mongooseLeanVirtuals) +/** + * @deprecated Superceded by MediaObjects + * @param name + * @returns + */ export const getXMediaModel = (name: string = 'xmedia'): mongoose.Model => { return mongoose.model(name, XMediaSchema) } diff --git a/src/db/index.ts b/src/db/index.ts index 739216ac..dfba13f2 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -6,6 +6,7 @@ import { enableAllPlugins } from 'immer' import { getAreaModel } from './AreaSchema.js' import { getClimbModel } from './ClimbSchema.js' import { getMediaModel } from './MediaSchema.js' +import { getMediaObjectModel } from './MediaObjectSchema.js' import { getOrganizationModel } from './OrganizationSchema.js' import { getTickModel } from './TickSchema.js' import { getXMediaModel } from './XMediaSchema.js' @@ -71,6 +72,7 @@ export const createIndexes = async (): Promise => { await getTickModel().createIndexes() await getXMediaModel().createIndexes() await getPostModel().createIndexes() + await getMediaObjectModel().createIndexes() } export const gracefulExit = async (exitCode: number = 0): Promise => { @@ -98,5 +100,6 @@ export { getChangeLogModel, getXMediaModel, getPostModel, - getExperimentalUserModel + getExperimentalUserModel, + getMediaObjectModel } diff --git a/src/db/utils/index.ts b/src/db/utils/index.ts new file mode 100644 index 00000000..e96554a9 --- /dev/null +++ b/src/db/utils/index.ts @@ -0,0 +1,8 @@ +import { AuthorMetadata } from '../../types' + +export const getAuthorMetadataFromBaseNode = ({ updatedAt, updatedBy, createdAt, createdBy }: AuthorMetadata): AuthorMetadata => ({ + updatedAt, + updatedBy, + createdAt, + createdBy +}) diff --git a/src/db/utils/jobs/migration/CreateMediaMetaCollection.ts b/src/db/utils/jobs/migration/CreateMediaMetaCollection.ts new file mode 100644 index 00000000..abc19342 --- /dev/null +++ b/src/db/utils/jobs/migration/CreateMediaMetaCollection.ts @@ -0,0 +1,77 @@ +import sharp from 'sharp' +import { glob } from 'glob' +import { validate as uuidValidate } from 'uuid' +import muuid from 'uuid-mongodb' + +import { connectDB, gracefulExit } from '../../../index.js' +import { logger } from '../../../../logger.js' +import { MediaObject } from '../../../MediaObjectTypes.js' +import { getMediaObjectModel } from '../../../MediaObjectSchema.js' +import { getFileInfo } from './SirvClient.js' + +const LOCAL_MEDIA_DIR = process.env.LOCAL_MEDIA_DIR + +if (LOCAL_MEDIA_DIR == null) { + throw new Error('LOCAL_MEDIA_DIR env not defined') +} + +/** + * Build the media object collection from media files on disk + */ +const onConnected = async (): Promise => { + logger.info('Creating photo collection') + const model = getMediaObjectModel() + await model.ensureIndexes() + const images = await glob(LOCAL_MEDIA_DIR, { + nodir: true, + stat: true, + withFileTypes: true + }) + + let list: any[] = [] + let count = 0 + for (const image of images) { + const { width, height, format } = await sharp(image.fullpath()).metadata() + if (width == null || height == null || image.size == null) continue + if ((format !== 'avif' && format !== 'jpeg' && format !== 'png' && format !== 'webp')) { + logger.warn({ format, file: image.name }, 'Unexpected media format') + continue + } + + const folderUuidStr = image.parent?.name ?? '' + if (!uuidValidate(folderUuidStr)) { + logger.error({ file: image.name, parent: folderUuidStr }, 'Error: expect folder name to have uuid format. Found ') + continue + } + const userUuid = muuid.from(folderUuidStr) + const mediaUrl = `/u/${folderUuidStr}/${image.name}` + const { btime } = await getFileInfo(mediaUrl) + const meta: Omit = { + userUuid, + mediaUrl, + size: image.size, + width, + height, + format, + entityTags: [], + createdAt: btime + } + list.push(meta) + count = count + 1 + + if (list.length === 20) { + await model.insertMany(list) + list = [] + } + } + + if (list.length > 0) { + await model.insertMany(list) + } + + logger.info({ count }, 'Finish') + + await gracefulExit() +} + +void connectDB(onConnected) diff --git a/src/db/utils/jobs/migration/MigrateTagsToMediaCollection.ts b/src/db/utils/jobs/migration/MigrateTagsToMediaCollection.ts new file mode 100644 index 00000000..96f46f93 --- /dev/null +++ b/src/db/utils/jobs/migration/MigrateTagsToMediaCollection.ts @@ -0,0 +1,150 @@ +import { connectDB, getMediaModel, gracefulExit } from '../../../index.js' +import { logger } from '../../../../logger.js' +import { getMediaObjectModel } from '../../../MediaObjectSchema.js' +import { EntityTag } from '../../../MediaObjectTypes.js' + +/** + * Move tags in Media collection to embedded tags in the new Media Objects collection. + */ +const onConnected = async (): Promise => { + logger.info('Migrating tags...') + const mediaObjectModel = getMediaObjectModel() + const oldTagModel = getMediaModel() + + /** + * Initialize entityTags to [] + */ + await mediaObjectModel.updateMany({}, { + $set: { + entityTags: [] + } + }) + + let count = 0 + + const taggedClimbsPipeline = [ + { + $lookup: { + from: 'climbs', // other collection name + foreignField: '_id', // climb._id + localField: 'destinationId', + as: 'taggedClimb', + pipeline: [{ + $lookup: { // also allow ancestor areas to inherent climb photo + from: 'areas', // other collection name + foreignField: 'metadata.area_id', + localField: 'metadata.areaRef', // climb.metadata.areaRef + as: 'area' + } + }, + { + $unwind: '$area' + } + ] + } + }, + { + $unwind: { + path: '$taggedClimb', + preserveNullAndEmptyArrays: true + } + } + ] + + const taggedAreasPipeline = [ + { + $lookup: { + from: 'areas', // other collection name + foreignField: 'metadata.area_id', + localField: 'destinationId', + as: 'taggedArea' + } + }, + { + $unwind: { + path: '$taggedArea', + preserveNullAndEmptyArrays: true + } + } + ] + + await oldTagModel.aggregate([ + ...taggedClimbsPipeline, + ...taggedAreasPipeline, + { + /** + * Only include documents with either climb or area tags + */ + $match: { + $or: [ + { + taggedClimb: { + $exists: true + } + }, + { + taggedArea: { + $exists: true + } + } + ] + } + }, + { + $group: { + _id: { + destType: '$destType', + mediaUrl: '$mediaUrl', + targetId: '$destinationId' + }, + taggedAreas: { $push: '$taggedArea' }, + taggedClimbs: { $push: '$taggedClimb' } + } + } + ]).cursor().eachAsync(async doc => { + const mediaUrl: string = doc._id.mediaUrl + + let d: EntityTag[] = [] + switch (doc._id.destType) { + case 0: { + console.log('#Add climb tags') + d = doc.taggedClimbs.map((tag) => ({ + targetId: tag._id, + climbName: tag.name, + areaName: tag.area.area_name, + ancestors: tag.area.ancestors, + type: 0, + lnglat: tag.metadata.lnglat + })) + + break + } + case 1: { + console.log('#Add area tags') + + d = doc.taggedAreas.map(tag => ({ + targetId: tag.metadata.area_id, + areaName: tag.area_name, + ancestors: tag.ancestors, + type: 1, + lnglat: tag.metadata.lnglat + })) + + break + } + } + + if (d.length > 0) { + await mediaObjectModel.updateOne({ mediaUrl }, { + $addToSet: { entityTags: d } + }).lean() + + count = count + d.length + } + }) + + console.log('##count', count) + await gracefulExit() +} + +void connectDB(onConnected) diff --git a/src/db/utils/jobs/migration/SirvClient.ts b/src/db/utils/jobs/migration/SirvClient.ts new file mode 100644 index 00000000..e8dcd246 --- /dev/null +++ b/src/db/utils/jobs/migration/SirvClient.ts @@ -0,0 +1,78 @@ +import axios from 'axios' + +const SIRV_CONFIG = { + clientId: process.env.SIRV_CLIENT_ID_RO ?? null, + clientSecret: process.env.SIRV_CLIENT_SECRET_RO ?? null +} + +const client = axios.create({ + baseURL: 'https://api.sirv.com/v2', + headers: { + 'content-type': 'application/json' + } +}) + +const headers = { + 'content-type': 'application/json' +} + +interface TokenParamsType { + clientId: string | null + clientSecret: string | null +} + +const getToken = async (): Promise => { + const params: TokenParamsType = { + clientId: SIRV_CONFIG.clientId, + clientSecret: SIRV_CONFIG.clientSecret + } + + try { + const res = await client.post( + '/token', + params) + + if (res.status === 200) { + return res.data.token + } + } catch (e) { + console.error(e) + process.exit(1) + } + return null +} + +const token = await getToken() ?? '' + +interface FileMetadaata { + mtime: Date + btime: Date +} + +/** + * When downloading photos from Sirv using rclone or on the UI, + * the image file's upload time is lost. This function gets + * the original upload timestamp. + * @param filename + * @returns + */ +export const getFileInfo = async (filename: string): Promise => { + const res = await client.get( + '/files/stat?filename=' + encodeURIComponent(filename), + { + headers: { + ...headers, + Authorization: `bearer ${token}` + } + } + ) + + if (res.status === 200) { + const { ctime, mtime } = res.data + return ({ + btime: new Date(ctime), + mtime: new Date(mtime) + }) + } + throw new Error('Sirv API.getFileInfo() error' + res.statusText) +} diff --git a/src/db/utils/jobs/migration/Tests.ts b/src/db/utils/jobs/migration/Tests.ts new file mode 100644 index 00000000..d20a9b16 --- /dev/null +++ b/src/db/utils/jobs/migration/Tests.ts @@ -0,0 +1,92 @@ +import { connectDB, getMediaModel, gracefulExit } from '../../../index.js' +import { logger } from '../../../../logger.js' +import { getMediaObjectModel } from '../../../MediaObjectSchema.js' + +const knownIssuesFilter = { + $match: { + $and: [ + /** + * We don't support .heic files + */ + { mediaUrl: { $not: /heic$/i } }, + /** + * User folder was deleted from Sirv.com but this tag is still left behind + */ + { mediaUrl: { $ne: '/u/515b2003-9b53-46f8-ac6e-667718315c10/rCh6Fnbb6G.jpeg' } } + ] + } +} + +/** + * Verify existing tags are migrated to the new collection, MediaObjects. + */ +const onConnected = async (): Promise => { + logger.info('Verifying...') + const mediaObjectModel = getMediaObjectModel() + const oldTagModel = getMediaModel() + + const checkTagCounts = async (): Promise => { + const rs0 = await oldTagModel.aggregate<{ count: number }>([ + knownIssuesFilter, + { + $group: { + _id: '$onModel', + count: { $count: {} } + } + }, + { + $project: { + _id: 0, + tagType: '$_id', + count: 1 + } + } + ]) + + const rs1 = await mediaObjectModel.aggregate([ + { + $unwind: { + path: '$entityTags' + } + } + ]) + + const oldTagCount = rs0[0].count + rs0[1].count + const newTagCount = rs1.length + + logger.info({ oldTagCount, newTagCount, result: oldTagCount === newTagCount ? 'PASS' : 'FAIL' }, 'Old vs new tag count') + } + + /** + * Compare all tagged climbs and areas in the older collection + * to see if they're added to the new entityTags. + */ + const checkOrphaneIDs = async (): Promise => { + const rs = await oldTagModel.aggregate([ + knownIssuesFilter, + { + $lookup: { + from: mediaObjectModel.modelName, + foreignField: 'entityTags.targetId', + localField: 'destinationId', + as: 'matchingTags' + } + }, + { + $match: { + matchingTags: { + $size: 0 + } + } + } + ]) + + logger.info({ found: rs, result: rs.length === 0 ? 'PASS' : 'FAIL' }, 'Orphane IDs check') + } + await checkTagCounts() + await checkOrphaneIDs() + + await gracefulExit() +} + +void connectDB(onConnected) diff --git a/src/graphql/history/HistoryFieldResolvers.ts b/src/graphql/history/HistoryFieldResolvers.ts index f036c247..68eeb920 100644 --- a/src/graphql/history/HistoryFieldResolvers.ts +++ b/src/graphql/history/HistoryFieldResolvers.ts @@ -1,13 +1,15 @@ import { ChangeLogType, BaseChangeRecordType, SupportedCollectionTypes, DocumentKind } from '../../db/ChangeLogType.js' -import { exhaustiveCheck } from '../../utils/helpers.js' +import { AuthorMetadata } from '../../types.js' +import { exhaustiveCheck, getUserNickFromMediaDir } from '../../utils/helpers.js' /** - * Customize to resolve individual fields + * History schama field resolvers */ const resolvers = { History: { id: (node: ChangeLogType) => node._id.toString(), - editedBy: (node: ChangeLogType) => node.editedBy.toUUID().toString() + editedBy: (node: ChangeLogType) => node.editedBy.toUUID().toString(), + editedByUser: async (node: ChangeLogType) => (await getUserNickFromMediaDir(node.editedBy.toUUID().toString())) }, Change: { @@ -36,6 +38,13 @@ const resolvers = { return exhaustiveCheck(node.kind) } } + }, + + AuthorMetadata: { + createdBy: (node: AuthorMetadata) => node?.createdBy?.toUUID().toString(), + updatedBy: (node: AuthorMetadata) => node?.updatedBy?.toUUID().toString(), + createdByUser: async (node: AuthorMetadata) => await getUserNickFromMediaDir(node?.createdBy?.toUUID().toString() ?? ''), + updatedByUser: async (node: AuthorMetadata) => (await getUserNickFromMediaDir(node?.updatedBy?.toUUID().toString() ?? '')) } } diff --git a/src/graphql/media/MediaResolvers.ts b/src/graphql/media/MediaResolvers.ts index 5ff02d10..b25afb17 100644 --- a/src/graphql/media/MediaResolvers.ts +++ b/src/graphql/media/MediaResolvers.ts @@ -1,39 +1,38 @@ -import { AreaTagType, ClimbTagType, MediaListByAuthorType, MediaType, RefModelType, TagEntryResultType } from '../../db/MediaTypes.js' +import { EntityTag, MediaByUsers, MediaObject, TagByUser } from '../../db/MediaObjectTypes.js' +import { getUserNickFromMediaDir, geojsonPointToLatitude, geojsonPointToLongitude } from '../../utils/helpers.js' + const MediaResolvers = { - MediaTagType: { - mediaUuid: (node: MediaType) => node.mediaUuid.toUUID().toString(), - destination: (node: MediaType) => node.destinationId.toUUID().toString() - }, - TagEntryResult: { - __resolveType (obj: TagEntryResultType) { - if (obj.onModel === RefModelType.climbs) { - return 'ClimbTag' - } - if (obj.onModel === RefModelType.areas) { - return 'AreaTag' - } - return null - } + MediaByUsers: { + userUuid: (node: MediaByUsers) => node.userUuid.toUUID().toString(), + username: async (node: MediaByUsers) => ( + await getUserNickFromMediaDir(node.userUuid.toUUID().toString())) }, - DeleteTagResult: { - // nothing to override + MediaWithTags: { + id: (node: MediaObject) => node._id, + username: async (node: MediaObject) => ( + await getUserNickFromMediaDir(node.userUuid.toUUID().toString())), + uploadTime: (node: MediaObject) => node.createdAt }, - MediaListByAuthorType: { - authorUuid: (node: MediaListByAuthorType) => node._id + EntityTag: { + id: (node: EntityTag) => node._id, + targetId: (node: EntityTag) => node.targetId.toUUID().toString(), + lat: (node: EntityTag) => geojsonPointToLatitude(node.lnglat), + lng: (node: EntityTag) => geojsonPointToLongitude(node.lnglat) }, - ClimbTag: { - id: (node: ClimbTagType) => node._id.toString(), - mediaUuid: (node: ClimbTagType) => node.mediaUuid.toUUID().toString() + DeleteTagResult: { + // nothing to override }, - AreaTag: { - id: (node: AreaTagType) => node._id.toString(), - mediaUuid: (node: AreaTagType) => node.mediaUuid.toUUID().toString() + TagsByUser: { + userUuid: (node: TagByUser) => node.userUuid.toUUID().toString(), + username: async (node: TagByUser) => ( + await getUserNickFromMediaDir(node.userUuid.toUUID().toString())) } + } export default MediaResolvers diff --git a/src/graphql/media/queries.ts b/src/graphql/media/queries.ts index 3ec8369b..44329075 100644 --- a/src/graphql/media/queries.ts +++ b/src/graphql/media/queries.ts @@ -1,25 +1,21 @@ -import { MediaListByAuthorType, TagsLeaderboardType } from '../../db/MediaTypes.js' +import { TagsLeaderboardType, MediaObject, MediaByUsers, UserMediaQueryInput, MediaForFeedInput } from '../../db/MediaObjectTypes.js' import { DataSourcesType } from '../../types.js' const MediaQueries = { - /** - * Given a list of media IDs return all tags. - */ - getTagsByMediaIdList: async (_, { uuidList }: { uuidList: string[] }, { dataSources }) => { + getMediaForFeed: async (_, { input }, { dataSources }): Promise => { const { media }: DataSourcesType = dataSources - return await media.getTagsByMediaIds(uuidList) + const { maxUsers = 10, maxFiles = 20 } = input as MediaForFeedInput + return await media.getMediaByUsers({ maxUsers, maxFiles }) }, - /** - * Return most recent tags - */ - getRecentTags: async (_, { userLimit = 10 }: { userLimit: number | undefined }, { dataSources }): Promise => { + getUserMedia: async (_: any, { input }, { dataSources }): Promise => { const { media }: DataSourcesType = dataSources - return await media.getRecentTags(userLimit) + const { userUuid, maxFiles = 1000 } = input as UserMediaQueryInput + return await media.getOneUserMedia(userUuid, maxFiles) }, - getTagsLeaderboard: async (_, { limit = 30 }: { limit: number }, { dataSources }): Promise => { + getTagsLeaderboard: async (_, { limit = 30 }: { limit: number }, { dataSources }): Promise => { const { media }: DataSourcesType = dataSources return await media.getTagsLeaderboard(limit) } diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 3b2d09bf..b3b3b1ff 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,6 +1,9 @@ import { makeExecutableSchema } from '@graphql-tools/schema' import { DataSources } from 'apollo-server-core/dist/graphqlOptions' import muid from 'uuid-mongodb' +import fs from 'fs' +import { gql } from 'apollo-server' +import { DocumentNode } from 'graphql' import { CommonResolvers, CommonTypeDef } from './common/index.js' import { HistoryQueries, HistoryFieldResolvers } from '../graphql/history/index.js' @@ -17,10 +20,9 @@ import { ClimbMutations } from './climb/index.js' import { OrganizationMutations, OrganizationQueries } from './organization/index.js' import TickMutations from './tick/TickMutations.js' import TickQueries from './tick/TickQueries.js' -import fs from 'fs' - -import { gql } from 'apollo-server' -import { DocumentNode } from 'graphql' +import MediaDataSource from '../model/MediaDataSource.js' +import { getAuthorMetadataFromBaseNode } from '../db/utils/index.js' +import { geojsonPointToLatitude, geojsonPointToLongitude } from '../utils/helpers.js' /** * It takes a file name as an argument, reads the file, and returns a GraphQL DocumentNode. @@ -152,21 +154,27 @@ const resolvers = { grades: (node: ClimbGQLQueryType) => node.grades ?? null, - metadata: (node: ClimbGQLQueryType) => ({ - ...node.metadata, - leftRightIndex: node.metadata.left_right_index, - climb_id: node._id.toUUID().toString(), - climbId: node._id.toUUID().toString(), + metadata: (node: ClimbGQLQueryType) => { + const { metadata } = node // convert internal Geo type to simple lng,lat - lng: node.metadata.lnglat.coordinates[0], - lat: node.metadata.lnglat.coordinates[1] - }), + const lng = geojsonPointToLongitude(metadata.lnglat) + const lat = geojsonPointToLatitude(metadata.lnglat) + const climbId = node._id.toUUID().toString() + return ({ + ...node.metadata, + leftRightIndex: metadata.left_right_index, + climb_id: climbId, + climbId, + lng, + lat + }) + }, ancestors: (node: ClimbGQLQueryType) => node.ancestors.split(','), - media: async (node: any, args: any, { dataSources }) => { - const { areas }: { areas: AreaDataSource } = dataSources - return await areas.findMediaByClimbId(node._id) + media: async (node: ClimbType, args: any, { dataSources }) => { + const { media }: { media: MediaDataSource } = dataSources + return await media.findMediaByClimbId(node._id, node.name) }, content: (node: ClimbGQLQueryType) => node.content == null @@ -177,8 +185,7 @@ const resolvers = { } : node.content, - createdBy: (node: ClimbGQLQueryType) => node?.createdBy?.toUUID().toString(), - updatedBy: (node: ClimbGQLQueryType) => node?.updatedBy?.toUUID().toString() + authorMetadata: getAuthorMetadataFromBaseNode }, Area: { @@ -219,26 +226,32 @@ const resolvers = { return areas.findManyClimbsByUuids(node.climbs) }, - metadata: (node: AreaType) => ({ - ...node.metadata, - isDestination: node.metadata?.isDestination ?? false, - isBoulder: node.metadata?.isBoulder ?? false, - leftRightIndex: node.metadata?.leftRightIndex ?? -1, - area_id: node.metadata.area_id.toUUID().toString(), - areaId: node.metadata.area_id.toUUID().toString(), + metadata: (node: AreaType) => { + const { metadata } = node // convert internal Geo type to simple lng,lat - lng: node.metadata.lnglat.coordinates[0], - lat: node.metadata.lnglat.coordinates[1], - mp_id: node.metadata.ext_id ?? '' - }), + const lng = geojsonPointToLongitude(metadata.lnglat) + const lat = geojsonPointToLatitude(metadata.lnglat) + + const areaId = node.metadata.area_id.toUUID().toString() + + return ({ + ...node.metadata, + isDestination: metadata?.isDestination ?? false, + isBoulder: metadata?.isBoulder ?? false, + leftRightIndex: metadata?.leftRightIndex ?? -1, + area_id: areaId, + areaId, + lng, + lat, + mp_id: metadata.ext_id ?? '' + }) + }, media: async (node: any, args: any, { dataSources }) => { - const { areas }: { areas: AreaDataSource } = dataSources - return await areas.findMediaByAreaId(node.metadata.area_id, node.ancestors) + const { media }: { media: MediaDataSource } = dataSources + return await media.findMediaByAreaId(node.metadata.area_id, node.ancestors) }, - - createdBy: (node: AreaType) => node?.createdBy?.toUUID().toString(), - updatedBy: (node: AreaType) => node?.updatedBy?.toUUID().toString() + authorMetadata: getAuthorMetadataFromBaseNode }, CountByDisciplineType: { @@ -263,5 +276,6 @@ export const graphqlSchema = makeExecutableSchema({ XMediaTypeDef, TagTypeDef ], - resolvers + resolvers, + inheritResolversFromInterfaces: true }) diff --git a/src/graphql/schema/Area.gql b/src/graphql/schema/Area.gql index ec4ed12b..7b8a3c47 100644 --- a/src/graphql/schema/Area.gql +++ b/src/graphql/schema/Area.gql @@ -65,11 +65,9 @@ type Area { "The total number of climbs in this area" totalClimbs: Int! "Media associated with this area, or its child climbs" - media: [MediaTagType] - createdAt: Date - createdBy: ID - updatedAt: Date - updatedBy: ID + media: [MediaWithTags] + "Metadata about creation & update of this area" + authorMetadata: AuthorMetadata! } type AreaMetadata { diff --git a/src/graphql/schema/Climb.gql b/src/graphql/schema/Climb.gql index 02490a84..ffe57db5 100644 --- a/src/graphql/schema/Climb.gql +++ b/src/graphql/schema/Climb.gql @@ -52,17 +52,15 @@ type Climb { ancestors: [String!]! "Media associated with this climb" - media: [MediaTagType] + media: [MediaWithTags] yds: String @deprecated(reason: "Migrating to 'grades' field") "The parent area object" parent: Area! - createdAt: Date - updatedAt: Date - updatedBy: ID - createdBy: ID + "Metadata about creation & update of this climb" + authorMetadata: AuthorMetadata! } type ClimbMetadata { diff --git a/src/graphql/schema/History.gql b/src/graphql/schema/History.gql index 2b32b535..1940f789 100644 --- a/src/graphql/schema/History.gql +++ b/src/graphql/schema/History.gql @@ -28,7 +28,8 @@ union Document = Area | Climb | Organization type History { id: ID! - editedBy: String! + editedBy: ID! + editedByUser: String operation: String! createdAt: Date! changes: [Change] @@ -39,3 +40,13 @@ type Query { getAreaHistory(filter: AreaHistoryFilter): [History] getOrganizationHistory(filter: OrganizationHistoryFilter): [History] } + +"""Author metadata""" +type AuthorMetadata { + createdAt: Date + createdBy: ID + createdByUser: String + updatedAt: Date + updatedBy: ID + updatedByUser: String +} diff --git a/src/graphql/schema/Media.gql b/src/graphql/schema/Media.gql index 86251a9d..7cc9dd11 100644 --- a/src/graphql/schema/Media.gql +++ b/src/graphql/schema/Media.gql @@ -1,5 +1,5 @@ type Mutation { - setTag(input: MediaInput): TagEntryResult + setTag(input: MediaInput): MediaWithTags } type Mutation { @@ -7,59 +7,97 @@ type Mutation { } type Query { - getTagsByMediaIdList(uuidList: [ID]): [TagEntryResult] -} + """ + Get recent media with tags group by users. + """ + getMediaForFeed(input: MediaForFeedInput): [MediaByUsers] -type Query { - getRecentTags(userLimit: Int): [MediaListByAuthorType] -} + """ + Get all media belonging to a user (media with or without tags). + """ + getUserMedia(input: UserMediaInput): [MediaWithTags] -type Query { """ Get a list of users and their tagged photo count """ - getTagsLeaderboard(limit: Int): [TagsLeaderboardType] + getTagsLeaderboard(limit: Int): TagsLeaderboard } -type TagsLeaderboardType { +type TagsByUser { + username: String userUuid: ID! total: Int! } -"A tag linking the media with a climb or an area" -type MediaTagType { - mediaUuid: ID! - mediaUrl: String! - mediaType: Int! - destination: ID! - destType: Int! +type AllTimeTags { + totalMediaWithTags: Int! + byUsers: [TagsByUser]! } -"A tag linking the media with a climb" -type ClimbTag { - id: ID! - mediaUuid: ID! - mediaUrl: String! - mediaType: Int! - climb: Climb - destType: Int! +type TagsLeaderboard { + allTime: AllTimeTags } -"A tag linking the media with an area" -type AreaTag { +"Media object metadata" +interface IMediaMetadata { + """Unique id""" id: ID! - mediaUuid: ID! - mediaUrl: String! - mediaType: Int! - area: Area - destType: Int! + + """Width in pixels""" + width: Int! + + """Height in pixels""" + height: Int! + + """Valid format: jpeg, png, webp, avif""" + format: String! + + """Upload time""" + uploadTime: Date! + + """File size in bytes""" + size: Int! } -union TagEntryResult = ClimbTag | AreaTag +"All tags by an author" +type MediaByUsers { + username: String + userUuid: ID! + mediaWithTags: [MediaWithTags] +} + +"""A tag target (an area or a climb)""" +type EntityTag { + id: ID! + """Area or climb ID""" + targetId: ID! + """Climb name""" + climbName: String + """Area name""" + areaName: String! + + """ancestors name""" + ancestors: String! + """Target type: 0: climb, 1: area""" + type: Int! -type MediaListByAuthorType { - authorUuid: ID! - tagList: [MediaTagType] + "Longitude" + lng: Float! + + "Latitude" + lat: Float! +} + +type MediaWithTags implements IMediaMetadata { + id: ID! + username: String + mediaUrl: String! + width: Int! + height: Int! + format: String! + uploadTime: Date! + size: Int! + entityTags: [EntityTag] } "Result of a delete tag operation" @@ -78,3 +116,14 @@ input MediaInput { destinationId: ID! destType: Int! } + +"Input param for user media query" +input UserMediaInput { + userUuid: ID! + maxFiles: Int +} + +input MediaForFeedInput { + maxUsers: Int + maxFiles: Int +} diff --git a/src/model/AreaDataSource.ts b/src/model/AreaDataSource.ts index c360861c..1e9e8c00 100644 --- a/src/model/AreaDataSource.ts +++ b/src/model/AreaDataSource.ts @@ -1,9 +1,9 @@ import { MongoDataSource } from 'apollo-datasource-mongodb' import { Filter } from 'mongodb' -import muuid, { MUUID } from 'uuid-mongodb' +import muuid from 'uuid-mongodb' import bboxPolygon from '@turf/bbox-polygon' -import { getAreaModel, getMediaModel } from '../db/index.js' +import { getAreaModel, getMediaModel, getMediaObjectModel } from '../db/index.js' import { AreaType } from '../db/AreaTypes' import { GQLFilter, AreaFilterParams, PathTokenParams, LeafStatusParams, ComparisonFilterParams, StatisticsType, CragsNear, BBoxType } from '../types' import { getClimbModel } from '../db/ClimbSchema.js' @@ -13,7 +13,8 @@ import { logger } from '../logger.js' export default class AreaDataSource extends MongoDataSource { areaModel = getAreaModel() climbModel = getClimbModel() - mediaModel = getMediaModel() + tagModel = getMediaModel() + mediaObjectModal = getMediaObjectModel() async findAreasByFilter (filters?: GQLFilter): Promise { let mongoFilter: any = {} @@ -121,101 +122,6 @@ export default class AreaDataSource extends MongoDataSource { return rs } - async findMediaByAreaId (areaId: MUUID, ancestors: string): Promise { - const rs1 = await getMediaModel().aggregate([ - { - // SELECT * - // FROM media - // LEFT JOIN climbs - // ON media.destinationId == areas.metadata.area_id - $lookup: { - from: 'areas', // other collection name - foreignField: 'metadata.area_id', - localField: 'destinationId', - as: 'taggedArea', - pipeline: [{ - $match: { - $expr: { - $or: [ - { // Case 1: given a child area, inheret its ancestor's photos - // - input: A,B,C <-- area I want to search for tags - // - regex: A,B - $regexMatch: { - input: ancestors, - regex: '$ancestors', - options: 'i' - } - }, - { // Case 2: given a ancestor area, inherit descendant photos - // - input: A,B,C - // - regex: A,B <-- area I want to search for tags - $regexMatch: { - input: '$ancestors', - regex: ancestors, - options: 'i' - } - } - ] - } - } - }] - } - }, - { - $match: { - taggedArea: { - $ne: [] - } - } - } - ]) - const rs = await getMediaModel() - .aggregate([ - { - // SELECT * - // FROM media - // LEFT OUTER climbs - // ON climbs._id == media.destinationId - $lookup: { - from: 'climbs', // other collection name - foreignField: '_id', // climb._id - localField: 'destinationId', - as: 'taggedClimb', - pipeline: [{ - $lookup: { // also allow ancestor areas to inherent climb photo - from: 'areas', // other collection name - foreignField: 'metadata.area_id', - localField: 'metadata.areaRef', // climb.metadata.areaRef - as: 'area' - } - }, - { - $match: { - 'area.ancestors': { $regex: areaId.toUUID().toString() } - } - }, - { - $unwind: '$area' - } - ] - } - }, - { - $match: { - taggedClimb: { - $ne: [] - } - } - } - ]) - - if (rs != null) { - return rs.concat(rs1) - } else { - return rs1 - } - } - /** * Find a climb by uuid. Also return the parent area object (crag or boulder). * @@ -260,14 +166,6 @@ export default class AreaDataSource extends MongoDataSource { return null } - async findMediaByClimbId (climbId: MUUID): Promise { - const rs = await getMediaModel().find({ destinationId: climbId }).lean() - if (rs != null) { - return rs - } - return null - } - /** * Find all descendants (inclusive) starting from path * @param path comma-separated _id's of area diff --git a/src/model/MediaDataSource.ts b/src/model/MediaDataSource.ts index 4d3a3368..089f8798 100644 --- a/src/model/MediaDataSource.ts +++ b/src/model/MediaDataSource.ts @@ -1,96 +1,176 @@ import { MongoDataSource } from 'apollo-datasource-mongodb' -import muid from 'uuid-mongodb' +import muid, { MUUID } from 'uuid-mongodb' +import { logger } from '../logger.js' +import { getMediaObjectModel } from '../db/index.js' +import { TagsLeaderboardType, AllTimeTagStats, MediaByUsers, MediaForFeedInput, MediaObject } from '../db/MediaObjectTypes.js' -import { getMediaModel } from '../db/index.js' -import { MediaType, MediaListByAuthorType, TagsLeaderboardType } from '../db/MediaTypes.js' +const HARD_MAX_FILES = 1000 +const HARD_MAX_USERS = 100 -export default class MediaDataSource extends MongoDataSource { - async getTagsByMediaIds (uuidList: string[]): Promise { - if (uuidList !== undefined && uuidList.length > 0) { - const muidList = uuidList.map(entry => muid.from(entry)) - const rs = await getMediaModel() - .find({ mediaUuid: { $in: muidList } }) - .populate('climb') - .populate('area') - .lean({ virtual: true }) - return rs // type: TagEntryResultType +export default class MediaDataSourcmnee extends MongoDataSource { + mediaObjectModel = getMediaObjectModel() + + /** + * A reusable filter to exclude documents with empty entityTags + */ + entityTagsNotEmptyFilter = [{ + $match: { + entityTags: { $exists: true, $type: 4, $ne: [] } } - return [] - } + }] - async getRecentTags (userLimit: number = 10): Promise { - const rs = await getMediaModel().aggregate([ - { - $project: { - mediaUuid: 1, - mediaUrl: 1, - mediaType: 1, - destinationId: 1, - destType: 1, - onModel: 1, - authorUuid: { $substr: ['$mediaUrl', 3, 36] } + /** + * Get all media & tags grouped by users + * @param uuidStr optional user uuid to limit the search, otherwise include all users. + * @param maxUsers limit the number of users. + * @param maxFiles limit the number of files per user. + * @param includesNoEntityTags By default the query exludes media without tags. Specify 'true' override this behavoir. + * @returns MediaByUsers array + */ + async getMediaByUsers ({ uuidStr, maxUsers = 10, maxFiles = 10, includesNoEntityTags = false }: MediaForFeedInput): Promise { + const safeMaxFiles = maxFiles > HARD_MAX_FILES ? HARD_MAX_FILES : maxFiles + const safeMaxUsers = maxUsers > HARD_MAX_USERS ? HARD_MAX_USERS : maxUsers + + let userFilter: any[] = [] + if (uuidStr != null) { + userFilter = [{ + $match: { + userUuid: muid.from(uuidStr) } + }] + } + + const toIncludeMediaWithTagsOrNotfilters = includesNoEntityTags + ? [] + : this.entityTagsNotEmptyFilter + + const rs = await this.mediaObjectModel.aggregate([ + ...userFilter, + ...toIncludeMediaWithTagsOrNotfilters, + { + /** + * Sort by most recently uploaded media first + */ + $sort: { createdAt: -1 } }, { $group: { - _id: '$authorUuid', - mediaList: { $push: '$$ROOT' } + _id: { + userUuid: '$userUuid' + }, + mediaWithTags: { $push: '$$ROOT' } } }, { - $limit: userLimit // limit the number of authors - }, - { - $unwind: '$mediaList' - }, - { - $unset: 'mediaList.authorUuid' - }, - { - $sort: { 'mediaList._id': -1 } + $limit: safeMaxUsers }, { - $group: { - _id: '$_id', - tagList: { $push: '$mediaList' } + $project: { + _id: 0, + userUuid: '$_id.userUuid', + mediaWithTags: { + $slice: ['$mediaWithTags', safeMaxFiles] + } } } ]) return rs } + /** + * Get all media belonging to a user + * @param userLimit + */ + async getOneUserMedia (uuidStr: string, limit: number): Promise { + const rs = await this.getMediaByUsers({ uuidStr, maxUsers: 1, maxFiles: limit, includesNoEntityTags: true }) + if (rs.length !== 1) { + logger.error(`Expecting 1 user in result set but got ${rs.length}`) + return [] + } + return rs[0].mediaWithTags + } + /** * Get a list of users and their tagged photo count * @param limit how many entries * @returns Array of TagsLeaderboardType */ - async getTagsLeaderboard (limit = 30): Promise { - const rs = await getMediaModel().aggregate([ + async getTagsLeaderboard (limit = 30): Promise { + const rs = await this.mediaObjectModel.aggregate([ + ...this.entityTagsNotEmptyFilter, { - $project: { - mediaUuid: 1, - authorUuid: { $substr: ['$mediaUrl', 3, 36] } + $group: { + _id: '$userUuid', + total: { $count: {} } } }, { - $group: { - _id: '$authorUuid', - uniqueCount: { $addToSet: '$mediaUuid' } // A photo can have multiple tags. Use 'Set' to count multiple occurences once. - } + /** + * Sort by media count descending + */ + $sort: { total: -1 } }, { $project: { _id: 0, userUuid: '$_id', - total: { $size: '$uniqueCount' } + total: 1 } }, { - $sort: { total: -1 } + $group: { + _id: null, + totalMediaWithTags: { $sum: '$$ROOT.total' }, + byUsers: { $push: '$$ROOT' } + } }, { - $limit: limit - }]) + $unset: ['_id'] + } + ], { + /** + * Read from secondary node since data freshness is not too important + * for this query. + * See https://www.mongodb.com/docs/manual/core/read-preference/ + */ + readPreference: 'secondaryPreferred' + }) + + if (rs?.length !== 1) throw new Error('Unexpected leaderboard query error') + + return { + allTime: rs[0] + } + } + + /** + * Find tags associated with a given climb id. + * + * @param climbId + * @returns `MediaWithTags` array + */ + async findMediaByClimbId (climbId: MUUID, climbName: string): Promise { + const rs = await this.mediaObjectModel.find({ + 'entityTags.targetId': climbId + }).lean() + return rs + } + + /** + * Find all media tags associated with an area. An area can have its own + * tags or inherit tags from their children. A note on inheritance: + * + * 1. A parent area and their ancestors will inherit tags from their children. + * 2. Child area or climb will **not** inherit their parent/ancestor tags. + * + * @param areaId + * @param ancestors + * @returns `UserMediaWithTags` array + */ + async findMediaByAreaId (areaId: MUUID, ancestors: string): Promise { + const rs = await this.mediaObjectModel.find({ + 'entityTags.ancestors': { $regex: areaId.toUUID().toString() } + }).lean() return rs } } diff --git a/src/model/__tests__/MediaDataSource.ts b/src/model/__tests__/MediaDataSource.ts index 73366b77..eb722c16 100644 --- a/src/model/__tests__/MediaDataSource.ts +++ b/src/model/__tests__/MediaDataSource.ts @@ -120,23 +120,24 @@ describe('MediaDataSource', () => { await expect(media.setTag(areaTag1)).rejects.toThrowError(/Duplicate/) }) - it('should return recent tags', async () => { + it.skip('should return recent tags', async () => { if (areaForTagging == null) fail('Pre-seeded test area not found') - let tags = await media.getRecentTags() + let tags = await media.getMediaByUsers({}) expect(tags).toHaveLength(0) await media.setTag(areaTag1) - tags = await media.getRecentTags() + tags = await media.getMediaByUsers({}) expect(tags).toHaveLength(1) - expect(tags[0].tagList).toHaveLength(1) + expect(tags[0].mediaWithTags).toHaveLength(1) - expect(tags[0].tagList[0]).toMatchObject({ + expect(tags[0].mediaWithTags[0]).toMatchObject({ mediaType: areaTag1.mediaType, mediaUrl: areaTag1.mediaUrl }) - expect(tags[0].tagList[0].mediaUuid.toUUID().toString()).toEqual(areaTag1.mediaUuid.toUUID().toString()) + // @ts-expect-error + expect(tags[0].mediaWithTags[0].mediaUuid.toUUID().toString()).toEqual(areaTag1.mediaUuid.toUUID().toString()) }) }) diff --git a/src/types.ts b/src/types.ts index 2e4d721e..49226d37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,3 +102,10 @@ export interface Context { export interface ContextWithAuth extends Context { user: AuthUserType } + +export interface AuthorMetadata { + updatedAt?: Date + updatedBy?: MUUID + createdAt?: Date + createdBy?: MUUID +} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 5c7400f8..abee2175 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,17 +1,44 @@ import { MUUID } from 'uuid-mongodb' +import axios, { AxiosResponse } from 'axios' +import pMemoize from 'p-memoize' +import ExpiryMap from 'expiry-map' +import { Point } from '@turf/helpers' + +import { logger } from '../logger.js' + +const cache = new ExpiryMap(600000) // TTL = 10 minutes + +export const cdnHttpClient = axios.create({ + baseURL: process.env.CDN_URL ?? '', + timeout: 2000 +} +) export const muuidToString = (m: MUUID): string => m.toUUID().toString() +interface UID_TYPE { + uid: string +} + +const _getUserNickFromMediaDir = async (uuid: string): Promise => { + let res: AxiosResponse | undefined + try { + res = await cdnHttpClient.get(`/u/${uuid}/uid.json`) + if (res.status >= 200 && res.status <= 204) { + return res?.data?.uid ?? null + } else return null + } catch (e) { + logger.error(e, `Error fetching /u/${uuid}/uid.json`) + return null + } +} + /** - * Detects if string is in uuid-mongodb's "canonical" base64 format. - * @param s input string - * @returns + * Given a user uuid, locate the media server for the user home dir and their nick name. In the future we will store uuid -> username mapping in this DB. + * @param uuid + * @returns user nick name or `null` if not found */ -export const isBase64Str = (s: string): boolean => { - const bc = /[A-Za-z0-9+/=]/.test(s) - const lc = /.*=$/.test(s) // make sure it ends with '=' - return bc && lc -} +export const getUserNickFromMediaDir = pMemoize(_getUserNickFromMediaDir, { cache }) /** * Ensures that type-checking errors out if enums are not @@ -28,3 +55,6 @@ export const isBase64Str = (s: string): boolean => { export function exhaustiveCheck (_value: never): never { throw new Error(`ERROR! Enum not handled for ${JSON.stringify(_value)}`) } + +export const geojsonPointToLongitude = (point: Point): number => point.coordinates[0] +export const geojsonPointToLatitude = (point: Point): number => point.coordinates[1] diff --git a/yarn.lock b/yarn.lock index 2902fe9f..a8d689b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1148,6 +1148,47 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.38.0.tgz#73a8a0d8aa8a8e6fe270431c5e72ae91b5337892" integrity sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g== +"@google-cloud/paginator@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" + integrity sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/projectify@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-3.0.0.tgz#302b25f55f674854dce65c2532d98919b118a408" + integrity sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA== + +"@google-cloud/promisify@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-3.0.1.tgz#8d724fb280f47d1ff99953aee0c1669b25238c2e" + integrity sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA== + +"@google-cloud/storage@^6.9.5": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-6.9.5.tgz#2df7e753b90dba22c7926ecbe16affbd7489939d" + integrity sha512-fcLsDA8YKcGuqvhk0XTjJGVpG9dzs5Em8IcUjSjspYvERuHYqMy9CMChWapSjv3Lyw//exa3mv4nUxPlV93BnA== + dependencies: + "@google-cloud/paginator" "^3.0.7" + "@google-cloud/projectify" "^3.0.0" + "@google-cloud/promisify" "^3.0.0" + abort-controller "^3.0.0" + async-retry "^1.3.3" + compressible "^2.0.12" + duplexify "^4.0.0" + ent "^2.2.0" + extend "^3.0.2" + gaxios "^5.0.0" + google-auth-library "^8.0.1" + mime "^3.0.0" + mime-types "^2.0.8" + p-limit "^3.0.1" + retry-request "^5.0.0" + teeny-request "^8.0.0" + uuid "^8.0.0" + "@graphql-tools/batch-execute@8.5.0": version "8.5.0" resolved "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.0.tgz" @@ -1541,6 +1582,11 @@ resolved "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz" integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw== +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" @@ -1618,6 +1664,11 @@ dependencies: "@sinonjs/commons" "^2.0.0" +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@turf/area@^6.5.0": version "6.5.0" resolved "https://registry.npmjs.org/@turf/area/-/area-6.5.0.tgz" @@ -2348,6 +2399,11 @@ array.prototype.tosorted@^1.1.1: es-shim-unscopables "^1.0.0" get-intrinsic "^1.1.3" +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -2360,7 +2416,7 @@ async-mutex@^0.3.2: dependencies: tslib "^2.3.1" -async-retry@^1.2.1: +async-retry@^1.2.1, async-retry@^1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz" integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== @@ -2389,6 +2445,15 @@ axios@^0.26.0: dependencies: follow-redirects "^1.14.8" +axios@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.6.tgz#1ace9a9fb994314b5f6327960918406fa92c6646" + integrity sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.4.2: version "29.4.2" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.2.tgz#b17b9f64be288040877cbe2649f91ac3b63b2ba6" @@ -2454,11 +2519,16 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bignumber.js@^9.0.0: + version "9.1.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" + integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -2499,6 +2569,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -2636,6 +2713,11 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +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.8.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" @@ -2646,6 +2728,15 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +cliui@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2684,11 +2775,27 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2716,6 +2823,13 @@ component-emitter@^1.3.0: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +compressible@^2.0.12: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2773,7 +2887,7 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -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, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2818,11 +2932,23 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -2861,6 +2987,11 @@ destroy@1.2.0: resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-libc@^2.0.0, detect-libc@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -2958,7 +3089,17 @@ dotenv@^10.0.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -ecdsa-sig-formatter@1.0.11: +duplexify@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" + integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.0" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -2990,13 +3131,18 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.4.1: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -3430,6 +3576,11 @@ exit@^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: version "29.0.3" resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.3.tgz#6be65ddb945202f143c4e07c083f4f39f3bd326f" @@ -3452,6 +3603,13 @@ expect@^29.4.2: jest-message-util "^29.4.2" jest-util "^29.4.2" +expiry-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/expiry-map/-/expiry-map-2.0.0.tgz#b441ee8e8865291ad9a542783076d33bae0f3582" + integrity sha512-K1I5wJe2fiqjyUZf/xhxwTpaopw3F+19DsO7Oggl20+3SVTXDIevVRJav0aBMfposQdkl2E4+gnuOKd3j2X0sA== + dependencies: + map-age-cleaner "^0.2.0" + express@^4.17.1: version "4.18.1" resolved "https://registry.npmjs.org/express/-/express-4.18.1.tgz" @@ -3489,6 +3647,11 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -3525,6 +3688,11 @@ fast-safe-stringify@^2.1.1: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-text-encoding@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + fast-xml-parser@4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz#5a98c18238d28a57bbdfa9fe4cda01211fff8f4a" @@ -3638,6 +3806,11 @@ follow-redirects@^1.14.8: resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" @@ -3645,6 +3818,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3709,6 +3890,24 @@ functions-have-names@^1.2.2: resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gaxios@^5.0.0, gaxios@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.0.tgz#133b77b45532be71eec72012b7e97c2320b6140a" + integrity sha512-aezGIjb+/VfsJtIcHGcBSerNEDdfdHeMros+RbYbGpmonKWQCOVOes0LVZhn1lDtIgq55qq0HaxymIoae3Fl/A== + dependencies: + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.7" + +gcp-metadata@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.2.0.tgz#b4772e9c5976241f5d3e69c4f446c906d25506ec" + integrity sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw== + dependencies: + 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" @@ -3774,6 +3973,11 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -3788,6 +3992,17 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.2.tgz#ce2468727de7e035e8ecf684669dc74d0526ab75" + integrity sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.0.3" + minimatch "^9.0.0" + minipass "^5.0.0" + path-scurry "^1.7.0" + glob@^7.1.3, glob@^7.1.4, glob@^7.1.5: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" @@ -3831,6 +4046,28 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +google-auth-library@^8.0.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.7.0.tgz#e36e255baba4755ce38dded4c50f896cf8515e51" + integrity sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^5.0.0" + gcp-metadata "^5.0.0" + gtoken "^6.1.0" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-p12-pem@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a" + integrity sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ== + dependencies: + node-forge "^1.3.1" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -3877,6 +4114,15 @@ graphql@^16.5.0: resolved "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz" integrity sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA== +gtoken@^6.1.0: + version "6.1.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc" + integrity sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ== + dependencies: + gaxios "^5.0.1" + google-p12-pem "^4.0.0" + jws "^4.0.0" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" @@ -3954,7 +4200,16 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -https-proxy-agent@^5.0.1: +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -4035,6 +4290,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" @@ -4077,6 +4337,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" @@ -4271,6 +4536,15 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jackspeak@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.0.tgz#69831fe5346532888f279102f39fc4452ebbe6c2" + integrity sha512-DiEwVPqsieUzZBNxQ2cxznmFzfg/AMgJUjYw5xl6rSmCxAQXECcbSdwcLM6Ds6T09+SBfSNCGPhYUoQ96P4h7A== + dependencies: + cliui "^7.0.4" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-changed-files@^29.4.2: version "29.4.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.2.tgz#bee1fafc8b620d6251423d1978a0080546bc4376" @@ -4745,6 +5019,13 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" @@ -4815,6 +5096,15 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jwks-rsa@^2.1.4: version "2.1.4" resolved "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz" @@ -4835,6 +5125,14 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + kareem@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.4.1.tgz#7d81ec518204a48c1cb16554af126806c3cd82b0" @@ -5014,6 +5312,11 @@ lru-cache@^7.10.1: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.12.0.tgz" integrity sha512-OIP3DwzRZDfLg9B9VP/huWBlpvbkmbfiBy8xmsXp4RPmE4A3MhwNozc5ZJ3fWnSg8fDcdlE/neRTPG2ycEKliw== +lru-cache@^9.0.0: + version "9.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.1.tgz#c58a93de58630b688de39ad04ef02ef26f1902f1" + integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A== + lru-cache@~4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz" @@ -5049,6 +5352,13 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +map-age-cleaner@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.2.0.tgz#0196bc278f7244ddeb7ca0cb3df329b06241a44b" + integrity sha512-AvxTC6id0fzSf6OyNBTp1syyCuKO7nOJvHgYlhT0Qkkjvk40zZo+av3ayVgXlxnF/DxEzEfY9mMdd7FHsd+wKQ== + dependencies: + p-defer "^1.0.0" + md5-file@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-5.0.0.tgz#e519f631feca9c39e7f9ea1780b63c4745012e20" @@ -5092,12 +5402,12 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -5114,11 +5424,26 @@ mime@2.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +mime@^3.0.0: + version "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-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +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: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -5126,11 +5451,33 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56" + integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.6" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.3: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mongodb-connection-string-url@^2.5.2: version "2.5.3" resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz#c0c572b71570e58be2bd52b33dffd1330cfb6990" @@ -5257,6 +5604,11 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -5289,6 +5641,18 @@ nock@^13.3.0: lodash "^4.17.21" propagate "^2.0.0" +node-abi@^3.3.0: + version "3.40.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.40.0.tgz#51d8ed44534f70ff1357dfbc3a89717b1ceac1b4" + integrity sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA== + dependencies: + semver "^7.3.5" + +node-addon-api@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== + node-fetch@2: version "2.6.8" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e" @@ -5303,6 +5667,18 @@ node-fetch@^2.1.2, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.1: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "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" @@ -5427,7 +5803,7 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -5453,6 +5829,11 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw== + p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" @@ -5460,7 +5841,7 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2, p-limit@^3.1.0: +p-limit@^3.0.1, p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -5502,6 +5883,14 @@ p-locate@^6.0.0: dependencies: p-limit "^4.0.0" +p-memoize@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/p-memoize/-/p-memoize-7.1.1.tgz#53b1d0e6007288f7261cfa11a7603b84c9261bfa" + integrity sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA== + dependencies: + mimic-fn "^4.0.0" + type-fest "^3.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -5572,6 +5961,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.7.0.tgz#99c741a2cfbce782294a39994d63748b5a24f6db" + integrity sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg== + dependencies: + lru-cache "^9.0.0" + minipass "^5.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" @@ -5669,6 +6066,24 @@ postcss@^8.3.11: picocolors "^1.0.0" source-map-js "^1.0.2" +prebuild-install@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -5741,11 +6156,24 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pseudomap@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" @@ -5790,6 +6218,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -5894,6 +6332,14 @@ resolve@^2.0.0-next.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +retry-request@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-5.0.2.tgz#143d85f90c755af407fcc46b7166a4ba520e44da" + integrity sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ== + dependencies: + debug "^4.1.1" + extend "^3.0.2" + retry@0.13.1: version "0.13.1" resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" @@ -6034,6 +6480,20 @@ sha.js@^2.4.11: inherits "^2.0.1" safe-buffer "^5.0.1" +sharp@^0.32.0: + version "0.32.0" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.0.tgz#146b3e1930d56518699908d9116d8a03be1f5cf6" + integrity sha512-yLAypVcqj1toSAqRSwbs86nEzfyZVDYqjuUX8grhFpeij0DDNagKJXELS/auegDBRDg1XBtELdOGfo2X1cCpeA== + dependencies: + color "^4.2.3" + detect-libc "^2.0.1" + node-addon-api "^6.0.0" + prebuild-install "^7.1.1" + semver "^7.3.8" + simple-get "^4.0.1" + tar-fs "^2.1.1" + tunnel-agent "^0.6.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -6065,6 +6525,32 @@ signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.1.tgz#96a61033896120ec9335d96851d902cc98f0ba2a" + integrity sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw== + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0, simple-get@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + 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" @@ -6160,6 +6646,18 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -6270,11 +6768,21 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + strnum@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + superagent@^8.0.5: version "8.0.9" resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" @@ -6325,6 +6833,16 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +tar-fs@^2.0.0, tar-fs@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + tar-stream@^2.1.4: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" @@ -6336,6 +6854,17 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +teeny-request@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-8.0.3.tgz#5cb9c471ef5e59f2fca8280dc3c5909595e6ca24" + integrity sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.1" + 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" @@ -6460,6 +6989,13 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -6487,6 +7023,11 @@ type-fest@^0.3.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^3.0.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.9.0.tgz#36a9e46e6583649f9e6098b267bc577275e9e4f4" + integrity sha512-hR8JP2e8UiH7SME5JZjsobBlEiatFoxpzCP+R3ZeCo7kAaG1jXQE5X/buLzogM6GJu8le9Y4OcfNuIQX0rZskA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz"