xiongzhu 2 роки тому
батько
коміт
93ee7b1f5f

+ 3 - 0
package.json

@@ -53,6 +53,7 @@
     "cld": "^2.9.0",
     "crypto": "^1.0.1",
     "date-fns": "^2.29.3",
+    "dedent": "^0.7.0",
     "eventsource-parser": "^1.0.0",
     "express-basic-auth": "^1.2.1",
     "express-handlebars": "^7.0.6",
@@ -79,8 +80,10 @@
     "reflect-metadata": "^0.1.13",
     "rimraf": "^4.1.2",
     "rxjs": "^7.8.0",
+    "sequelize": "^6.31.1",
     "tnwx": "^2.5.6",
     "typeorm": "^0.3.12",
+    "util": "^0.12.5",
     "uuid": "^9.0.0",
     "yup": "^1.0.0"
   },

+ 0 - 72
pdf.mjs

@@ -1,72 +0,0 @@
-import PdfParse from '@cyber2024/pdf-parse-fixed'
-import { readFileSync } from 'fs'
-import cld from 'cld'
-import { Configuration, OpenAIApi } from 'azure-openai'
-import pg from 'pg'
-
-async function pdf2text(path) {
-    const pdf = await PdfParse(readFileSync(path))
-    const contents = []
-    let newParagraph = ''
-    pdf.text
-        .trim()
-        .split('\n')
-        .forEach((line) => {
-            line = line.trim()
-            newParagraph += line
-            if (isFullSentence(line)) {
-                contents.push(newParagraph)
-                newParagraph = ''
-            }
-        })
-    if (newParagraph) {
-        contents.push(newParagraph)
-    }
-
-    const lang = await cld.detect(contents.join('\n'))
-    console.log(contents.length)
-}
-
-function isFullSentence(str) {
-    return /[.!?。!?…;;::”’)】》」』〕〉》〗〞〟»"'\])}]+$/.test(str)
-}
-
-await pdf2text('/Users/drew/Downloads/《Python 3学习笔记(上卷)》_1-50.pdf')
-
-const openai = new OpenAIApi(
-    new Configuration({
-        apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
-        // add azure info into configuration
-        azure: {
-            apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
-            endpoint: 'https://zouma.openai.azure.com/'
-        }
-    })
-)
-// const response = await openai.createEmbedding({
-//     model: 'embedding',
-//     input: 'The food was delicious and the waiter...'
-// })
-// console.log(JSON.stringify(response.data, null, 4))
-
-const client = new pg.Client({
-    host: '47.97.42.229',
-    port: 5432,
-    user: 'postgres',
-    password: 'D$&g3a9BCJH&$Nzh',
-    database: 'gpt_test',
-    connectionTimeoutMillis: 5000
-})
-await client.connect()
-
-//table exists
-client.query(`create table if not exists public.chat_embedding (
-    id integer primary key not null default nextval('embedding_id_seq'::regclass),
-    name character varying,
-    text character varying,
-    embedding vector(1536)
-);`)
-const res = await client.query('SELECT * FROM chat_embedding')
-
-console.log(res.rows)
-client.end()

+ 0 - 149
pem.mjs

