From 04ce1306b76be94bc2e54151af4811c5d15c99c6 Mon Sep 17 00:00:00 2001 From: monoid Date: Fri, 29 Mar 2024 00:19:36 +0900 Subject: [PATCH] use kysely --- dprint.json | 23 - packages/dbtype/types.ts | 2 +- packages/server/app.ts | 148 +--- packages/server/comic_config.json | 3 + packages/server/knexfile.js | 5 - packages/server/package.json | 16 +- packages/server/src/SettingConfig.ts | 11 +- packages/server/src/config.ts | 2 +- packages/server/src/content/comic.ts | 8 +- packages/server/src/content/file.ts | 24 +- packages/server/src/content/video.ts | 5 +- packages/server/src/database.ts | 37 +- packages/server/src/db/doc.ts | 345 ++++----- packages/server/src/db/kysely.ts | 26 + packages/server/src/db/plugin.ts | 24 + packages/server/src/db/tag.ts | 90 +-- packages/server/src/db/user.ts | 121 ++- packages/server/src/diff/content_handler.ts | 6 +- packages/server/src/diff/content_list.ts | 2 +- packages/server/src/diff/diff.ts | 4 +- packages/server/src/diff/router.ts | 10 +- packages/server/src/diff/watcher.ts | 10 +- .../server/src/diff/watcher/comic_watcher.ts | 3 - .../server/src/diff/watcher/common_watcher.ts | 10 +- .../server/src/diff/watcher/compositer.ts | 6 +- .../src/diff/watcher/recursive_watcher.ts | 11 +- packages/server/src/diff/watcher/util.ts | 13 +- .../server/src/diff/watcher/watcher_filter.ts | 16 +- packages/server/src/login.ts | 38 +- packages/server/src/model/doc.ts | 11 +- packages/server/src/model/tag.ts | 2 +- packages/server/src/model/user.ts | 2 +- packages/server/src/permission/permission.ts | 10 +- packages/server/src/route/all.ts | 21 +- packages/server/src/route/comic.ts | 15 +- packages/server/src/route/contents.ts | 72 +- packages/server/src/route/error_handler.ts | 2 +- packages/server/src/route/tags.ts | 12 +- packages/server/src/route/util.ts | 10 +- packages/server/src/route/video.ts | 12 +- packages/server/src/server.ts | 34 +- packages/server/src/types/db.d.ts | 34 - packages/server/src/util/configRW.ts | 4 +- packages/server/src/util/type_check.ts | 8 +- packages/server/src/util/zipwrap.ts | 4 +- packages/server/tsconfig.json | 2 +- pnpm-lock.yaml | 709 +++++++++++++++++- 47 files changed, 1236 insertions(+), 747 deletions(-) delete mode 100644 dprint.json create mode 100644 packages/server/comic_config.json delete mode 100644 packages/server/knexfile.js create mode 100644 packages/server/src/db/kysely.ts create mode 100644 packages/server/src/db/plugin.ts delete mode 100644 packages/server/src/types/db.d.ts diff --git a/dprint.json b/dprint.json deleted file mode 100644 index 3911cdd..0000000 --- a/dprint.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "incremental": true, - "typescript": { - "indentWidth": 2 - }, - "json": { - }, - "markdown": { - }, - "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json,md}"], - "excludes": [ - "**/node_modules", - "**/*-lock.json", - "**/dist", - "build/", - "app/" - ], - "plugins": [ - "https://plugins.dprint.dev/typescript-0.84.4.wasm", - "https://plugins.dprint.dev/json-0.17.2.wasm", - "https://plugins.dprint.dev/markdown-0.15.2.wasm" - ] -} diff --git a/packages/dbtype/types.ts b/packages/dbtype/types.ts index 7a7beaf..3ea5ac5 100644 --- a/packages/dbtype/types.ts +++ b/packages/dbtype/types.ts @@ -34,7 +34,7 @@ export interface SchemaMigration { export interface Tags { description: string | null; - name: string | null; + name: string; } export interface Users { diff --git a/packages/server/app.ts b/packages/server/app.ts index 687a4c1..baf6dea 100644 --- a/packages/server/app.ts +++ b/packages/server/app.ts @@ -1,145 +1,7 @@ -import { app, BrowserWindow, dialog, session } from "electron"; -import { ipcMain } from "electron"; -import { join } from "path"; -import { accessTokenName, getAdminAccessTokenValue, getAdminRefreshTokenValue, refreshTokenName } from "./src/login"; -import { UserAccessor } from "./src/model/mod"; import { create_server } from "./src/server"; -import { get_setting } from "./src/SettingConfig"; -function registerChannel(cntr: UserAccessor) { - ipcMain.handle("reset_password", async (event, username: string, password: string) => { - const user = await cntr.findUser(username); - if (user === undefined) { - return false; - } - user.reset_password(password); - return true; - }); -} -const setting = get_setting(); -if (!setting.cli) { - let wnd: BrowserWindow | null = null; - - const createWindow = async () => { - wnd = new BrowserWindow({ - width: 800, - height: 600, - center: true, - useContentSize: true, - webPreferences: { - preload: join(__dirname, "preload.js"), - contextIsolation: true, - }, - }); - await wnd.loadURL(`data:text/html;base64,` + Buffer.from(loading_html).toString("base64")); - // await wnd.loadURL('../loading.html'); - // set admin cookies. - await session.defaultSession.cookies.set({ - url: `http://localhost:${setting.port}`, - name: accessTokenName, - value: getAdminAccessTokenValue(), - httpOnly: true, - secure: false, - sameSite: "strict", - }); - await session.defaultSession.cookies.set({ - url: `http://localhost:${setting.port}`, - name: refreshTokenName, - value: getAdminRefreshTokenValue(), - httpOnly: true, - secure: false, - sameSite: "strict", - }); - try { - const server = await create_server(); - const app = server.start_server(); - registerChannel(server.userController); - await wnd.loadURL(`http://localhost:${setting.port}`); - } catch (e) { - if (e instanceof Error) { - await dialog.showMessageBox({ - type: "error", - title: "error!", - message: e.message, - }); - } else { - await dialog.showMessageBox({ - type: "error", - title: "error!", - message: String(e), - }); - } - } - wnd.on("closed", () => { - wnd = null; - }); - }; - - const isPrimary = app.requestSingleInstanceLock(); - if (!isPrimary) { - app.quit(); // exit window - app.exit(); - } - app.on("second-instance", () => { - if (wnd != null) { - if (wnd.isMinimized()) { - wnd.restore(); - } - wnd.focus(); - } - }); - app.on("ready", (event, info) => { - createWindow(); - }); - - app.on("window-all-closed", () => { - // quit when all windows are closed - if (process.platform != "darwin") app.quit(); // (except leave MacOS app active until Cmd+Q) - }); - - app.on("activate", () => { - // re-recreate window when dock icon is clicked and no other windows open - if (wnd == null) createWindow(); - }); -} else { - (async () => { - try { - const server = await create_server(); - server.start_server(); - } catch (error) { - console.log(error); - } - })(); -} -const loading_html = ` - - -loading - - - - - -

Loading...

-
- -`; +create_server().then((server) => { + server.start_server(); +}).catch((err) => { + console.error(err); +}); \ No newline at end of file diff --git a/packages/server/comic_config.json b/packages/server/comic_config.json new file mode 100644 index 0000000..e9692d3 --- /dev/null +++ b/packages/server/comic_config.json @@ -0,0 +1,3 @@ +{ + "watch": [] +} \ No newline at end of file diff --git a/packages/server/knexfile.js b/packages/server/knexfile.js deleted file mode 100644 index 76f47c9..0000000 --- a/packages/server/knexfile.js +++ /dev/null @@ -1,5 +0,0 @@ -require("ts-node").register(); -const { Knex } = require("./src/config"); -// Update with your config settings. - -module.exports = Knex.config; diff --git a/packages/server/package.json b/packages/server/package.json index 0215d59..d24bf18 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -4,10 +4,8 @@ "description": "", "main": "build/app.js", "scripts": { - "compile": "tsc", - "compile:watch": "tsc -w", - "build": "cd src/client && pnpm run build:prod", - "build:watch": "cd src/client && pnpm run build:watch", + "compile": "swc src --out-dir dist", + "dev": "nodemon -r @swc-node/register --exec node app.ts", "start": "node build/app.js" }, "author": "", @@ -16,6 +14,7 @@ "@zip.js/zip.js": "^2.7.40", "better-sqlite3": "^9.4.3", "chokidar": "^3.6.0", + "dotenv": "^16.4.5", "jsonwebtoken": "^8.5.1", "koa": "^2.15.2", "koa-bodyparser": "^4.4.1", @@ -26,13 +25,18 @@ "tiny-async-pool": "^1.3.0" }, "devDependencies": { - "dbtype": "*", + "@swc-node/register": "^1.9.0", + "@swc/cli": "^0.3.10", + "@swc/core": "^1.4.11", + "@types/better-sqlite3": "^7.6.9", "@types/jsonwebtoken": "^8.5.9", "@types/koa": "^2.15.0", "@types/koa-bodyparser": "^4.3.12", "@types/koa-compose": "^3.2.8", "@types/koa-router": "^7.4.8", "@types/node": "^14.18.63", - "@types/tiny-async-pool": "^1.0.5" + "@types/tiny-async-pool": "^1.0.5", + "dbtype": "workspace:^", + "nodemon": "^3.1.0" } } diff --git a/packages/server/src/SettingConfig.ts b/packages/server/src/SettingConfig.ts index f012079..a59944e 100644 --- a/packages/server/src/SettingConfig.ts +++ b/packages/server/src/SettingConfig.ts @@ -1,6 +1,6 @@ -import { randomBytes } from "crypto"; -import { existsSync, readFileSync, writeFileSync } from "fs"; -import { Permission } from "./permission/permission"; +import { randomBytes } from "node:crypto"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import type { Permission } from "./permission/permission"; export interface SettingConfig { /** @@ -46,6 +46,7 @@ const default_setting: SettingConfig = { }; let setting: null | SettingConfig = null; +// biome-ignore lint/suspicious/noExplicitAny: const setEmptyToDefault = (target: any, default_table: SettingConfig) => { let diff_occur = false; for (const key in default_table) { @@ -59,7 +60,7 @@ const setEmptyToDefault = (target: any, default_table: SettingConfig) => { }; export const read_setting_from_file = () => { - let ret = existsSync("settings.json") ? JSON.parse(readFileSync("settings.json", { encoding: "utf8" })) : {}; + const ret = existsSync("settings.json") ? JSON.parse(readFileSync("settings.json", { encoding: "utf8" })) : {}; const partial_occur = setEmptyToDefault(ret, default_setting); if (partial_occur) { writeFileSync("settings.json", JSON.stringify(ret)); @@ -70,7 +71,7 @@ export function get_setting(): SettingConfig { if (setting === null) { setting = read_setting_from_file(); const env = process.env.NODE_ENV; - if (env !== undefined && env != "production" && env != "development") { + if (env !== undefined && env !== "production" && env !== "development") { throw new Error('process unknown value in NODE_ENV: must be either "development" or "production"'); } setting.mode = env ?? setting.mode; diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index dcf3463..6807e05 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -1,4 +1,4 @@ -import { Knex as k } from "knex"; +import type { Knex as k } from "knex"; export namespace Knex { export const config: { diff --git a/packages/server/src/content/comic.ts b/packages/server/src/content/comic.ts index 1b241c7..e623c36 100644 --- a/packages/server/src/content/comic.ts +++ b/packages/server/src/content/comic.ts @@ -1,7 +1,7 @@ -import { extname } from "path"; -import { DocumentBody } from "../model/doc"; +import { extname } from "node:path"; +import type { DocumentBody } from "../model/doc"; import { readAllFromZip, readZip } from "../util/zipwrap"; -import { ContentConstructOption, ContentFile, createDefaultClass, registerContentReferrer } from "./file"; +import { type ContentConstructOption, createDefaultClass, registerContentReferrer } from "./file"; type ComicType = "doujinshi" | "artist cg" | "manga" | "western"; interface ComicDesc { @@ -51,7 +51,7 @@ export class ComicReferrer extends createDefaultClass("comic") { tags = tags.concat(this.desc.character?.map((x) => `character:${x}`) ?? []); tags = tags.concat(this.desc.group?.map((x) => `group:${x}`) ?? []); tags = tags.concat(this.desc.series?.map((x) => `series:${x}`) ?? []); - const type = this.desc.type instanceof Array ? this.desc.type[0] : this.desc.type; + const type = Array.isArray(this.desc.type) ? this.desc.type[0] : this.desc.type; tags.push(`type:${type}`); return { ...basebody, diff --git a/packages/server/src/content/file.ts b/packages/server/src/content/file.ts index 350248e..681a348 100644 --- a/packages/server/src/content/file.ts +++ b/packages/server/src/content/file.ts @@ -1,10 +1,9 @@ -import { createHash } from "crypto"; -import { promises, Stats } from "fs"; +import { createHash } from "node:crypto"; +import { promises, type Stats } from "node:fs"; import { Context, DefaultContext, DefaultState, Middleware, Next } from "koa"; import Router from "koa-router"; -import { extname } from "path"; -import path from "path"; -import { DocumentBody } from "../model/mod"; +import path, { extname } from "node:path"; +import type { DocumentBody } from "../model/mod"; /** * content file or directory referrer */ @@ -24,13 +23,17 @@ type ContentFileConstructor = (new ( content_type: string; }; export const createDefaultClass = (type: string): ContentFileConstructor => { - let cons = class implements ContentFile { + const cons = class implements ContentFile { readonly path: string; // type = type; static content_type = type; protected hash: string | undefined; protected stat: Stats | undefined; + protected getStat(){ + return this.stat; + } + constructor(path: string, option?: ContentConstructOption) { this.path = path; this.hash = option?.hash; @@ -67,14 +70,17 @@ export const createDefaultClass = (type: string): ContentFileConstructor => { return this.hash; } async getMtime(): Promise { - if (this.stat !== undefined) return this.stat.mtimeMs; + const oldStat = this.getStat(); + if (oldStat !== undefined) return oldStat.mtimeMs; await this.getHash(); - return this.stat!.mtimeMs; + const newStat = this.getStat(); + if (newStat === undefined) throw new Error("stat is undefined"); + return newStat.mtimeMs; } }; return cons; }; -let ContstructorTable: { [k: string]: ContentFileConstructor } = {}; +const ContstructorTable: { [k: string]: ContentFileConstructor } = {}; export function registerContentReferrer(s: ContentFileConstructor) { console.log(`registered content type: ${s.content_type}`); ContstructorTable[s.content_type] = s; diff --git a/packages/server/src/content/video.ts b/packages/server/src/content/video.ts index 46c7548..c26cfd5 100644 --- a/packages/server/src/content/video.ts +++ b/packages/server/src/content/video.ts @@ -1,9 +1,6 @@ -import { ContentConstructOption, ContentFile, registerContentReferrer } from "./file"; +import { registerContentReferrer } from "./file"; import { createDefaultClass } from "./file"; export class VideoReferrer extends createDefaultClass("video") { - constructor(path: string, desc?: ContentConstructOption) { - super(path, desc); - } } registerContentReferrer(VideoReferrer); diff --git a/packages/server/src/database.ts b/packages/server/src/database.ts index fc681d9..997fe9f 100644 --- a/packages/server/src/database.ts +++ b/packages/server/src/database.ts @@ -1,47 +1,26 @@ -import { existsSync } from "fs"; -import Knex from "knex"; -import { Knex as KnexConfig } from "./config"; +import { existsSync } from "node:fs"; import { get_setting } from "./SettingConfig"; +import { getKysely } from "./db/kysely"; export async function connectDB() { - const env = get_setting().mode; - const config = KnexConfig.config[env]; - if (!config.connection) { - throw new Error("connection options required."); - } - const connection = config.connection; - if (typeof connection === "string") { - throw new Error("unknown connection options"); - } - if (typeof connection === "function") { - throw new Error("connection provider not supported..."); - } - if (!("filename" in connection)) { - throw new Error("sqlite3 config need"); - } - const init_need = !existsSync(connection.filename); - const knex = Knex(config); + const kysely = getKysely(); + let tries = 0; for (;;) { try { console.log("try to connect db"); - await knex.raw("select 1 + 1;"); + await kysely.selectNoFrom(eb=> eb.val(1).as("dummy")).execute(); console.log("connect success"); } catch (err) { if (tries < 3) { tries++; console.error(`connection fail ${err} retry...`); + await new Promise((resolve) => setTimeout(resolve, 1000)); continue; - } else { - throw err; } + throw err; } break; } - if (init_need) { - console.log("first execute: initialize database..."); - const migrate = await import("../migrations/initial"); - await migrate.up(knex); - } - return knex; + return kysely; } diff --git a/packages/server/src/db/doc.ts b/packages/server/src/db/doc.ts index 544fc0c..b89a7d3 100644 --- a/packages/server/src/db/doc.ts +++ b/packages/server/src/db/doc.ts @@ -1,235 +1,222 @@ -import { Knex } from "knex"; -import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc"; -import { TagAccessor } from "../model/tag"; -import { createKnexTagController } from "./tag"; +import { getKysely } from "./kysely"; +import { jsonArrayFrom } from "kysely/helpers/sqlite"; +import type { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc"; +import { ParseJSONResultsPlugin, type NotNull } from "kysely"; +import { MyParseJSONResultsPlugin } from "./plugin"; export type DBTagContentRelation = { doc_id: number; tag_name: string; }; -class KnexDocumentAccessor implements DocumentAccessor { - knex: Knex; - tagController: TagAccessor; - constructor(knex: Knex) { - this.knex = knex; - this.tagController = createKnexTagController(knex); +class SqliteDocumentAccessor implements DocumentAccessor { + constructor(private kysely = getKysely()) { } async search(search_word: string): Promise { throw new Error("Method not implemented."); - const sw = `%${search_word}%`; - const docs = await this.knex.select("*").from("document").where("title", "like", sw); - return docs; } async addList(content_list: DocumentBody[]): Promise { - return await this.knex.transaction(async (trx) => { + return await this.kysely.transaction().execute(async (trx) => { // add tags const tagCollected = new Set(); - content_list - .map((x) => x.tags) - .forEach((x) => { - x.forEach((x) => { - tagCollected.add(x); - }); - }); - const tagCollectPromiseList = []; - const tagController = createKnexTagController(trx); - for (const it of tagCollected) { - const p = tagController.addTag({ name: it }); - tagCollectPromiseList.push(p); - } - await Promise.all(tagCollectPromiseList); - // add for each contents - const ret = []; for (const content of content_list) { - const { tags, additional, ...rest } = content; - const id_lst = await trx - .insert({ + for (const tag of content.tags) { + tagCollected.add(tag); + } + } + await trx.insertInto("tags") + .values(Array.from(tagCollected).map((x) => ({ name: x }))) + .onConflict((oc) => oc.doNothing()) + .execute(); + + const ids = await trx.insertInto("document") + .values(content_list.map((content) => { + const { tags, additional, ...rest } = content; + return { additional: JSON.stringify(additional), created_at: Date.now(), ...rest, - }) - .into("document"); - const id = id_lst[0]; - if (tags.length > 0) { - await trx - .insert( - tags.map((y) => ({ - doc_id: id, - tag_name: y, - })), - ) - .into("doc_tag_relation"); - } - ret.push(id); - } - return ret; + }; + })) + .returning("id") + .execute(); + const id_lst = ids.map((x) => x.id); + + const doc_tags = content_list.flatMap((content, index) => { + const { tags, ...rest } = content; + return tags.map((tag) => ({ doc_id: id_lst[index], tag_name: tag })); + }); + await trx.insertInto("doc_tag_relation") + .values(doc_tags) + .execute(); + return id_lst; }); } async add(c: DocumentBody) { - const { tags, additional, ...rest } = c; - const id_lst = await this.knex - .insert({ - additional: JSON.stringify(additional), - created_at: Date.now(), - ...rest, - }) - .into("document"); - const id = id_lst[0]; - for (const it of tags) { - this.tagController.addTag({ name: it }); - } - if (tags.length > 0) { - await this.knex - .insert(tags.map((x) => ({ doc_id: id, tag_name: x }))) - .into("doc_tag_relation"); - } - return id; + return await this.kysely.transaction().execute(async (trx) => { + const { tags, additional, ...rest } = c; + const id_lst = await trx.insertInto("document").values({ + additional: JSON.stringify(additional), + created_at: Date.now(), + ...rest, + }) + .returning("id") + .executeTakeFirst() as { id: number }; + const id = id_lst.id; + + // add tags + await trx.insertInto("tags") + .values(tags.map((x) => ({ name: x }))) + // on conflict is supported in sqlite and postgresql. + .onConflict((oc) => oc.doNothing()); + + if (tags.length > 0) { + await trx.insertInto("doc_tag_relation") + .values(tags.map((x) => ({ doc_id: id, tag_name: x }))); + } + return id; + }); } async del(id: number) { - if ((await this.findById(id)) !== undefined) { - await this.knex.delete().from("doc_tag_relation").where({ doc_id: id }); - await this.knex.delete().from("document").where({ id: id }); - return true; - } - return false; + // delete tags + await this.kysely + .deleteFrom("doc_tag_relation") + .where("doc_id", "=", id) + .execute(); + // delete document + const result = await this.kysely + .deleteFrom("document") + .where("id", "=", id) + .executeTakeFirst(); + return result.numDeletedRows > 0; } async findById(id: number, tagload?: boolean): Promise { - const s = await this.knex.select("*").from("document").where({ id: id }); - if (s.length === 0) return undefined; - const first = s[0]; - let ret_tags: string[] = []; - if (tagload === true) { - const tags: DBTagContentRelation[] = await this.knex - .select("*") - .from("doc_tag_relation") - .where({ doc_id: first.id }); - ret_tags = tags.map((x) => x.tag_name); - } + const doc = await this.kysely.selectFrom("document") + .selectAll() + .where("id", "=", id) + .$if(tagload ?? false, (qb) => + qb.select(eb => jsonArrayFrom( + eb.selectFrom("doc_tag_relation") + .select(["doc_tag_relation.tag_name"]) + .whereRef("document.id", "=", "doc_tag_relation.doc_id") + .select("tag_name") + ).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags")) + ) + .executeTakeFirst(); + if (!doc) return undefined; + return { - ...first, - tags: ret_tags, - additional: first.additional !== null ? JSON.parse(first.additional) : {}, + ...doc, + content_hash: doc.content_hash ?? "", + additional: doc.additional !== null ? JSON.parse(doc.additional) : {}, + tags: doc.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [], }; } async findDeleted(content_type: string) { - const s = await this.knex - .select("*") - .where({ content_type: content_type }) - .whereNotNull("update_at") - .from("document"); - return s.map((x) => ({ + const docs = await this.kysely + .selectFrom("document") + .selectAll() + .where("content_type", "=", content_type) + .where("deleted_at", "is not", null) + .$narrowType<{ deleted_at: NotNull }>() + .execute(); + return docs.map((x) => ({ ...x, tags: [], + content_hash: x.content_hash ?? "", additional: {}, })); } async findList(option?: QueryListOption) { - option = option ?? {}; - const allow_tag = option.allow_tag ?? []; - const eager_loading = option.eager_loading ?? true; - const limit = option.limit ?? 20; - const use_offset = option.use_offset ?? false; - const offset = option.offset ?? 0; - const word = option.word; - const content_type = option.content_type; - const cursor = option.cursor; + const { + allow_tag = [], + eager_loading = true, + limit = 20, + use_offset = false, + offset = 0, + word, + content_type, + cursor, + } = option ?? {}; - const buildquery = () => { - let query = this.knex.select("document.*"); - if (allow_tag.length > 0) { - query = query.from("doc_tag_relation as tags_0"); - query = query.where("tags_0.tag_name", "=", allow_tag[0]); - for (let index = 1; index < allow_tag.length; index++) { - const element = allow_tag[index]; - query = query.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.doc_id`, "tags_0.doc_id"); - query = query.where(`tags_${index}.tag_name`, "=", element); - } - query = query.innerJoin("document", "tags_0.doc_id", "document.id"); - } else { - query = query.from("document"); - } - if (word !== undefined) { - // don't worry about sql injection. - query = query.where("title", "like", `%${word}%`); - } - if (content_type !== undefined) { - query = query.where("content_type", "=", content_type); - } - if (use_offset) { - query = query.offset(offset); - } else { - if (cursor !== undefined) { - query = query.where("id", "<", cursor); - } - } - query = query.limit(limit); - query = query.orderBy("id", "desc"); - return query; - }; - let query = buildquery(); - // console.log(query.toSQL()); - let result: Document[] = await query; - for (let i of result) { - i.additional = JSON.parse(i.additional as unknown as string); - } - if (eager_loading) { - let idmap: { [index: number]: Document } = {}; - for (const r of result) { - idmap[r.id] = r; - r.tags = []; - } - let subquery = buildquery(); - let tagquery = this.knex - .select("id", "doc_tag_relation.tag_name") - .from(subquery) - .innerJoin("doc_tag_relation", "doc_tag_relation.doc_id", "id"); - // console.log(tagquery.toSQL()); - let tagresult: { id: number; tag_name: string }[] = await tagquery; - for (const { id, tag_name } of tagresult) { - idmap[id].tags.push(tag_name); - } - } else { - result.forEach((v) => { - v.tags = []; - }); - } - return result; + const result = await this.kysely + .selectFrom("document") + .selectAll() + .$if(allow_tag.length > 0, (qb) => { + return allow_tag.reduce((prevQb ,tag, index) => { + return prevQb.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.tag_name`, "document.id") + .where(`tags_${index}.tag_name`, "=", tag); + }, qb) as unknown as typeof qb; + }) + .$if(word !== undefined, (qb) => qb.where("title", "like", `%${word}%`)) + .$if(content_type !== undefined, (qb) => qb.where("content_type", "=", content_type as string)) + .$if(use_offset, (qb) => qb.offset(offset)) + .$if(!use_offset && cursor !== undefined, (qb) => qb.where("id", "<", cursor as number)) + .limit(limit) + .$if(eager_loading, (qb) => { + return qb.select(eb => jsonArrayFrom( + eb.selectFrom("doc_tag_relation") + .select(["doc_tag_relation.tag_name"]) + .whereRef("document.id", "=", "doc_tag_relation.doc_id") + ).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags")) + }) + .orderBy("id", "desc") + .execute(); + return result.map((x) => ({ + ...x, + content_hash: x.content_hash ?? "", + additional: x.additional !== null ? (JSON.parse(x.additional)) : {}, + tags: x.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [], + })); } async findByPath(path: string, filename?: string): Promise { - const e = filename == undefined ? {} : { filename: filename }; - const results = await this.knex - .select("*") - .from("document") - .where({ basepath: path, ...e }); + const results = await this.kysely + .selectFrom("document") + .selectAll() + .where("basepath", "=", path) + .$if(filename !== undefined, (qb) => qb.where("filename", "=", filename as string)) + .execute(); return results.map((x) => ({ ...x, + content_hash: x.content_hash ?? "", tags: [], additional: {}, })); } async update(c: Partial & { id: number }) { - const { id, tags, ...rest } = c; - if ((await this.findById(id)) !== undefined) { - await this.knex.update(rest).where({ id: id }).from("document"); - return true; - } - return false; + const { id, tags, additional, ...rest } = c; + const r = await this.kysely.updateTable("document") + .set({ + ...rest, + modified_at: Date.now(), + additional: additional !== undefined ? JSON.stringify(additional) : undefined, + }) + .where("id", "=", id) + .executeTakeFirst(); + return r.numUpdatedRows > 0; } async addTag(c: Document, tag_name: string) { if (c.tags.includes(tag_name)) return false; - this.tagController.addTag({ name: tag_name }); - await this.knex.insert({ tag_name: tag_name, doc_id: c.id }).into("doc_tag_relation"); + await this.kysely.insertInto("tags") + .values({ name: tag_name }) + .onConflict((oc) => oc.doNothing()) + .execute(); + await this.kysely.insertInto("doc_tag_relation") + .values({ tag_name: tag_name, doc_id: c.id }) + .execute(); c.tags.push(tag_name); return true; } async delTag(c: Document, tag_name: string) { if (c.tags.includes(tag_name)) return false; - await this.knex.delete().where({ tag_name: tag_name, doc_id: c.id }).from("doc_tag_relation"); - c.tags.push(tag_name); + await this.kysely.deleteFrom("doc_tag_relation") + .where("tag_name", "=", tag_name) + .where("doc_id", "=", c.id) + .execute(); + c.tags.splice(c.tags.indexOf(tag_name), 1); return true; } } -export const createKnexDocumentAccessor = (knex: Knex): DocumentAccessor => { - return new KnexDocumentAccessor(knex); +export const createSqliteDocumentAccessor = (kysely = getKysely()): DocumentAccessor => { + return new SqliteDocumentAccessor(kysely); }; diff --git a/packages/server/src/db/kysely.ts b/packages/server/src/db/kysely.ts new file mode 100644 index 0000000..31e1ba2 --- /dev/null +++ b/packages/server/src/db/kysely.ts @@ -0,0 +1,26 @@ +import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely"; +import SqliteDatabase from "better-sqlite3"; +import type { DB } from "dbtype/types"; + +export function createSqliteDialect() { + const url = process.env.DATABASE_URL; + if (!url) { + throw new Error("DATABASE_URL is not set"); + } + const db = new SqliteDatabase(url); + return new SqliteDialect({ + database: db, + }); +} + +// Create a new Kysely instance with a new SqliteDatabase instance +let kysely: Kysely | null = null; +export function getKysely() { + if (!kysely) { + kysely = new Kysely({ + dialect: createSqliteDialect(), + // plugins: [new ParseJSONResultsPlugin()], + }); + } + return kysely; +} \ No newline at end of file diff --git a/packages/server/src/db/plugin.ts b/packages/server/src/db/plugin.ts new file mode 100644 index 0000000..5574973 --- /dev/null +++ b/packages/server/src/db/plugin.ts @@ -0,0 +1,24 @@ +import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs, QueryResult, RootOperationNode, UnknownRow } from "kysely"; + +export class MyParseJSONResultsPlugin implements KyselyPlugin { + + constructor(private readonly itemPath: string) { } + + transformQuery(args: PluginTransformQueryArgs): RootOperationNode { + // do nothing + return args.node; + } + async transformResult(args: PluginTransformResultArgs): Promise> { + return { + ...args.result, + rows: args.result.rows.map((row) => { + const newRow = { ...row }; + const item = newRow[this.itemPath]; + if (typeof item === "string") { + newRow[this.itemPath] = JSON.parse(item); + } + return newRow; + }) + } + } +} \ No newline at end of file diff --git a/packages/server/src/db/tag.ts b/packages/server/src/db/tag.ts index 6e1badc..35ff2cf 100644 --- a/packages/server/src/db/tag.ts +++ b/packages/server/src/db/tag.ts @@ -1,61 +1,65 @@ -import { Knex } from "knex"; -import { Tag, TagAccessor, TagCount } from "../model/tag"; -import { DBTagContentRelation } from "./doc"; +import { getKysely } from "./kysely"; +import { jsonArrayFrom } from "kysely/helpers/sqlite"; +import type { Tag, TagAccessor, TagCount } from "../model/tag"; +import type { DBTagContentRelation } from "./doc"; -type DBTags = { - name: string; - description?: string; -}; +class SqliteTagAccessor implements TagAccessor { -class KnexTagAccessor implements TagAccessor { - knex: Knex; - constructor(knex: Knex) { - this.knex = knex; + constructor(private kysely = getKysely()) { } async getAllTagCount(): Promise { - const result = await this.knex("doc_tag_relation") + const result = await this.kysely + .selectFrom("doc_tag_relation") .select("tag_name") - .count("*", { as: "occurs" }) - .groupBy("tag_name"); + .select(qb => qb.fn.count("doc_id").as("occurs")) + .groupBy("tag_name") + .execute(); return result; } - async getAllTagList(onlyname?: boolean) { - onlyname = onlyname ?? false; - const t: DBTags[] = await this.knex.select(onlyname ? "*" : "name").from("tags"); - return t; + async getAllTagList(): Promise { + return (await this.kysely.selectFrom("tags") + .selectAll() + .execute() + ).map((x) => ({ + name: x.name, + description: x.description ?? undefined, + })); } async getTagByName(name: string) { - const t: DBTags[] = await this.knex.select("*").from("tags").where({ name: name }); - if (t.length === 0) return undefined; - return t[0]; + const result = await this.kysely + .selectFrom("tags") + .selectAll() + .where("name", "=", name) + .executeTakeFirst(); + if (result === undefined) { + return undefined; + } + return { + name: result.name, + description: result.description ?? undefined, + }; } async addTag(tag: Tag) { - if ((await this.getTagByName(tag.name)) === undefined) { - await this.knex - .insert({ - name: tag.name, - description: tag.description === undefined ? "" : tag.description, - }) - .into("tags"); - return true; - } - return false; + const result = await this.kysely.insertInto("tags") + .values([tag]) + .onConflict((oc) => oc.doNothing()) + .executeTakeFirst(); + return (result.numInsertedOrUpdatedRows ?? 0n) > 0; } async delTag(name: string) { - if ((await this.getTagByName(name)) !== undefined) { - await this.knex.delete().where({ name: name }).from("tags"); - return true; - } - return false; + const result = await this.kysely.deleteFrom("tags") + .where("name", "=", name) + .executeTakeFirst(); + return (result.numDeletedRows ?? 0n) > 0; } async updateTag(name: string, desc: string) { - if ((await this.getTagByName(name)) !== undefined) { - await this.knex.update({ description: desc }).where({ name: name }).from("tags"); - return true; - } - return false; + const result = await this.kysely.updateTable("tags") + .set({ description: desc }) + .where("name", "=", name) + .executeTakeFirst(); + return (result.numUpdatedRows ?? 0n) > 0; } } -export const createKnexTagController = (knex: Knex): TagAccessor => { - return new KnexTagAccessor(knex); +export const createSqliteTagController = (kysely = getKysely()): TagAccessor => { + return new SqliteTagAccessor(kysely); }; diff --git a/packages/server/src/db/user.ts b/packages/server/src/db/user.ts index adfd255..ba2c899 100644 --- a/packages/server/src/db/user.ts +++ b/packages/server/src/db/user.ts @@ -1,88 +1,87 @@ -import { Knex } from "knex"; -import { IUser, Password, UserAccessor, UserCreateInput } from "../model/user"; +import { getKysely } from "./kysely"; +import { type IUser, Password, type UserAccessor, type UserCreateInput } from "../model/user"; -type PermissionTable = { - username: string; - name: string; -}; -type DBUser = { - username: string; - password_hash: string; - password_salt: string; -}; -class KnexUser implements IUser { - private knex: Knex; +class SqliteUser implements IUser { readonly username: string; readonly password: Password; - constructor(username: string, pw: Password, knex: Knex) { + constructor(username: string, pw: Password, private kysely = getKysely()) { this.username = username; this.password = pw; - this.knex = knex; } async reset_password(password: string) { this.password.set_password(password); - await this.knex - .from("users") - .where({ username: this.username }) - .update({ password_hash: this.password.hash, password_salt: this.password.salt }); + await this.kysely + .updateTable("users") + .where("username", "=", this.username) + .set({ password_hash: this.password.hash, password_salt: this.password.salt }) + .execute(); } async get_permissions() { - let b = (await this.knex.select("*").from("permissions").where({ username: this.username })) as PermissionTable[]; - return b.map((x) => x.name); + const permissions = await this.kysely + .selectFrom("permissions") + .selectAll() + .where("username", "=", this.username) + .execute(); + return permissions.map((x) => x.name); } async add(name: string) { - if (!(await this.get_permissions()).includes(name)) { - const r = await this.knex - .insert({ - username: this.username, - name: name, - }) - .into("permissions"); - return true; - } - return false; + const result = await this.kysely + .insertInto("permissions") + .values({ username: this.username, name }) + .onConflict((oc) => oc.doNothing()) + .executeTakeFirst(); + return (result.numInsertedOrUpdatedRows ?? 0n) > 0; } async remove(name: string) { - const r = await this.knex - .from("permissions") - .where({ - username: this.username, - name: name, - }) - .delete(); - return r !== 0; + const result = await this.kysely + .deleteFrom("permissions") + .where("username", "=", this.username) + .where("name", "=", name) + .executeTakeFirst(); + return (result.numDeletedRows ?? 0n) > 0; } } -export const createKnexUserController = (knex: Knex): UserAccessor => { - const createUserKnex = async (input: UserCreateInput) => { - if (undefined !== (await findUserKenx(input.username))) { +export const createSqliteUserController = (kysely = getKysely()): UserAccessor => { + const createUser = async (input: UserCreateInput) => { + if (undefined !== (await findUser(input.username))) { return undefined; } - const user = new KnexUser(input.username, new Password(input.password), knex); - await knex - .insert({ - username: user.username, - password_hash: user.password.hash, - password_salt: user.password.salt, - }) - .into("users"); + const user = new SqliteUser(input.username, new Password(input.password), kysely); + await kysely + .insertInto("users") + .values({ username: user.username, password_hash: user.password.hash, password_salt: user.password.salt }) + .execute(); return user; }; - const findUserKenx = async (id: string) => { - let user: DBUser[] = await knex.select("*").from("users").where({ username: id }); - if (user.length == 0) return undefined; - const first = user[0]; - return new KnexUser(first.username, new Password({ hash: first.password_hash, salt: first.password_salt }), knex); + const findUser = async (id: string) => { + const user = await kysely + .selectFrom("users") + .selectAll() + .where("username", "=", id) + .executeTakeFirst(); + if (!user) return undefined; + if (!user.password_hash || !user.password_salt) { + throw new Error("password hash or salt is missing"); + } + if (user.username === null) { + throw new Error("username is null"); + } + return new SqliteUser(user.username, new Password({ + hash: user.password_hash, + salt: user.password_salt + }), kysely); }; - const delUserKnex = async (id: string) => { - let r = await knex.delete().from("users").where({ username: id }); - return r === 0; + const delUser = async (id: string) => { + const result = await kysely.deleteFrom("users") + .where("username", "=", id) + .executeTakeFirst(); + return (result.numDeletedRows ?? 0n) > 0; }; return { - createUser: createUserKnex, - findUser: findUserKenx, - delUser: delUserKnex, + createUser: createUser, + findUser: findUser, + delUser: delUser, }; }; diff --git a/packages/server/src/diff/content_handler.ts b/packages/server/src/diff/content_handler.ts index d70f90c..d0e0cc6 100644 --- a/packages/server/src/diff/content_handler.ts +++ b/packages/server/src/diff/content_handler.ts @@ -1,8 +1,8 @@ -import { basename, dirname, join as pathjoin } from "path"; +import { basename, dirname, join as pathjoin } from "node:path"; import { ContentFile, createContentFile } from "../content/mod"; -import { Document, DocumentAccessor } from "../model/mod"; +import type { Document, DocumentAccessor } from "../model/mod"; import { ContentList } from "./content_list"; -import { IDiffWatcher } from "./watcher"; +import type { IDiffWatcher } from "./watcher"; // refactoring needed. export class ContentDiffHandler { diff --git a/packages/server/src/diff/content_list.ts b/packages/server/src/diff/content_list.ts index 51035fb..9e3f7f8 100644 --- a/packages/server/src/diff/content_list.ts +++ b/packages/server/src/diff/content_list.ts @@ -1,4 +1,4 @@ -import { ContentFile } from "../content/mod"; +import type { ContentFile } from "../content/mod"; export class ContentList { /** path map */ diff --git a/packages/server/src/diff/diff.ts b/packages/server/src/diff/diff.ts index df75b22..5fd7c5a 100644 --- a/packages/server/src/diff/diff.ts +++ b/packages/server/src/diff/diff.ts @@ -1,7 +1,7 @@ import asyncPool from "tiny-async-pool"; -import { DocumentAccessor } from "../model/doc"; +import type { DocumentAccessor } from "../model/doc"; import { ContentDiffHandler } from "./content_handler"; -import { IDiffWatcher } from "./watcher"; +import type { IDiffWatcher } from "./watcher"; export class DiffManager { watching: { [content_type: string]: ContentDiffHandler }; diff --git a/packages/server/src/diff/router.ts b/packages/server/src/diff/router.ts index 21b7358..725bb6e 100644 --- a/packages/server/src/diff/router.ts +++ b/packages/server/src/diff/router.ts @@ -1,9 +1,9 @@ -import Koa from "koa"; +import type Koa from "koa"; import Router from "koa-router"; -import { ContentFile } from "../content/mod"; +import type { ContentFile } from "../content/mod"; import { AdminOnlyMiddleware } from "../permission/permission"; import { sendError } from "../route/error_handler"; -import { DiffManager } from "./diff"; +import type { DiffManager } from "./diff"; function content_file_to_return(x: ContentFile) { return { path: x.path, type: x.type }; @@ -24,7 +24,7 @@ type PostAddedBody = { }[]; function checkPostAddedBody(body: any): body is PostAddedBody { - if (body instanceof Array) { + if (Array.isArray(body)) { return body.map((x) => "type" in x && "path" in x).every((x) => x); } return false; @@ -54,7 +54,7 @@ export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouter sendError(400, 'format exception: there is no "type"'); return; } - const t = reqbody["type"]; + const t = reqbody.type; if (typeof t !== "string") { sendError(400, 'format exception: invalid type of "type"'); return; diff --git a/packages/server/src/diff/watcher.ts b/packages/server/src/diff/watcher.ts index 0513374..70b01c8 100644 --- a/packages/server/src/diff/watcher.ts +++ b/packages/server/src/diff/watcher.ts @@ -1,8 +1,8 @@ -import event from "events"; -import { FSWatcher, watch } from "fs"; -import { promises } from "fs"; -import { join } from "path"; -import { DocumentAccessor } from "../model/doc"; +import type event from "node:events"; +import { FSWatcher, watch } from "node:fs"; +import { promises } from "node:fs"; +import { join } from "node:path"; +import type { DocumentAccessor } from "../model/doc"; const readdir = promises.readdir; diff --git a/packages/server/src/diff/watcher/comic_watcher.ts b/packages/server/src/diff/watcher/comic_watcher.ts index 5fd4942..96f5273 100644 --- a/packages/server/src/diff/watcher/comic_watcher.ts +++ b/packages/server/src/diff/watcher/comic_watcher.ts @@ -1,6 +1,3 @@ -import { EventEmitter } from "events"; -import { DocumentAccessor } from "../../model/doc"; -import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; import { ComicConfig } from "./ComicConfig"; import { WatcherCompositer } from "./compositer"; import { RecursiveWatcher } from "./recursive_watcher"; diff --git a/packages/server/src/diff/watcher/common_watcher.ts b/packages/server/src/diff/watcher/common_watcher.ts index 5d9444d..2782660 100644 --- a/packages/server/src/diff/watcher/common_watcher.ts +++ b/packages/server/src/diff/watcher/common_watcher.ts @@ -1,8 +1,8 @@ -import event from "events"; -import { FSWatcher, promises, watch } from "fs"; -import { join } from "path"; -import { DocumentAccessor } from "../../model/doc"; -import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; +import event from "node:events"; +import { type FSWatcher, promises, watch } from "node:fs"; +import { join } from "node:path"; +import type { DocumentAccessor } from "../../model/doc"; +import type { DiffWatcherEvent, IDiffWatcher } from "../watcher"; import { setupHelp } from "./util"; const { readdir } = promises; diff --git a/packages/server/src/diff/watcher/compositer.ts b/packages/server/src/diff/watcher/compositer.ts index f993f61..b356f58 100644 --- a/packages/server/src/diff/watcher/compositer.ts +++ b/packages/server/src/diff/watcher/compositer.ts @@ -1,6 +1,6 @@ -import { EventEmitter } from "events"; -import { DocumentAccessor } from "../../model/doc"; -import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; +import { EventEmitter } from "node:events"; +import type { DocumentAccessor } from "../../model/doc"; +import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher"; export class WatcherCompositer extends EventEmitter implements IDiffWatcher { refWatchers: IDiffWatcher[]; diff --git a/packages/server/src/diff/watcher/recursive_watcher.ts b/packages/server/src/diff/watcher/recursive_watcher.ts index d14a31f..14fe70d 100644 --- a/packages/server/src/diff/watcher/recursive_watcher.ts +++ b/packages/server/src/diff/watcher/recursive_watcher.ts @@ -1,9 +1,8 @@ -import { FSWatcher, watch } from "chokidar"; -import { EventEmitter } from "events"; -import { join } from "path"; -import { DocumentAccessor } from "../../model/doc"; -import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; -import { setupHelp, setupRecursive } from "./util"; +import { type FSWatcher, watch } from "chokidar"; +import { EventEmitter } from "node:events"; +import type { DocumentAccessor } from "../../model/doc"; +import type { DiffWatcherEvent, IDiffWatcher } from "../watcher"; +import { setupRecursive } from "./util"; type RecursiveWatcherOption = { /** @default true */ diff --git a/packages/server/src/diff/watcher/util.ts b/packages/server/src/diff/watcher/util.ts index 1823deb..7e136f0 100644 --- a/packages/server/src/diff/watcher/util.ts +++ b/packages/server/src/diff/watcher/util.ts @@ -1,14 +1,13 @@ -import { EventEmitter } from "events"; -import { promises } from "fs"; -import { join } from "path"; +import { promises } from "node:fs"; +import { join } from "node:path"; const { readdir } = promises; -import { DocumentAccessor } from "../../model/doc"; -import { IDiffWatcher } from "../watcher"; +import type { DocumentAccessor } from "../../model/doc"; +import type { IDiffWatcher } from "../watcher"; function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) { // Todo : reduce O(nm) to O(n+m) using hash map. - let added = cur.filter((x) => !initial_filenames.includes(x)); - let deleted = initial_filenames.filter((x) => !cur.includes(x)); + const added = cur.filter((x) => !initial_filenames.includes(x)); + const deleted = initial_filenames.filter((x) => !cur.includes(x)); for (const it of added) { const cpath = join(basepath, it); watcher.emit("create", cpath); diff --git a/packages/server/src/diff/watcher/watcher_filter.ts b/packages/server/src/diff/watcher/watcher_filter.ts index a8a41f9..67d0fca 100644 --- a/packages/server/src/diff/watcher/watcher_filter.ts +++ b/packages/server/src/diff/watcher/watcher_filter.ts @@ -1,6 +1,6 @@ -import { EventEmitter } from "events"; -import { DocumentAccessor } from "../../model/doc"; -import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; +import { EventEmitter } from "node:events"; +import type { DocumentAccessor } from "../../model/doc"; +import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher"; export class WatcherFilter extends EventEmitter implements IDiffWatcher { refWatcher: IDiffWatcher; @@ -21,18 +21,16 @@ export class WatcherFilter extends EventEmitter implements IDiffWatcher { if (this.filter(prev)) { if (this.filter(cur)) { return super.emit("change", prev, cur); - } else { - return super.emit("delete", cur); } - } else { + return super.emit("delete", cur); + } if (this.filter(cur)) { return super.emit("create", cur); } - } return false; - } else if (!this.filter(arg[0])) { + }if (!this.filter(arg[0])) { return false; - } else return super.emit(event, ...arg); + }return super.emit(event, ...arg); } constructor(refWatcher: IDiffWatcher, filter: (filename: string) => boolean) { super(); diff --git a/packages/server/src/login.ts b/packages/server/src/login.ts index 4d69feb..e10917a 100644 --- a/packages/server/src/login.ts +++ b/packages/server/src/login.ts @@ -1,10 +1,10 @@ -import { request } from "http"; +import { request } from "node:http"; import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken"; import Knex from "knex"; -import Koa from "koa"; +import type Koa from "koa"; import Router from "koa-router"; -import { createKnexUserController } from "./db/mod"; -import { IUser, UserAccessor } from "./model/mod"; +import { createSqliteUserController } from "./db/mod"; +import type { IUser, UserAccessor } from "./model/mod"; import { sendError } from "./route/error_handler"; import { get_setting } from "./SettingConfig"; @@ -19,7 +19,7 @@ export type UserState = { const isUserState = (obj: object | string): obj is PayloadInfo => { if (typeof obj === "string") return false; - return "username" in obj && "permission" in obj && (obj as { permission: unknown }).permission instanceof Array; + return "username" in obj && "permission" in obj && Array.isArray((obj as { permission: unknown }).permission); }; type RefreshPayloadInfo = { username: string }; const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => { @@ -57,7 +57,7 @@ const publishRefreshToken = (secretKey: string, username: string, expiredtime: n }; function setToken(ctx: Koa.Context, token_name: string, token_payload: string | null, expiredtime: number) { const setting = get_setting(); - if (token_payload === null && !!!ctx.cookies.get(token_name)) { + if (token_payload === null && !ctx.cookies.get(token_name)) { return; } ctx.cookies.set(token_name, token_payload, { @@ -72,11 +72,11 @@ export const createLoginMiddleware = (userController: UserAccessor) => async (ct const secretKey = setting.jwt_secretkey; const body = ctx.request.body; // check format - if (typeof body == "string" || !("username" in body) || !("password" in body)) { + if (typeof body === "string" || !("username" in body) || !("password" in body)) { return sendError(400, "invalid form : username or password is not found in query."); } - const username = body["username"]; - const password = body["password"]; + const username = body.username; + const password = body.password; // check type if (typeof username !== "string" || typeof password !== "string") { return sendError(400, "invalid form : username or password is not string"); @@ -125,7 +125,7 @@ export const createUserMiddleWare = const setGuest = async () => { setToken(ctx, accessTokenName, null, 0); setToken(ctx, refreshTokenName, null, 0); - ctx.state["user"] = { username: "", permission: setting.guest }; + ctx.state.user = { username: "", permission: setting.guest }; return await next(); }; return await refreshToken(ctx, setGuest, next); @@ -134,7 +134,7 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai const accessPayload = ctx.cookies.get(accessTokenName); const setting = get_setting(); const secretKey = setting.jwt_secretkey; - if (accessPayload == undefined) { + if (accessPayload === undefined) { return await checkRefreshAndUpdate(); } try { @@ -142,20 +142,19 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai if (isUserState(o)) { ctx.state.user = o; return await next(); - } else { + } console.error("invalid token detected"); throw new Error("token form invalid"); - } } catch (e) { if (e instanceof TokenExpiredError) { return await checkRefreshAndUpdate(); - } else throw e; + }throw e; } async function checkRefreshAndUpdate() { const refreshPayload = ctx.cookies.get(refreshTokenName); if (refreshPayload === undefined) { return await fail(); // refresh token doesn't exist - } else { + } try { const o = verify(refreshPayload, secretKey); if (isRefreshToken(o)) { @@ -173,9 +172,8 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai if (e instanceof TokenExpiredError) { // refresh token is expired. return await fail(); - } else throw e; + }throw e; } - } return await next(); } }; @@ -205,9 +203,9 @@ export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.C if (typeof body !== "object" || !("username" in body) || !("oldpassword" in body) || !("newpassword" in body)) { return sendError(400, "request body is invalid format"); } - const username = body["username"]; - const oldpw = body["oldpassword"]; - const newpw = body["newpassword"]; + const username = body.username; + const oldpw = body.oldpassword; + const newpw = body.newpassword; if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") { return sendError(400, "request body is invalid format"); } diff --git a/packages/server/src/model/doc.ts b/packages/server/src/model/doc.ts index 3e01e75..9b2dca2 100644 --- a/packages/server/src/model/doc.ts +++ b/packages/server/src/model/doc.ts @@ -1,4 +1,4 @@ -import { JSONMap } from "../types/json"; +import type { JSONMap } from "../types/json"; import { check_type } from "../util/type_check"; import { TagAccessor } from "./tag"; @@ -8,7 +8,7 @@ export interface DocumentBody { basepath: string; filename: string; modified_at: number; - content_hash: string; + content_hash: string | null; additional: JSONMap; tags: string[]; // eager loading } @@ -23,7 +23,7 @@ export const MetaContentBody = { tags: "string[]", }; -export const isDocBody = (c: any): c is DocumentBody => { +export const isDocBody = (c: unknown): c is DocumentBody => { return check_type(c, MetaContentBody); }; @@ -33,8 +33,9 @@ export interface Document extends DocumentBody { readonly deleted_at: number | null; } -export const isDoc = (c: any): c is Document => { - if ("id" in c && typeof c["id"] === "number") { +export const isDoc = (c: unknown): c is Document => { + if (typeof c !== "object" || c === null) return false; + if ("id" in c && typeof c.id === "number") { const { id, ...rest } = c; return isDocBody(rest); } diff --git a/packages/server/src/model/tag.ts b/packages/server/src/model/tag.ts index f2592fc..ab3f985 100644 --- a/packages/server/src/model/tag.ts +++ b/packages/server/src/model/tag.ts @@ -9,7 +9,7 @@ export interface TagCount { } export interface TagAccessor { - getAllTagList: (onlyname?: boolean) => Promise; + getAllTagList: () => Promise; getAllTagCount(): Promise; getTagByName: (name: string) => Promise; addTag: (tag: Tag) => Promise; diff --git a/packages/server/src/model/user.ts b/packages/server/src/model/user.ts index 700793d..f4b8005 100644 --- a/packages/server/src/model/user.ts +++ b/packages/server/src/model/user.ts @@ -1,4 +1,4 @@ -import { createHmac, randomBytes } from "crypto"; +import { createHmac, randomBytes } from "node:crypto"; function hashForPassword(salt: string, password: string) { return createHmac("sha256", salt).update(password).digest("hex"); diff --git a/packages/server/src/permission/permission.ts b/packages/server/src/permission/permission.ts index e6212e1..8229316 100644 --- a/packages/server/src/permission/permission.ts +++ b/packages/server/src/permission/permission.ts @@ -1,5 +1,5 @@ -import Koa from "koa"; -import { UserState } from "../login"; +import type Koa from "koa"; +import type { UserState } from "../login"; import { sendError } from "../route/error_handler"; export enum Permission { @@ -37,7 +37,7 @@ export enum Permission { export const createPermissionCheckMiddleware = (...permissions: string[]) => async (ctx: Koa.ParameterizedContext, next: Koa.Next) => { - const user = ctx.state["user"]; + const user = ctx.state.user; if (user.username === "admin") { return await next(); } @@ -46,12 +46,12 @@ export const createPermissionCheckMiddleware = if (!permissions.map((p) => user_permission.includes(p)).every((x) => x)) { if (user.username === "") { return sendError(401, "you are guest. login needed."); - } else return sendError(403, "do not have permission"); + }return sendError(403, "do not have permission"); } await next(); }; export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext, next: Koa.Next) => { - const user = ctx.state["user"]; + const user = ctx.state.user; if (user.username !== "admin") { return sendError(403, "admin only"); } diff --git a/packages/server/src/route/all.ts b/packages/server/src/route/all.ts index e99ba16..49724ef 100644 --- a/packages/server/src/route/all.ts +++ b/packages/server/src/route/all.ts @@ -1,8 +1,8 @@ -import { DefaultContext, Middleware, Next, ParameterizedContext } from "koa"; +import type { DefaultContext, Middleware, Next, ParameterizedContext } from "koa"; import compose from "koa-compose"; -import Router, { IParamMiddleware } from "koa-router"; +import Router from "koa-router"; import ComicRouter from "./comic"; -import { ContentContext } from "./context"; +import type { ContentContext } from "./context"; import VideoRouter from "./video"; const table: { [s: string]: Router | undefined } = { @@ -12,25 +12,26 @@ const table: { [s: string]: Router | undefined } = { const all_middleware = (cont: string | undefined, restarg: string | undefined) => async (ctx: ParameterizedContext, next: Next) => { - if (cont == undefined) { + if (cont === undefined) { ctx.status = 404; return; } - if (ctx.state.location.type != cont) { + if (ctx.state.location.type !== cont) { console.error("not matched"); ctx.status = 404; return; } const router = table[cont]; - if (router == undefined) { + if (router === undefined) { ctx.status = 404; return; } - const rest = "/" + (restarg ?? ""); + const rest = `/${restarg ?? ""}`; const result = router.match(rest, "GET"); if (!result.route) { return await next(); } + // biome-ignore lint/suspicious/noExplicitAny: const chain = result.pathAndMethod.reduce((combination: Middleware[], cur) => { combination.push(async (ctx, next) => { const captures = cur.captures(rest); @@ -47,11 +48,11 @@ export class AllContentRouter extends Router { constructor() { super(); this.get("/:content_type", async (ctx, next) => { - return await all_middleware(ctx.params["content_type"], undefined)(ctx, next); + return await all_middleware(ctx.params.content_type, undefined)(ctx, next); }); this.get("/:content_type/:rest(.*)", async (ctx, next) => { - const cont = ctx.params["content_type"] as string; - return await all_middleware(cont, ctx.params["rest"])(ctx, next); + const cont = ctx.params.content_type as string; + return await all_middleware(cont, ctx.params.rest)(ctx, next); }); } } diff --git a/packages/server/src/route/comic.ts b/packages/server/src/route/comic.ts index 652f9bc..8606268 100644 --- a/packages/server/src/route/comic.ts +++ b/packages/server/src/route/comic.ts @@ -1,14 +1,14 @@ -import { Context, DefaultContext, DefaultState, Next } from "koa"; +import { type Context, DefaultContext, DefaultState, Next } from "koa"; import Router from "koa-router"; -import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, ZipAsync } from "../util/zipwrap"; -import { ContentContext } from "./context"; +import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, type ZipAsync } from "../util/zipwrap"; +import type { ContentContext } from "./context"; import { since_last_modified } from "./util"; /** * zip stream cache. */ -let ZipStreamCache: { [path: string]: [ZipAsync, number] } = {}; +const ZipStreamCache: { [path: string]: [ZipAsync, number] } = {}; async function acquireZip(path: string) { if (!(path in ZipStreamCache)) { @@ -16,12 +16,11 @@ async function acquireZip(path: string) { ZipStreamCache[path] = [ret, 1]; // console.log(`acquire ${path} 1`); return ret; - } else { + } const [ret, refCount] = ZipStreamCache[path]; ZipStreamCache[path] = [ret, refCount + 1]; // console.log(`acquire ${path} ${refCount + 1}`); return ret; - } } function releaseZip(path: string) { @@ -40,7 +39,7 @@ function releaseZip(path: string) { async function renderZipImage(ctx: Context, path: string, page: number) { const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"]; // console.log(`opened ${page}`); - let zip = await acquireZip(path); + const zip = await acquireZip(path); const entries = (await entriesByNaturalOrder(zip)).filter((x) => { const ext = x.name.split(".").pop(); return ext !== undefined && image_ext.includes(ext); @@ -84,7 +83,7 @@ export class ComicRouter extends Router { await renderZipImage(ctx, ctx.state.location.path, 0); }); this.get("/:page(\\d+)", async (ctx, next) => { - const page = Number.parseInt(ctx.params["page"]); + const page = Number.parseInt(ctx.params.page); await renderZipImage(ctx, ctx.state.location.path, page); }); this.get("/thumbnail", async (ctx, next) => { diff --git a/packages/server/src/route/contents.ts b/packages/server/src/route/contents.ts index dbb0cf6..af995d0 100644 --- a/packages/server/src/route/contents.ts +++ b/packages/server/src/route/contents.ts @@ -1,22 +1,22 @@ -import { Context, Next } from "koa"; +import type { Context, Next } from "koa"; import Router from "koa-router"; -import { join } from "path"; -import { Document, DocumentAccessor, isDocBody } from "../model/doc"; -import { QueryListOption } from "../model/doc"; +import { join } from "node:path"; +import { type Document, type DocumentAccessor, isDocBody } from "../model/doc"; +import type { QueryListOption } from "../model/doc"; import { AdminOnlyMiddleware as AdminOnly, createPermissionCheckMiddleware as PerCheck, Permission as Per, } from "../permission/permission"; import { AllContentRouter } from "./all"; -import { ContentLocation } from "./context"; +import type { ContentLocation } from "./context"; import { sendError } from "./error_handler"; import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util"; const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - const num = Number.parseInt(ctx.params["num"]); - let document = await controller.findById(num, true); - if (document == undefined) { + const num = Number.parseInt(ctx.params.num); + const document = await controller.findById(num, true); + if (document === undefined) { return sendError(404, "document does not exist."); } ctx.body = document; @@ -24,28 +24,22 @@ const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, console.log(document.additional); }; const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - const num = Number.parseInt(ctx.params["num"]); - let document = await controller.findById(num, true); - if (document == undefined) { + const num = Number.parseInt(ctx.params.num); + const document = await controller.findById(num, true); + if (document === undefined) { return sendError(404, "document does not exist."); } ctx.body = document.tags; ctx.type = "json"; }; const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - let query_limit = ctx.query["limit"]; - let query_cursor = ctx.query["cursor"]; - let query_word = ctx.query["word"]; - let query_content_type = ctx.query["content_type"]; - let query_offset = ctx.query["offset"]; - let query_use_offset = ctx.query["use_offset"]; - if ( - query_limit instanceof Array || - query_cursor instanceof Array || - query_word instanceof Array || - query_content_type instanceof Array || - query_offset instanceof Array || - query_use_offset instanceof Array + const query_limit = ctx.query.limit; + const query_cursor = ctx.query.cursor; + const query_word = ctx.query.word; + const query_content_type = ctx.query.content_type; + const query_offset = ctx.query.offset; + const query_use_offset = ctx.query.use_offset; + if (Array.isArray(query_limit) ||Array.isArray(query_cursor) ||Array.isArray(query_word) ||Array.isArray(query_content_type) ||Array.isArray(query_offset) ||Array.isArray(query_use_offset) ) { return sendError(400, "paramter can not be array"); } @@ -54,10 +48,10 @@ const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Contex const word = ParseQueryArgString(query_word); const content_type = ParseQueryArgString(query_content_type); const offset = ParseQueryNumber(query_offset); - if (limit === NaN || cursor === NaN || offset === NaN) { + if (Number.isNaN(limit) || Number.isNaN(cursor) || Number.isNaN(offset)) { return sendError(400, "parameter limit, cursor or offset is not a number"); } - const allow_tag = ParseQueryArray(ctx.query["allow_tag"]); + const allow_tag = ParseQueryArray(ctx.query.allow_tag); const [ok, use_offset] = ParseQueryBoolean(query_use_offset); if (!ok) { return sendError(400, "use_offset must be true or false."); @@ -72,12 +66,12 @@ const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Contex use_offset: use_offset, content_type: content_type, }; - let document = await controller.findList(option); + const document = await controller.findList(option); ctx.body = document; ctx.type = "json"; }; const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - const num = Number.parseInt(ctx.params["num"]); + const num = Number.parseInt(ctx.params.num); if (ctx.request.type !== "json") { return sendError(400, "update fail. invalid document type: it is not json."); @@ -95,9 +89,9 @@ const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Conte }; const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - let tag_name = ctx.params["tag"]; - const num = Number.parseInt(ctx.params["num"]); - if (typeof tag_name === undefined) { + let tag_name = ctx.params.tag; + const num = Number.parseInt(ctx.params.num); + if (typeof tag_name === "undefined") { return sendError(400, "??? Unreachable"); } tag_name = String(tag_name); @@ -110,9 +104,9 @@ const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex ctx.type = "json"; }; const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - let tag_name = ctx.params["tag"]; - const num = Number.parseInt(ctx.params["num"]); - if (typeof tag_name === undefined) { + let tag_name = ctx.params.tag; + const num = Number.parseInt(ctx.params.num); + if (typeof tag_name === "undefined") { return sendError(400, "?? Unreachable"); } tag_name = String(tag_name); @@ -125,22 +119,22 @@ const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex ctx.type = "json"; }; const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - const num = Number.parseInt(ctx.params["num"]); + const num = Number.parseInt(ctx.params.num); const r = await controller.del(num); ctx.body = JSON.stringify(r); ctx.type = "json"; }; const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { - const num = Number.parseInt(ctx.params["num"]); - let document = await controller.findById(num, true); - if (document == undefined) { + const num = Number.parseInt(ctx.params.num); + const document = await controller.findById(num, true); + if (document === undefined) { return sendError(404, "document does not exist."); } if (document.deleted_at !== null) { return sendError(404, "document has been removed."); } const path = join(document.basepath, document.filename); - ctx.state["location"] = { + ctx.state.location = { path: path, type: document.content_type, additional: document.additional, diff --git a/packages/server/src/route/error_handler.ts b/packages/server/src/route/error_handler.ts index deb1da9..a329645 100644 --- a/packages/server/src/route/error_handler.ts +++ b/packages/server/src/route/error_handler.ts @@ -1,4 +1,4 @@ -import { Context, Next } from "koa"; +import type { Context, Next } from "koa"; export interface ErrorFormat { code: number; diff --git a/packages/server/src/route/tags.ts b/packages/server/src/route/tags.ts index edbbb75..da0f535 100644 --- a/packages/server/src/route/tags.ts +++ b/packages/server/src/route/tags.ts @@ -1,13 +1,13 @@ -import { Context, Next } from "koa"; -import Router, { RouterContext } from "koa-router"; -import { TagAccessor } from "../model/tag"; +import { type Context, Next } from "koa"; +import Router, { type RouterContext } from "koa-router"; +import type { TagAccessor } from "../model/tag"; import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission"; import { sendError } from "./error_handler"; export function getTagRounter(tagController: TagAccessor) { - let router = new Router(); + const router = new Router(); router.get("/", PerCheck(Permission.QueryContent), async (ctx: Context) => { - if (ctx.query["withCount"]) { + if (ctx.query.withCount) { const c = await tagController.getAllTagCount(); ctx.body = c; } else { @@ -17,7 +17,7 @@ export function getTagRounter(tagController: TagAccessor) { ctx.type = "json"; }); router.get("/:tag_name", PerCheck(Permission.QueryContent), async (ctx: RouterContext) => { - const tag_name = ctx.params["tag_name"]; + const tag_name = ctx.params.tag_name; const c = await tagController.getTagByName(tag_name); if (!c) { sendError(404, "tags not found"); diff --git a/packages/server/src/route/util.ts b/packages/server/src/route/util.ts index c07f9c4..bdc39f0 100644 --- a/packages/server/src/route/util.ts +++ b/packages/server/src/route/util.ts @@ -1,13 +1,13 @@ -import { Context } from "koa"; +import type { Context } from "koa"; export function ParseQueryNumber(s: string[] | string | undefined): number | undefined { if (s === undefined) return undefined; - else if (typeof s === "object") return undefined; - else return Number.parseInt(s); + if (typeof s === "object") return undefined; + return Number.parseInt(s); } export function ParseQueryArray(s: string[] | string | undefined) { - s = s ?? []; - const r = s instanceof Array ? s : [s]; + const input = s ?? []; + const r = Array.isArray(input) ? input : [input]; return r.map((x) => decodeURIComponent(x)); } export function ParseQueryArgString(s: string[] | string | undefined) { diff --git a/packages/server/src/route/video.ts b/packages/server/src/route/video.ts index cc96367..198adc2 100644 --- a/packages/server/src/route/video.ts +++ b/packages/server/src/route/video.ts @@ -1,7 +1,7 @@ -import { createReadStream, promises } from "fs"; -import { Context } from "koa"; +import { createReadStream, promises } from "node:fs"; +import type { Context } from "koa"; import Router from "koa-router"; -import { ContentContext } from "./context"; +import type { ContentContext } from "./context"; export async function renderVideo(ctx: Context, path: string) { const ext = path.trim().split(".").pop(); @@ -27,7 +27,7 @@ export async function renderVideo(ctx: Context, path: string) { } ctx.status = 200; ctx.length = stat.size; - let stream = createReadStream(path); + const stream = createReadStream(path); ctx.body = stream; } else { const m = range_text.match(/^bytes=(\d+)-(\d*)/); @@ -35,8 +35,8 @@ export async function renderVideo(ctx: Context, path: string) { ctx.status = 416; return; } - start = parseInt(m[1]); - end = m[2].length > 0 ? parseInt(m[2]) : start + 1024 * 1024; + start = Number.parseInt(m[1]); + end = m[2].length > 0 ? Number.parseInt(m[2]) : start + 1024 * 1024; end = Math.min(end, stat.size - 1); if (start > end) { ctx.status = 416; diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index a9cdc81..2b11d9e 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -5,18 +5,21 @@ import { connectDB } from "./database"; import { createDiffRouter, DiffManager } from "./diff/mod"; import { get_setting, SettingConfig } from "./SettingConfig"; -import { createReadStream, readFileSync } from "fs"; +import { createReadStream, readFileSync } from "node:fs"; import bodyparser from "koa-bodyparser"; -import { createKnexDocumentAccessor, createKnexTagController, createKnexUserController } from "./db/mod"; +import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod"; import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login"; import getContentRouter from "./route/contents"; import { error_handler } from "./route/error_handler"; -import { createInterface as createReadlineInterface } from "readline"; +import { createInterface as createReadlineInterface } from "node:readline"; import { createComicWatcher } from "./diff/watcher/comic_watcher"; -import { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod"; +import type { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod"; import { getTagRounter } from "./route/tags"; +import { config } from "dotenv"; +config(); + class ServerApplication { readonly userController: UserAccessor; readonly documentController: DocumentAccessor; @@ -61,12 +64,12 @@ class ServerApplication { app.use(error_handler); app.use(createUserMiddleWare(this.userController)); - let diff_router = createDiffRouter(this.diffManger); + const diff_router = createDiffRouter(this.diffManger); this.diffManger.register("comic", createComicWatcher()); console.log("setup router"); - let router = new Router(); + const router = new Router(); router.use("/api/(.*)", async (ctx, next) => { // For CORS ctx.res.setHeader("access-control-allow-origin", "*"); @@ -92,12 +95,12 @@ class ServerApplication { router.use("/user", login_router.routes()); router.use("/user", login_router.allowedMethods()); - if (setting.mode == "development") { + if (setting.mode === "development") { let mm_count = 0; app.use(async (ctx, next) => { console.log(`==========================${mm_count++}`); const ip = ctx.get("X-Real-IP") ?? ctx.ip; - const fromClient = ctx.state["user"].username === "" ? ip : ctx.state["user"].username; + const fromClient = ctx.state.user.username === "" ? ip : ctx.state.user.username; console.log(`${fromClient} : ${ctx.method} ${ctx.url}`); await next(); // console.log(`404`); @@ -132,8 +135,9 @@ class ServerApplication { } private serve_with_meta_index(router: Router) { const DocMiddleware = async (ctx: Koa.ParameterizedContext) => { - const docId = Number.parseInt(ctx.params["id"]); + const docId = Number.parseInt(ctx.params.id); const doc = await this.documentController.findById(docId, true); + // biome-ignore lint/suspicious/noImplicitAnyLet: let meta; if (doc === undefined) { ctx.status = 404; @@ -190,7 +194,7 @@ class ServerApplication { } private serve_static_file(router: Router) { const static_file_server = (path: string, type: string) => { - router.get("/" + path, async (ctx, next) => { + router.get(`/${path}`, async (ctx, next) => { const setting = get_setting(); ctx.type = type; ctx.body = createReadStream(path); @@ -211,19 +215,19 @@ class ServerApplication { } } start_server() { - let setting = get_setting(); + const setting = get_setting(); // todo : support https console.log(`listen on http://${setting.localmode ? "localhost" : "0.0.0.0"}:${setting.port}`); return this.app.listen(setting.port, setting.localmode ? "127.0.0.1" : "0.0.0.0"); } static async createServer() { const setting = get_setting(); - let db = await connectDB(); + const db = await connectDB(); const app = new ServerApplication({ - userController: createKnexUserController(db), - documentController: createKnexDocumentAccessor(db), - tagController: createKnexTagController(db), + userController: createSqliteUserController(db), + documentController: createSqliteDocumentAccessor(db), + tagController: createSqliteTagController(db), }); await app.setup(); return app; diff --git a/packages/server/src/types/db.d.ts b/packages/server/src/types/db.d.ts deleted file mode 100644 index 7e1a63e..0000000 --- a/packages/server/src/types/db.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Knex } from "knex"; - -declare module "knex" { - interface Tables { - tags: { - name: string; - description?: string; - }; - users: { - username: string; - password_hash: string; - password_salt: string; - }; - document: { - id: number; - title: string; - content_type: string; - basepath: string; - filename: string; - created_at: number; - deleted_at: number | null; - content_hash: string; - additional: string | null; - }; - doc_tag_relation: { - doc_id: number; - tag_name: string; - }; - permissions: { - username: string; - name: string; - }; - } -} diff --git a/packages/server/src/util/configRW.ts b/packages/server/src/util/configRW.ts index c769c9c..e39d1b4 100644 --- a/packages/server/src/util/configRW.ts +++ b/packages/server/src/util/configRW.ts @@ -1,7 +1,7 @@ -import { existsSync, promises as fs, readFileSync, writeFileSync } from "fs"; +import { existsSync, promises as fs, readFileSync, writeFileSync } from "node:fs"; import { validate } from "jsonschema"; -export class ConfigManager { +export class ConfigManager { path: string; default_config: T; config: T | null; diff --git a/packages/server/src/util/type_check.ts b/packages/server/src/util/type_check.ts index 160eab9..6ee6d50 100644 --- a/packages/server/src/util/type_check.ts +++ b/packages/server/src/util/type_check.ts @@ -1,13 +1,15 @@ -export function check_type(obj: any, check_proto: Record): obj is T { +export function check_type(obj: unknown, check_proto: Record): obj is T { + if (typeof obj !== "object" || obj === null) return false; for (const it in check_proto) { let defined = check_proto[it]; if (defined === undefined) return false; defined = defined.trim(); if (defined.endsWith("[]")) { - if (!(obj[it] instanceof Array)) { + if (!Array.isArray((obj as Record)[it])) { return false; } - } else if (defined !== typeof obj[it]) { + // biome-ignore lint/suspicious/useValidTypeof: + } else if (defined !== typeof (obj as Record)[it]) { return false; } } diff --git a/packages/server/src/util/zipwrap.ts b/packages/server/src/util/zipwrap.ts index 46270e5..d1f6164 100644 --- a/packages/server/src/util/zipwrap.ts +++ b/packages/server/src/util/zipwrap.ts @@ -1,6 +1,6 @@ -import { ZipEntry } from "node-stream-zip"; +import type { ZipEntry } from "node-stream-zip"; -import { ReadStream } from "fs"; +import { ReadStream } from "node:fs"; import { orderBy } from "natural-orderby"; import StreamZip from "node-stream-zip"; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 4b13120..e824e11 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -6,7 +6,7 @@ // "incremental": true, /* Enable incremental compilation */ "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - "lib": ["DOM", "ES6"], /* Specify library files to be included in the compilation. */ + "lib": ["DOM", "ESNext"], // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e92ae14..4db78ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: chokidar: specifier: ^3.6.0 version: 3.6.0 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 jsonwebtoken: specifier: ^8.5.1 version: 8.5.1 @@ -103,6 +106,18 @@ importers: specifier: ^1.3.0 version: 1.3.0 devDependencies: + '@swc-node/register': + specifier: ^1.9.0 + version: 1.9.0(@swc/core@1.4.11)(@swc/types@0.1.6)(typescript@5.4.3) + '@swc/cli': + specifier: ^0.3.10 + version: 0.3.10(@swc/core@1.4.11)(chokidar@3.6.0) + '@swc/core': + specifier: ^1.4.11 + version: 1.4.11 + '@types/better-sqlite3': + specifier: ^7.6.9 + version: 7.6.9 '@types/jsonwebtoken': specifier: ^8.5.9 version: 8.5.9 @@ -124,6 +139,12 @@ importers: '@types/tiny-async-pool': specifier: ^1.0.5 version: 1.0.5 + dbtype: + specifier: workspace:^ + version: link:../dbtype + nodemon: + specifier: ^3.1.0 + version: 3.1.0 packages: @@ -447,7 +468,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -469,7 +490,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -484,6 +505,20 @@ packages: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true + /@mole-inc/bin-wrapper@8.0.1: + resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bin-check: 4.1.0 + bin-version-check: 5.1.0 + content-disposition: 0.5.4 + ext-name: 5.0.0 + file-type: 17.1.6 + filenamify: 5.1.1 + got: 11.8.6 + os-filter-obj: 2.0.0 + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -609,6 +644,72 @@ packages: dev: true optional: true + /@sindresorhus/is@4.6.0: + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + dev: true + + /@swc-node/core@1.13.0(@swc/core@1.4.11)(@swc/types@0.1.6): + resolution: {integrity: sha512-lFPD4nmy4ifAOVMChFjwlpXN5KQXvegqeyuzz1KQz42q1lf+cL3Qux1/GteGuZjh8HC+Rj1RdNrHpE/MCfJSTw==} + engines: {node: '>= 10'} + peerDependencies: + '@swc/core': '>= 1.3' + '@swc/types': '>= 0.1' + dependencies: + '@swc/core': 1.4.11 + '@swc/types': 0.1.6 + dev: true + + /@swc-node/register@1.9.0(@swc/core@1.4.11)(@swc/types@0.1.6)(typescript@5.4.3): + resolution: {integrity: sha512-i0iYInD4q5v3xQC6bKvs0QtfUxu197CU5qKALmpxEqTYs7sIhQ7KFLe3kP+eAR4gRkJTvAgjQgrokXLN2jZrOw==} + peerDependencies: + '@swc/core': '>= 1.3' + typescript: '>= 4.3' + dependencies: + '@swc-node/core': 1.13.0(@swc/core@1.4.11)(@swc/types@0.1.6) + '@swc-node/sourcemap-support': 0.5.0 + '@swc/core': 1.4.11 + colorette: 2.0.20 + debug: 4.3.4(supports-color@5.5.0) + pirates: 4.0.6 + tslib: 2.6.2 + typescript: 5.4.3 + transitivePeerDependencies: + - '@swc/types' + - supports-color + dev: true + + /@swc-node/sourcemap-support@0.5.0: + resolution: {integrity: sha512-fbhjL5G0YvFoWwNhWleuBUfotiX+USiA9oJqu9STFw+Hb0Cgnddn+HVS/K5fI45mn92e8V+cHD2jgFjk4w2T9Q==} + dependencies: + source-map-support: 0.5.21 + tslib: 2.6.2 + dev: true + + /@swc/cli@0.3.10(@swc/core@1.4.11)(chokidar@3.6.0): + resolution: {integrity: sha512-YWfYo9kXdbmIuGwIPth9geKgb0KssCMTdZa44zAN5KoqcuCP2rTW9s60heQDSRNpbtCmUr7BKF1VivsoHXrvrQ==} + engines: {node: '>= 16.14.0'} + hasBin: true + peerDependencies: + '@swc/core': ^1.2.66 + chokidar: ^3.5.1 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + '@mole-inc/bin-wrapper': 8.0.1 + '@swc/core': 1.4.11 + '@swc/counter': 0.1.3 + chokidar: 3.6.0 + commander: 8.3.0 + fast-glob: 3.3.2 + minimatch: 9.0.3 + piscina: 4.4.0 + semver: 7.6.0 + slash: 3.0.0 + source-map: 0.7.4 + dev: true + /@swc/core-darwin-arm64@1.4.11: resolution: {integrity: sha512-C1j1Qp/IHSelVWdEnT7f0iONWxQz6FAqzjCF2iaL+0vFg4V5f2nlgrueY8vj5pNNzSGhrAlxsMxEIp4dj1MXkg==} engines: {node: '>=10'} @@ -734,6 +835,17 @@ packages: '@swc/counter': 0.1.3 dev: true + /@szmarczak/http-timer@4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: true + + /@tokenizer/token@0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: true + /@types/accepts@1.3.7: resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} dependencies: @@ -753,6 +865,15 @@ packages: '@types/node': 14.18.63 dev: true + /@types/cacheable-request@6.0.3: + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 14.18.63 + '@types/responselike': 1.0.3 + dev: true + /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: @@ -798,6 +919,10 @@ packages: resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==} dev: true + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: true + /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true @@ -816,6 +941,12 @@ packages: resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} dev: true + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 14.18.63 + dev: true + /@types/koa-bodyparser@4.3.12: resolution: {integrity: sha512-hKMmRMVP889gPIdLZmmtou/BijaU1tHPyMNmcK7FAHAdATnRcGQQy78EqTTxLH1D4FTsrxIzklAQCso9oGoebQ==} dependencies: @@ -885,6 +1016,12 @@ packages: csstype: 3.1.3 dev: true + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + dependencies: + '@types/node': 14.18.63 + dev: true + /@types/scheduler@0.23.0: resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==} dev: true @@ -929,7 +1066,7 @@ packages: '@typescript-eslint/type-utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) '@typescript-eslint/visitor-keys': 7.4.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -955,7 +1092,7 @@ packages: '@typescript-eslint/types': 7.4.0 '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) '@typescript-eslint/visitor-keys': 7.4.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.4.3 transitivePeerDependencies: @@ -982,7 +1119,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.3) typescript: 5.4.3 @@ -1006,7 +1143,7 @@ packages: dependencies: '@typescript-eslint/types': 7.4.0 '@typescript-eslint/visitor-keys': 7.4.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -1064,6 +1201,10 @@ packages: engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'} dev: false + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1120,7 +1261,10 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: false + + /arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + dev: true /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1145,10 +1289,34 @@ packages: bindings: 1.5.0 prebuild-install: 7.1.2 + /bin-check@4.1.0: + resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==} + engines: {node: '>=4'} + dependencies: + execa: 0.7.0 + executable: 4.1.1 + dev: true + + /bin-version-check@5.1.0: + resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==} + engines: {node: '>=12'} + dependencies: + bin-version: 6.0.0 + semver: 7.6.0 + semver-truncate: 3.0.0 + dev: true + + /bin-version@6.0.0: + resolution: {integrity: sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==} + engines: {node: '>=12'} + dependencies: + execa: 5.1.1 + find-versions: 5.1.0 + dev: true + /binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - dev: false /bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -1185,6 +1353,10 @@ packages: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} dev: false + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -1204,6 +1376,24 @@ packages: ylru: 1.3.2 dev: false + /cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + dev: true + + /cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + dev: true + /call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -1250,11 +1440,16 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 - dev: false /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: true + /co-body@6.1.0: resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} dependencies: @@ -1290,6 +1485,15 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -1299,7 +1503,6 @@ packages: engines: {node: '>= 0.6'} dependencies: safe-buffer: 5.2.1 - dev: false /content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -1318,6 +1521,14 @@ packages: resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} dev: false + /cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + dev: true + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1331,7 +1542,7 @@ packages: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: true - /debug@4.3.4: + /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} peerDependencies: @@ -1341,6 +1552,7 @@ packages: optional: true dependencies: ms: 2.1.2 + supports-color: 5.5.0 /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} @@ -1360,6 +1572,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: true + /define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1421,7 +1638,6 @@ packages: /dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} - dev: true /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -1500,6 +1716,11 @@ packages: engines: {node: '>=10'} dev: true + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: true + /eslint-plugin-react-hooks@4.6.0(eslint@8.57.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} @@ -1546,7 +1767,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -1610,10 +1831,60 @@ packages: engines: {node: '>=0.10.0'} dev: true + /execa@0.7.0: + resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} + engines: {node: '>=4'} + dependencies: + cross-spawn: 5.1.0 + get-stream: 3.0.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + dependencies: + pify: 2.3.0 + dev: true + /expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + /ext-list@2.2.2: + resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} + engines: {node: '>=0.10.0'} + dependencies: + mime-db: 1.52.0 + dev: true + + /ext-name@5.0.0: + resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} + engines: {node: '>=4'} + dependencies: + ext-list: 2.2.2 + sort-keys-length: 1.0.1 + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1650,9 +1921,32 @@ packages: flat-cache: 3.2.0 dev: true + /file-type@17.1.6: + resolution: {integrity: sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 7.0.0 + token-types: 5.0.1 + dev: true + /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + /filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /filenamify@5.1.1: + resolution: {integrity: sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==} + engines: {node: '>=12.20'} + dependencies: + filename-reserved-regex: 3.0.0 + strip-outer: 2.0.0 + trim-repeated: 2.0.0 + dev: true + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -1667,6 +1961,13 @@ packages: path-exists: 4.0.0 dev: true + /find-versions@5.1.0: + resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==} + engines: {node: '>=12'} + dependencies: + semver-regex: 4.0.5 + dev: true + /flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1713,6 +2014,23 @@ packages: hasown: 2.0.2 dev: false + /get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + dev: true + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + /git-diff@2.0.6: resolution: {integrity: sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA==} engines: {node: '>= 4.8.0'} @@ -1776,6 +2094,23 @@ packages: get-intrinsic: 1.2.4 dev: false + /got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + dev: true + /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true @@ -1783,7 +2118,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -1827,6 +2161,10 @@ packages: http-errors: 1.8.1 dev: false + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + /http-errors@1.8.1: resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} engines: {node: '>= 0.6'} @@ -1849,6 +2187,19 @@ packages: toidentifier: 1.0.1 dev: false + /http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1859,6 +2210,10 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -1905,7 +2260,6 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.3.0 - dev: false /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} @@ -1939,6 +2293,21 @@ packages: engines: {node: '>=8'} dev: true + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -2035,7 +2404,7 @@ packages: resolution: {integrity: sha512-gaDdj3GtzoLoeosacd50kBBTnnh3B9AYxDThQUo4sfUyXdOhY6ku1qyZKW88tQCRgc3Sw6ChXYXWZwwgjOxE0w==} engines: {node: '>= 12'} dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) http-errors: 2.0.0 koa-compose: 4.1.0 methods: 1.1.2 @@ -2053,7 +2422,7 @@ packages: content-disposition: 0.5.4 content-type: 1.0.5 cookies: 0.9.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) delegates: 1.0.0 depd: 2.0.0 destroy: 1.2.0 @@ -2179,6 +2548,18 @@ packages: js-tokens: 4.0.0 dev: false + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: true + + /lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: true + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -2190,6 +2571,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2211,7 +2596,6 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: false /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} @@ -2220,6 +2604,16 @@ packages: mime-db: 1.52.0 dev: false + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: true + /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -2272,16 +2666,81 @@ packages: engines: {node: '>= 0.6'} dev: false + /nice-napi@1.0.2: + resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==} + os: ['!win32'] + requiresBuild: true + dependencies: + node-addon-api: 3.2.1 + node-gyp-build: 4.8.0 + dev: true + optional: true + /node-abi@3.56.0: resolution: {integrity: sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==} engines: {node: '>=10'} dependencies: semver: 7.6.0 + /node-addon-api@3.2.1: + resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + requiresBuild: true + dev: true + optional: true + + /node-gyp-build@4.8.0: + resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /nodemon@3.1.0: + resolution: {integrity: sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chokidar: 3.6.0 + debug: 4.3.4(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.6.0 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: false + + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: true + + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -2299,6 +2758,13 @@ packages: dependencies: wrappy: 1.0.2 + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + /only@0.0.2: resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} dev: false @@ -2315,6 +2781,23 @@ packages: type-check: 0.4.0 dev: true + /os-filter-obj@2.0.0: + resolution: {integrity: sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==} + engines: {node: '>=4'} + dependencies: + arch: 2.2.0 + dev: true + + /p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: true + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: true + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2351,6 +2834,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: true + /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2369,6 +2857,11 @@ packages: engines: {node: '>=8'} dev: true + /peek-readable@5.0.0: + resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==} + engines: {node: '>=14.16'} + dev: true + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -2377,6 +2870,22 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /piscina@4.4.0: + resolution: {integrity: sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==} + optionalDependencies: + nice-napi: 1.0.2 + dev: true + /postcss@8.4.38: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} @@ -2409,6 +2918,14 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + dev: true + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -2431,6 +2948,11 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + /raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -2475,12 +2997,18 @@ packages: string_decoder: 1.3.0 util-deprecate: 1.0.2 + /readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + dependencies: + readable-stream: 3.6.2 + dev: true + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 - dev: false /rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} @@ -2489,6 +3017,10 @@ packages: resolve: 1.22.8 dev: true + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: true + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2503,6 +3035,12 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + dependencies: + lowercase-keys: 2.0.0 + dev: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2557,6 +3095,18 @@ packages: loose-envify: 1.4.0 dev: false + /semver-regex@4.0.5: + resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} + engines: {node: '>=12'} + dev: true + + /semver-truncate@3.0.0: + resolution: {integrity: sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==} + engines: {node: '>=12'} + dependencies: + semver: 7.6.0 + dev: true + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -2585,6 +3135,13 @@ packages: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2592,6 +3149,11 @@ packages: shebang-regex: 3.0.0 dev: true + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: true + /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} @@ -2622,6 +3184,10 @@ packages: object-inspect: 1.13.1 dev: false + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -2632,16 +3198,54 @@ packages: once: 1.4.0 simple-concat: 1.0.1 + /simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} dev: true + /sort-keys-length@1.0.1: + resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} + engines: {node: '>=0.10.0'} + dependencies: + sort-keys: 1.1.2 + dev: true + + /sort-keys@1.1.2: + resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} + engines: {node: '>=0.10.0'} + dependencies: + is-plain-obj: 1.1.0 + dev: true + /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} dev: true + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: true + /statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -2664,6 +3268,16 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -2673,12 +3287,24 @@ packages: engines: {node: '>=8'} dev: true + /strip-outer@2.0.0: + resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /strtok3@7.0.0: + resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.0.0 + dev: true + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -2731,6 +3357,28 @@ packages: engines: {node: '>=0.6'} dev: false + /token-types@5.0.1: + resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: true + + /touch@3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + + /trim-repeated@2.0.0: + resolution: {integrity: sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==} + engines: {node: '>=12'} + dependencies: + escape-string-regexp: 5.0.0 + dev: true + /ts-api-utils@1.3.0(typescript@5.4.3): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -2740,6 +3388,10 @@ packages: typescript: 5.4.3 dev: true + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + /tsscmp@1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} @@ -2776,6 +3428,10 @@ packages: hasBin: true dev: true + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -2830,6 +3486,13 @@ packages: fsevents: 2.3.3 dev: true + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2841,6 +3504,10 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + dev: true + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}