@@ -1,149 +0,0 @@
-import pem from 'pem'
-import { readFileSync } from 'fs'
-import crypto from 'crypto'
-import axios from 'axios'
-import { format, addSeconds } from 'date-fns'
-import qs from 'querystring'
-
-const privateKey = crypto.createPrivateKey({ key: readFileSync('certs/6888806043057.key') })
-const publicKey = crypto.createPublicKey({ key: readFileSync('certs/sand.crt') })
-
-function privSign(str) {
-    const signer = crypto.createSign('SHA1')
-    signer.update(str)
-    signer.end()
-    const signature = signer.sign(privateKey)
-    return signature.toString('base64')
-}
-
-function verifySign(str, sign) {
-    const verifier = crypto.createVerify('SHA1')
-    verifier.update(str)
-    verifier.end()
-    return verifier.verify(publicKey, Buffer.from(sign.replace(/\s/g, '+'), 'base64'))
-}
-
-async function request(header, body, url) {
-    const data = {
-        head: header,
-        body
-    }
-    const dataStr = JSON.stringify(data)
-    console.log(JSON.stringify(data, null, 4))
-    const sign = await privSign(dataStr)
-    const postBody = {
-        charset: 'UTF-8',
-        data: dataStr,
-        signType: '01',
-        sign,
-        extend: ''
-    }
-    console.log(qs.stringify(postBody))
-    return await axios.post(url, qs.stringify(postBody), {
-        headers: {
-            'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
-        },
-        timeout: 30000,
-        timeoutErrorMessage: '请求超时'
-    })
-}
-const algorithm = 'aes-128-ecb'
-async function pay() {
-    const data = {
-        cityNo: '',
-        productId: '00000004',
-        bankType: '',
-        payMode: '',
-        accNo: '6222024301070380165',
-        accName: '熊竹',
-        bankName: '',
-        remark: '消费',
-        channelType: '',
-        accAttr: '0',
-        version: '01',
-        timeOut: '20230506150701',
-        extend: '',
-        extendParams: '',
-        tranTime: '20230506150401',
-        provNo: '',
-        phone: '',
-        tranAmt: '000000000100',
-        reqReserved: '',
-        orderCode: new Date().getTime() + '',
-        accType: '4',
-        currencyCode: '156'
-    }
-    const dataStr = JSON.stringify(data)
-
-    // 加密密钥,必须是 16、24 或 32 个字符长(分别对应 AES-128、AES-192 或 AES-256)
-    const key = crypto.randomBytes(16)
-
-    // 加密算法使用 AES-256-ECB
-
-    // 将要加密的数据转换为一个 Buffer 对象
-    const dataBuffer = Buffer.from(dataStr, 'utf8')
-
-    // 创建一个加密器对象,使用 PKCS5Padding 进行补位
-    const cipher = crypto.createCipheriv(algorithm, key, null)
-    cipher.setAutoPadding(true)
-
-    // 加密数据,并返回加密后的 Buffer 对象
-    const encryptedBuffer = Buffer.concat([cipher.update(dataBuffer), cipher.final()])
-
-    // 将加密后的 Buffer 对象转换为十六进制字符串
-    const encrypted = encryptedBuffer.toString('base64')
-
-    const sign = await privSign(dataStr)
-
-    console.log(encrypted)
-
-    const encryptKey = crypto
-        .publicEncrypt(
-            {
-                key: publicKey,
-                padding: crypto.constants.RSA_PKCS1_PADDING
-            },
-            key
-        )
-        .toString('base64')
-    console.log(encryptKey)
-
-    const body = {
-        transCode: 'RTPM',
-        accessType: '0',
-        merId: '6888806043057',
-        encryptKey: encryptKey,
-        encryptData: encrypted,
-        sign,
-        extend: ''
-    }
-    const res = await axios.post('http://120.78.171.194:11223/agent-main/openapi/agentpay', qs.stringify(body))
-
-    const retData = qs.parse(qs.unescape(res.data))
-    console.log(retData)
-
-    const decryptKey = crypto.privateDecrypt(
-        {
-            key: privateKey,
-            padding: crypto.constants.RSA_PKCS1_PADDING
-        },
-        Buffer.from(retData.encryptKey.replace(/\s/g, '+'), 'base64')
-    )
-    console.log(decryptKey.toString())
-
-    const decipher = crypto.createDecipheriv(algorithm, decryptKey, null)
-    decipher.setAutoPadding(true)
-
-    const decryptedBuffer = Buffer.concat([
-        decipher.update(Buffer.from(retData.encryptData.replace(/\s/g, '+'), 'base64')),
-        decipher.final()
-    ])
-    const decrypted = decryptedBuffer.toString('utf8')
-    console.log(decrypted)
-
-    const verify = verifySign(decrypted, retData.sign)
-    console.log(verify)
-
-
-}
-await pay()

+ 15 - 1
src/chat-pdf/chat-pdf.controller.ts

@@ -1,4 +1,4 @@
-import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'
+import { Body, Controller, Get, Post, Render, UploadedFile, UseInterceptors } from '@nestjs/common'
 import { Public } from '../auth/public.decorator'
 import { FileInterceptor } from '@nestjs/platform-express'
 import { ChatPdfService } from './chat-pdf.service'
@@ -13,4 +13,18 @@ export class ChatPdfController {
     public async uploadFile(@UploadedFile() file: Express.Multer.File) {
         return await this.chatPdfService.upload(file)
     }
+
+    @Public()
+    @Post('ask')
+    public async ask(@Body() { q, name }) {
+        return await this.chatPdfService.ask(q, name)
+    }
+
+    @Public()
+    @Public()
+    @Get('ui')
+    @Render('chatPdf')
+    public async answer() {
+        return {}
+    }
 }

+ 2 - 1
src/chat-pdf/chat-pdf.module.ts

@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'
 import { ChatPdfService } from './chat-pdf.service'
 import { ChatPdfController } from './chat-pdf.controller'
 import { TypeOrmModule } from '@nestjs/typeorm'
+import { ConfigModule } from '@nestjs/config'
 
 @Module({
-    imports: [TypeOrmModule.forFeature([])],
+    imports: [ConfigModule],
     providers: [ChatPdfService],
     controllers: [ChatPdfController]
 })

+ 155 - 35
src/chat-pdf/chat-pdf.service.ts

@@ -1,57 +1,120 @@
 import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
-import { mkdtempSync } from 'fs'
-import { tmpdir } from 'os'
 import * as PdfParse from '@cyber2024/pdf-parse-fixed'
 import { createHash } from 'crypto'
-import { DataSource } from 'typeorm'
-import { InjectDataSource } from '@nestjs/typeorm'
-import { get_encoding } from '@dqbd/tiktoken'
+import { Tiktoken, get_encoding } from '@dqbd/tiktoken'
 import { Configuration, OpenAIApi } from 'azure-openai'
 import * as queue from 'fastq'
 import { setTimeout } from 'timers/promises'
+import * as dedent from 'dedent'
+import { Sequelize, Model, DataTypes } from 'sequelize'
+import { ConfigService } from '@nestjs/config'
+import { ChatEmbedding } from './entities/chat-embedding.entity'
+import { VECTOR } from './pgvector'
+
+function formatEmbedding(embedding: number[]) {
+    return `[${embedding.join(', ')}]`
+}
 
 @Injectable()
 export class ChatPdfService {
-    tokenizer = get_encoding('cl100k_base')
-    constructor(
-        @InjectDataSource('db1')
-        private dataSource: DataSource
-    ) {}
+    private readonly tokenizer: Tiktoken
+    private readonly openai: OpenAIApi
+    private readonly sequelize: Sequelize
+    constructor(private readonly configService: ConfigService) {
+        this.tokenizer = get_encoding('cl100k_base')
+        this.openai = new OpenAIApi(
+            new Configuration({
+                apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
+                // add azure info into configuration
+                azure: {
+                    apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
+                    endpoint: 'https://zouma.openai.azure.com'
+                }
+            })
+        )
+        this.sequelize = new Sequelize({
+            dialect: 'postgres',
+            host: configService.get<string>('PG_HOST'),
+            port: configService.get<number>('PG_PORT'),
+            username: configService.get<string>('PG_USERNAME'),
+            password: configService.get<string>('PG_PASSWORD'),
+            database: configService.get<string>('PG_DATABASE'),
+            logging: (msg) => Logger.debug(msg, 'Sequelize')
+        })
+        ChatEmbedding.init(
+            {
+                id: {
+                    primaryKey: true,
+                    autoIncrement: true,
+                    type: DataTypes.INTEGER
+                },
+                name: {
+                    type: DataTypes.STRING
+                },
+                text: {
+                    type: DataTypes.TEXT({
+                        length: 'long'
+                    })
+                },
+                num: {
+                    type: DataTypes.INTEGER
+                },
+                embedding: {
+                    type: new VECTOR(1536)
+                }
+            },
+            { sequelize: this.sequelize }
+        )
+        this.sequelize.sync()
+    }
 
     public async upload(file: Express.Multer.File) {
         const { originalname, buffer, mimetype } = file
         const md5 = this.calculateMD5(buffer)
+        const res = await ChatEmbedding.findAll()
+        if (res.length) {
+            return {
+                name: md5
+            }
+        }
         const pdf = await PdfParse(buffer)
         const contents = []
-        let newParagraph = ''
+        let paragraph = ''
         pdf.text
             .trim()
             .split('\n')
             .forEach((line) => {
                 line = line.trim()
-                newParagraph += line
+                paragraph += line
                 if (this.isFullSentence(line)) {
-                    contents.push(newParagraph)
-                    newParagraph = ''
+                    contents.push(paragraph)
+                    paragraph = ''
                 }
             })
-        if (newParagraph) {
-            contents.push(newParagraph)
+        if (paragraph) {
+            contents.push(paragraph)
         }
 
         const embeddings = await this.createEmbeddings(contents)
+        Logger.log(
+            `create embeddings finished, total token usage: ${embeddings.reduce((acc, cur) => acc + cur.token, 0)}`
+        )
+        let i = 0
         for (const item of embeddings) {
-            const sql = `INSERT INTO chat_embedding (id, name, text, embedding) VALUES (default, '${md5}', '${item.text
-                .replace(/'/g, "''")
-                .replace(/\\/g, '\\\\')}', '[${item.embedding.join(',')}]')`
             try {
-                await this.dataSource.query(sql)
+                await ChatEmbedding.create({
+                    name: md5,
+                    text: item.text,
+                    num: i++,
+                    embedding: formatEmbedding(item.embedding)
+                })
             } catch (error) {
-                Logger.error(sql)
+                Logger.error(error.message)
             }
         }
-        const res = await this.dataSource.query('select * from chat_embedding')
-        return res
+        return {
+            name: md5
+        }
     }
 
     isFullSentence(str) {
@@ -69,6 +132,7 @@ export class ChatPdfService {
         const result = Array(content.length)
         async function worker(arg) {
             result[arg.index] = await self.getEmbedding(arg.text)
+            Logger.log(`create embedding for ${arg.index + 1}/${content.length}`)
         }
         const q = queue.promise(worker, 64)
         content.forEach((text, index) => {
@@ -82,24 +146,15 @@ export class ChatPdfService {
     }
 
     async getEmbedding(content: string, retry = 0) {
-        const openai = new OpenAIApi(
-            new Configuration({
-                apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
-                // add azure info into configuration
-                azure: {
-                    apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
-                    endpoint: 'https://zouma.openai.azure.com/'
-                }
-            })
-        )
         try {
-            const response = await openai.createEmbedding({
+            const response = await this.openai.createEmbedding({
                 model: 'embedding',
                 input: content
             })
             return {
                 text: content,
-                embedding: response.data.data[0].embedding
+                embedding: response.data.data[0].embedding,
+                token: response.data.usage.total_tokens
             }
         } catch (error) {
             if (retry < 3) {
@@ -109,4 +164,69 @@ export class ChatPdfService {
             throw new InternalServerErrorException(error.message)
         }
     }
+
+    async getKeywords(text: string) {
+        try {
+            const res = await this.openai.createChatCompletion({
+                model: 'gpt35',
+                messages: [
+                    {
+                        role: 'user',
+                        content: `You need to extract keywords from the statement or question and return a series of keywords separated by commas.\ncontent: ${text}\nkeywords: `
+                    }
+                ]
+            })
+            return res.data.choices[0].message.content
+        } catch (error) {
+            Logger.error(error.message)
+            throw new InternalServerErrorException(error.message)
+        }
+    }
+
+    async searchEmbedding(name: string, embedding: number[]) {
+        return await ChatEmbedding.findAll({
+            order: this.sequelize.literal(`embedding <-> '${formatEmbedding(embedding)}'`),
+            limit: 100
+        })
+    }
+
+    cutContext(context: string[]) {
+        let max = 4096 - 1024
+        for (let i = 0; i < context.length; i++) {
+            max -= this.tokenizer.encode(context[i]).length
+            if (max < 0) {
+                return context.slice(0, i)
+            }
+        }
+    }
+
+    async ask(q: string, name: string) {
+        const keywords = await this.getKeywords(q)
+        const { embedding: keywordEmbedding } = await this.getEmbedding(keywords)
+        const context = this.cutContext((await this.searchEmbedding(name, keywordEmbedding)).map((item) => item.text))
+        const content = dedent`You are a helpful AI article assistant. '
+        The following are the relevant article content fragments found from the article. 
+        The relevance is sorted from high to low. '
+        You can only answer according to the following content:
+        \`\`\`
+        ${context.join('\n')}
+        \`\`\`
+        You need to carefully consider your answer to ensure that it is based on the context. 
+        If the context does not mention the content or it is uncertain whether it is correct, 
+        please answer "Current context cannot provide effective information."
+        You must use Chinese to respond.`
+        Logger.log(content)
+        try {
+            const response = await this.openai.createChatCompletion({
+                model: 'gpt35',
+                messages: [
+                    { role: 'user', content },
+                    { role: 'user', content: q }
+                ]
+            })
+            return { answer: response.data.choices[0].message.content }
+        } catch (error) {
+            Logger.error(error.message)
+        }
+    }
 }

+ 14 - 0
src/chat-pdf/entities/chat-embedding.entity.ts

@@ -0,0 +1,14 @@
+import { Model } from 'sequelize'
+
+export class ChatEmbedding extends Model {
+
+    id: number
+
+    name: string
+
+    text: string
+
+    num: number
+
+    embedding: string
+}

+ 41 - 0
src/chat-pdf/pgvector/index.ts

@@ -0,0 +1,41 @@
+const util = require('util')
+import { DataTypes } from 'sequelize'
+
+function fromSql(value) {
+    return value
+        .substring(1, value.length - 1)
+        .split(',')
+        .map((v) => parseFloat(v))
+}
+
+function toSql(value) {
+    return JSON.stringify(value)
+}
+
+export class VECTOR extends DataTypes.ABSTRACT {
+    dimensions: number
+    key: string
+    constructor(dimensions) {
+        super()
+        this.dimensions = dimensions
+        this.key = `vector(${dimensions})`
+    }
+
+    toSql() {
+        if (this.dimensions === undefined) {
+            return 'VECTOR'
+        }
+        if (!Number.isInteger(this.dimensions)) {
+            throw new Error('expected integer')
+        }
+        return util.format('VECTOR(%d)', this.dimensions)
+    }
+
+    _stringify(value) {
+        return toSql(value)
+    }
+
+    static parse(value) {
+        return fromSql(value)
+    }
+}

+ 153 - 0
views/chatPdf.hbs

@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+    <head>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
+        <title>ChatPDF</title>
+        <script type="text/javascript" src="https://unpkg.com/jquery@3.3.1/dist/jquery.min.js"></script>
+        <script src="https://unpkg.com/eruda@3.0.0/eruda.js"></script>
+        <script src="https://cdn.tailwindcss.com"></script>
+        <script>
+            tailwind.config = {
+                corePlugins: {
+                    preflight: false
+                },
+                theme: {
+                    extend: {
+                        colors: {
+                            clifford: "#da373d"
+                        }
+                    }
+                }
+            };
+        </script>
+        <!-- Import style -->
+        <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
+        <!-- Import Vue 3 -->
+        <script src="//unpkg.com/vue@3"></script>
+        <!-- Import component library -->
+        <script src="//unpkg.com/element-plus"></script>
+        <script src="//unpkg.com/@element-plus/icons-vue"></script>
+        <style>
+            html,
+            body {
+                padding: 0;
+                margin: 0;
+            }
+            ::-webkit-scrollbar {
+                width: 0;
+                height: 0;
+                display: none;
+            }
+        </style>
+    </head>
+    <body class="h-full">
+        <div id="app" class="h-full">
+            <el-container class="h-full">
+                <el-main class="flex flex-col">
+                    <el-alert v-if="name" :title="fileName" type="success" effect="dark" @close="clear"></el-alert>
+                    <el-upload
+                        v-else
+                        class="upload-demo"
+                        drag
+                        v-model:file-list="fileList"
+                        action="/api/chat-pdf/upload"
+                        accept="application/pdf"
+                        :on-success="onSuccess"
+                    >
+                        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+                        <div class="el-upload__text">将文件拖入框内或点击此处上传</div>
+                    </el-upload>
+                    <div id="list" class="flex flex-col flex-1 overflow-auto">
+                        <div
+                            v-for="(item,i) in conversations"
+                            :key="i"
+                            class="mt-4 p-3 rounded-t-xl"
+                            :class="item.value.role==='system' ? 'mr-10 bg-slate-300	 rounded-r-xl' : 'ml-10 bg-neutral-200 rounded-l-xl'"
+                        >
+                            <el-icon v-if="item.value.loading" class="is-loading">
+                                <Loading />
+                            </el-icon>
+                            <span v-else>\{{item.value.content}}</span>
+                        </div>
+                    </div>
+                </el-main>
+                <el-footer class="py-4 flex items-end" @keyup.enter.stop.prevent="ask">
+                    <el-input v-model="q" :disabled="!name" placeholder="提问"></el-input>
+                    <el-button class="ml-3" type="primary" :disabled="!name" @click="ask">发送</el-button>
+                </el-footer>
+            </el-container>
+        </div>
+    </body>
+    <script>
+        var app = Vue.createApp({
+            data() {
+                return {
+                    name: "",
+                    fileName: "",
+                    fileList: [],
+                    q: "",
+                    conversations: []
+                };
+            },
+
+            methods: {
+                onSuccess(res, file) {
+                    console.log(res, file);
+                    this.name = res.name;
+                    this.fileName = file.name;
+                },
+                clear() {
+                    this.fileList = [];
+                    this.name = "";
+                    this.fileName = "";
+                    this.q = "";
+                    this.conversations = [];
+                },
+                ask(e) {
+                    console.log(this.q);
+                    if (this.q) {
+                        this.conversations.push(
+                            Vue.ref({
+                                role: "user",
+                                content: this.q
+                            })
+                        );
+                        var msg = Vue.ref({
+                            role: "system",
+                            loading: true,
+                            content: ""
+                        });
+                        this.conversations.push(msg);
+
+                        setTimeout(function () {
+                            $("#list").scrollTop($("#list")[0].scrollHeight);
+                        }, 50);
+                        $.post(
+                            "/api/chat-pdf/ask",
+                            {
+                                name: this.name,
+                                q: this.q
+                            },
+                            function (res) {
+                                console.log(res);
+                                msg.value.loading = false;
+                                msg.value.content = res.answer;
+                                setTimeout(function () {
+                                    $("#list").scrollTop($("#list")[0].scrollHeight);
+                                }, 50);
+                            }
+                        );
+                        this.q = "";
+                    }
+                }
+            }
+        });
+        app.use(ElementPlus);
+        for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+            app.component(key, component);
+        }
+        app.mount("#app");
+    </script>
+</html>

+ 185 - 14
yarn.lock

@@ -1297,6 +1297,13 @@
   resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz"
   integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==
 
+"@types/debug@^4.1.7":
+  version "4.1.8"
+  resolved "https://registry.npmmirror.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317"
+  integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==
+  dependencies:
+    "@types/ms" "*"
+
 "@types/eslint-scope@^3.7.3":
   version "3.7.4"
   resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz"
@@ -1393,6 +1400,11 @@
   resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz"
   integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
 
+"@types/ms@*":
+  version "0.7.31"
+  resolved "https://registry.npmmirror.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
+  integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
+
 "@types/multer@^1.4.7":
   version "1.4.7"
   resolved "https://registry.npmmirror.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e"
@@ -1488,6 +1500,11 @@
   resolved "https://registry.npmmirror.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6"
   integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==
 
+"@types/validator@^13.7.1":
+  version "13.7.17"
+  resolved "https://registry.npmmirror.com/@types/validator/-/validator-13.7.17.tgz#0a6d1510395065171e3378a4afc587a3aefa7cc1"
+  integrity sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==
+
 "@types/webidl-conversions@*":
   version "7.0.0"
   resolved "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7"
@@ -2009,6 +2026,11 @@ asynckit@^0.4.0:
   resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
   integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
 
+available-typed-arrays@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
+  integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+
 aws-sign2@~0.7.0:
   version "0.7.0"
   resolved "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -2297,7 +2319,7 @@ bytes@3.1.2:
   resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz"
   integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
 
-call-bind@^1.0.0:
+call-bind@^1.0.0, call-bind@^1.0.2:
   version "1.0.2"
   resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
   integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
@@ -2735,7 +2757,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
   dependencies:
     ms "2.0.0"
 
-debug@4, debug@4.x, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
+debug@4, debug@4.x, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
   version "4.3.4"
   resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
   integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -2758,7 +2780,7 @@ debug@^3.1.0:
 
 dedent@^0.7.0:
   version "0.7.0"
-  resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz"
+  resolved "https://registry.npmmirror.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
   integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==
 
 deep-is@^0.1.3, deep-is@~0.1.3:
@@ -2879,6 +2901,11 @@ dotenv@16.0.3, dotenv@^16.0.3:
   resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz"
   integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
 
+dottie@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.npmmirror.com/dottie/-/dottie-2.0.3.tgz#797a4f4c92a9a65499806be4051b9d9dcd5a5d77"
+  integrity sha512-4liA0PuRkZWQFQjwBypdxPfZaRWiv5tkhMXY2hzsa2pNf5s7U3m9cwUchfNKe8wZQxdGPQQzO6Rm2uGe0rvohQ==
+
 eastasianwidth@^0.2.0:
   version "0.2.0"
   resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@@ -3331,20 +3358,13 @@ fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1:
   resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz"
   integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
 
-fastq@^1.15.0:
+fastq@^1.15.0, fastq@^1.6.0:
   version "1.15.0"
   resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
   integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
   dependencies:
     reusify "^1.0.4"
 
-fastq@^1.6.0:
-  version "1.15.0"
-  resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz"
-  integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
-  dependencies:
-    reusify "^1.0.4"
-
 fb-watchman@^2.0.0:
   version "2.0.2"
   resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz"
@@ -3440,6 +3460,13 @@ follow-redirects@^1.14.8, follow-redirects@^1.15.0:
   resolved "https://registry.npmmirror.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.npmmirror.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+  integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+  dependencies:
+    is-callable "^1.1.3"
+
 foreachasync@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npmmirror.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6"
@@ -3626,6 +3653,16 @@ get-intrinsic@^1.0.2:
     has "^1.0.3"
     has-symbols "^1.0.3"
 
+get-intrinsic@^1.1.3:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
+  integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-proto "^1.0.1"
+    has-symbols "^1.0.3"
+
 get-package-type@^0.1.0:
   version "0.1.0"
   resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz"
@@ -3744,6 +3781,13 @@ globby@^11.1.0:
     merge2 "^1.4.1"
     slash "^3.0.0"
 
+gopd@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+  integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+  dependencies:
+    get-intrinsic "^1.1.3"
+
 graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
   version "4.2.10"
   resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz"
@@ -3794,11 +3838,23 @@ has-flag@^4.0.0:
   resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-symbols@^1.0.3:
+has-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
+  integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
+
+has-symbols@^1.0.2, has-symbols@^1.0.3:
   version "1.0.3"
   resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz"
   integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
 
+has-tostringtag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+  integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
+  dependencies:
+    has-symbols "^1.0.2"
+
 has-unicode@^2.0.1:
   version "2.0.1"
   resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz"
@@ -3941,6 +3997,11 @@ imurmurhash@^0.1.4:
   resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
   integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
 
+inflection@^1.13.2:
+  version "1.13.4"
+  resolved "https://registry.npmmirror.com/inflection/-/inflection-1.13.4.tgz#65aa696c4e2da6225b148d7a154c449366633a32"
+  integrity sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==
+
 inflight@^1.0.4:
   version "1.0.6"
   resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
@@ -4055,6 +4116,14 @@ ipaddr.js@1.9.1:
   resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
   integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
+is-arguments@^1.0.4:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
+  integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
@@ -4072,6 +4141,11 @@ is-buffer@~1.1.6:
   resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
+is-callable@^1.1.3:
+  version "1.2.7"
+  resolved "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
+  integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
+
 is-class-hotfix@~0.0.6:
   version "0.0.6"
   resolved "https://registry.npmmirror.com/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz#a527d31fb23279281dde5f385c77b5de70a72435"
@@ -4104,6 +4178,13 @@ is-generator-fn@^2.0.0:
   resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz"
   integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
 
+is-generator-function@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
+  integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
 is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
   version "4.0.3"
   resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
@@ -4150,6 +4231,17 @@ is-type-of@^1.0.0:
     is-class-hotfix "~0.0.6"
     isstream "~0.1.2"
 
+is-typed-array@^1.1.10, is-typed-array@^1.1.3:
+  version "1.1.10"
+  resolved "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f"
+  integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==
+  dependencies:
+    available-typed-arrays "^1.0.5"
+    call-bind "^1.0.2"
+    for-each "^0.3.3"
+    gopd "^1.0.1"
+    has-tostringtag "^1.0.0"
+
 is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -5111,6 +5203,18 @@ mkdirp@^2.1.3:
   resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.3.tgz"
   integrity sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==
 
+moment-timezone@^0.5.35:
+  version "0.5.43"
+  resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790"
+  integrity sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==
+  dependencies:
+    moment "^2.29.4"
+
+moment@^2.29.1, moment@^2.29.4:
+  version "2.29.4"
+  resolved "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
+  integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
+
 mongodb-connection-string-url@^2.6.0:
   version "2.6.0"
   resolved "https://registry.npmmirror.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf"
@@ -5702,7 +5806,7 @@ pg-cloudflare@^1.1.0:
   resolved "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz#833d70870d610d14bf9df7afb40e1cba310c17a0"
   integrity sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==
 
-pg-connection-string@^2.6.0:
+pg-connection-string@^2.5.0, pg-connection-string@^2.6.0:
   version "2.6.0"
   resolved "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.6.0.tgz#12a36cc4627df19c25cc1b9b736cc39ee1f73ae8"
   integrity sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==
@@ -6160,6 +6264,11 @@ restore-cursor@^4.0.0:
     onetime "^5.1.0"
     signal-exit "^3.0.2"
 
+retry-as-promised@^7.0.3:
+  version "7.0.4"
+  resolved "https://registry.npmmirror.com/retry-as-promised/-/retry-as-promised-7.0.4.tgz#9df73adaeea08cb2948b9d34990549dc13d800a2"
+  integrity sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==
+
 reusify@^1.0.4:
   version "1.0.4"
   resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz"
@@ -6294,6 +6403,33 @@ seq-queue@^0.0.5:
   resolved "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz"
   integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
 
+sequelize-pool@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.npmmirror.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz#210b391af4002762f823188fd6ecfc7413020768"
+  integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==
+
+sequelize@^6.31.1:
+  version "6.31.1"
+  resolved "https://registry.npmmirror.com/sequelize/-/sequelize-6.31.1.tgz#374bad3f0b8a9ba6a4ef15079502b4821dea1931"
+  integrity sha512-cahWtRrYLjqoZP/aurGBoaxn29qQCF4bxkAUPEQ/ozjJjt6mtL4Q113S3N39mQRmX5fgxRbli+bzZARP/N51eg==
+  dependencies:
+    "@types/debug" "^4.1.7"
+    "@types/validator" "^13.7.1"
+    debug "^4.3.3"
+    dottie "^2.0.2"
+    inflection "^1.13.2"
+    lodash "^4.17.21"
+    moment "^2.29.1"
+    moment-timezone "^0.5.35"
+    pg-connection-string "^2.5.0"
+    retry-as-promised "^7.0.3"
+    semver "^7.3.5"
+    sequelize-pool "^7.1.0"
+    toposort-class "^1.0.1"
+    uuid "^8.3.2"
+    validator "^13.7.0"
+    wkx "^0.5.0"
+
 serialize-javascript@^6.0.0:
   version "6.0.1"
   resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz"
@@ -6787,6 +6923,11 @@ toidentifier@1.0.1:
   resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
+toposort-class@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988"
+  integrity sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==
+
 toposort@^2.0.2:
   version "2.0.2"
   resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz"
@@ -7079,6 +7220,17 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
   integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
 
+util@^0.12.5:
+  version "0.12.5"
+  resolved "https://registry.npmmirror.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
+  integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
+  dependencies:
+    inherits "^2.0.3"
+    is-arguments "^1.0.4"
+    is-generator-function "^1.0.7"
+    is-typed-array "^1.1.3"
+    which-typed-array "^1.1.2"
+
 utility@^1.16.1, utility@^1.17.0, utility@^1.8.0:
   version "1.17.0"
   resolved "https://registry.npmmirror.com/utility/-/utility-1.17.0.tgz#60819f712a6e0ce774f52fb1d691992a5f59d362"
@@ -7095,7 +7247,7 @@ utils-merge@1.0.1, utils-merge@^1.0.1:
   resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
   integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
 
-uuid@8.3.2:
+uuid@8.3.2, uuid@^8.3.2:
   version "8.3.2"
   resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
@@ -7251,6 +7403,18 @@ whatwg-url@^5.0.0:
     tr46 "~0.0.3"
     webidl-conversions "^3.0.0"
 
+which-typed-array@^1.1.2:
+  version "1.1.9"
+  resolved "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"
+  integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==
+  dependencies:
+    available-typed-arrays "^1.0.5"
+    call-bind "^1.0.2"
+    for-each "^0.3.3"
+    gopd "^1.0.1"
+    has-tostringtag "^1.0.0"
+    is-typed-array "^1.1.10"
+
 which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
@@ -7279,6 +7443,13 @@ windows-release@^4.0.0:
   dependencies:
     execa "^4.0.2"
 
+wkx@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.npmmirror.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c"
+  integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==
+  dependencies:
+    "@types/node" "*"
+
 word-wrap@^1.2.3, word-wrap@~1.2.3:
   version "1.2.3"
   resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"