diff --git a/.eslintignore b/.eslintignore index a6f34fea..a9960b13 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ node_modules dist out .gitignore +migration.stub diff --git a/.prettierignore b/.prettierignore index 9b6e9df6..05d298a1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,3 @@ pnpm-lock.yaml LICENSE.md tsconfig.json tsconfig.*.json -src/main/migrations diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..852e7cc3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,68 @@ +# Security Policy + +## Purpose of the Policy + +The purpose of this Security Policy is to ensure the security of our project and maintain the trust of the community. + +## Who is Affected by the Policy + +This policy applies to all members of our project community, including developers, testers, repository administrators, and users. + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 2.0.x | :white_check_mark: | +| < 1.2.0 | :x: | + +## Development Recommendations + +### Best Practices + +- Follow secure coding principles. +- Use well-established libraries and frameworks. +- Regularly update dependencies. +- Conduct thorough testing, including security-related tests. + +### Unrecommended Practices + +- Do not use known vulnerabilities that have not been patched. +- Do not publish sensitive information such as API keys or passwords. +- Do not vote for changes that degrade the security of the project. + +### User-Generated Content + +- Ensure that user-generated content does not contain hidden threats. +- Be cautious when handling user data. + +### Community Interaction + +- Treat each other with respect and politeness. +- Do not spread spam or spam bots. +- Follow community guidelines. + +### Vulnerability Discovery and Reporting + +- If you discover a vulnerability, report it as an issue on GitHub. +- Your report should contain detailed information about the vulnerability, including steps to resolve it. + +### Reporting Method + +To report a vulnerability, create a new issue on GitHub and use branch isolation to provide details about the vulnerability. + +### Details to Provide + +Please provide the following information about the vulnerability: + +- Description of the vulnerability +- Steps to resolve the vulnerability +- Versions on which the vulnerability was found +- Code examples illustrating the vulnerability (if it is safe to do so) + +### Expected Behavior + +- If we accept the reported vulnerability, we will release a patch and update the security information on GitHub. +- If we reject the reported vulnerability, we will provide an explanation. diff --git a/package.json b/package.json index 9514f471..e3666029 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "build:mac": "electron-vite build && electron-builder --mac", "build:linux": "electron-vite build && electron-builder --linux", "prepare": "husky", - "typeorm:migration-create": "yarn typeorm migration:create" + "knex:migrate:make": "knex --knexfile src/main/knexfile.ts migrate:make --esm" }, "dependencies": { "@cospired/i18n-iso-languages": "^4.2.0", @@ -44,7 +44,7 @@ "@vanilla-extract/recipes": "^0.5.2", "auto-launch": "^5.0.6", "axios": "^1.6.8", - "better-sqlite3": "^9.5.0", + "better-sqlite3": "^11.2.1", "check-disk-space": "^3.4.0", "classnames": "^2.5.1", "color": "^4.2.3", @@ -62,6 +62,7 @@ "iso-639-1": "3.1.2", "jsdom": "^24.0.0", "jsonwebtoken": "^9.0.2", + "knex": "^3.1.0", "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", "parse-torrent": "^11.0.16", @@ -107,6 +108,7 @@ "prettier": "^3.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "ts-node": "^10.9.2", "typescript": "^5.3.3", "vite": "^5.0.12", "vite-plugin-svgr": "^4.2.0" diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e2726b79..08c9fda2 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -270,6 +270,7 @@ "pending": "Pending", "no_pending_invites": "You have no pending invites", "no_blocked_users": "You have no blocked users", - "friend_code_copied": "Friend code copied" + "friend_code_copied": "Friend code copied", + "undo_friendship_modal_text": "This will undo your friendship with {{displayName}}" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 8c7b826a..02823aa1 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -273,6 +273,7 @@ "pending": "Pendentes", "no_pending_invites": "Você não possui convites de amizade pendentes", "no_blocked_users": "Você não tem nenhum usuário bloqueado", - "friend_code_copied": "Código de amigo copiado" + "friend_code_copied": "Código de amigo copiado", + "undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}" } } diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 446ccbdc..29c72f8c 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -10,7 +10,6 @@ import { } from "@main/entity"; import { databasePath } from "./constants"; -import * as migrations from "./migrations"; export const dataSource = new DataSource({ type: "better-sqlite3", @@ -23,7 +22,6 @@ export const dataSource = new DataSource({ DownloadQueue, UserAuth, ], - synchronize: true, + synchronize: false, database: databasePath, - migrations, }); diff --git a/src/main/entity/repack.entity.ts b/src/main/entity/repack.entity.ts index ff3f16cb..36de2a7c 100644 --- a/src/main/entity/repack.entity.ts +++ b/src/main/entity/repack.entity.ts @@ -22,12 +22,6 @@ export class Repack { @Column("text", { unique: true }) magnet: string; - /** - * @deprecated Direct scraping capability has been removed - */ - @Column("int", { nullable: true }) - page: number; - @Column("text") repacker: string; diff --git a/src/main/hydra.dev.db b/src/main/hydra.dev.db new file mode 100644 index 00000000..d8c65f28 Binary files /dev/null and b/src/main/hydra.dev.db differ diff --git a/src/main/index.ts b/src/main/index.ts index e288302b..24c367fd 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,6 +9,7 @@ import { logger, PythonInstance, WindowManager } from "@main/services"; import { dataSource } from "@main/data-source"; import * as resources from "@locales"; import { userPreferencesRepository } from "@main/repository"; +import { knexClient, migrationConfig } from "./knex-client"; const { autoUpdater } = updater; @@ -52,6 +53,18 @@ if (process.defaultApp) { app.setAsDefaultProtocolClient(PROTOCOL); } +const runMigrations = async () => { + await knexClient.migrate.list(migrationConfig).then((result) => { + logger.log( + "Migrations to run:", + result[1].map((migration) => migration.name) + ); + }); + + await knexClient.migrate.latest(migrationConfig); + await knexClient.destroy(); +}; + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. @@ -63,8 +76,15 @@ app.whenReady().then(async () => { return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString()); }); + await runMigrations() + .then(() => { + logger.log("Migrations executed successfully"); + }) + .catch((err) => { + logger.log("Migrations failed to run:", err); + }); + await dataSource.initialize(); - await dataSource.runMigrations(); await import("./main"); @@ -86,10 +106,15 @@ app.on("browser-window-created", (_, window) => { const handleDeepLinkPath = (uri?: string) => { if (!uri) return; - const url = new URL(uri); - if (url.host === "install-source") { - WindowManager.redirect(`settings${url.search}`); + try { + const url = new URL(uri); + + if (url.host === "install-source") { + WindowManager.redirect(`settings${url.search}`); + } + } catch (error) { + logger.error("Error handling deep link", uri, error); } }; diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts new file mode 100644 index 00000000..031760f6 --- /dev/null +++ b/src/main/knex-client.ts @@ -0,0 +1,29 @@ +import knex, { Knex } from "knex"; +import { databasePath } from "./constants"; +import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3"; +import { RepackUris } from "./migrations/20240830143906_RepackUris"; + +export type HydraMigration = Knex.Migration & { name: string }; + +class MigrationSource implements Knex.MigrationSource { + getMigrations(): Promise { + return Promise.resolve([Hydra2_0_3, RepackUris]); + } + getMigrationName(migration: HydraMigration): string { + return migration.name; + } + getMigration(migration: HydraMigration): Promise { + return Promise.resolve(migration); + } +} + +export const knexClient = knex({ + client: "better-sqlite3", + connection: { + filename: databasePath, + }, +}); + +export const migrationConfig: Knex.MigratorConfig = { + migrationSource: new MigrationSource(), +}; diff --git a/src/main/knexfile.ts b/src/main/knexfile.ts new file mode 100644 index 00000000..df7972a9 --- /dev/null +++ b/src/main/knexfile.ts @@ -0,0 +1,10 @@ +const config = { + development: { + migrations: { + extension: "ts", + stub: "migrations/migration.stub", + }, + }, +}; + +export default config; diff --git a/src/main/migrations/1724081695967-Hydra_2_0_3.ts b/src/main/migrations/1724081695967-Hydra_2_0_3.ts deleted file mode 100644 index 5ab18acb..00000000 --- a/src/main/migrations/1724081695967-Hydra_2_0_3.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Hydra2031724081695967 implements MigrationInterface { - name = 'Hydra2031724081695967' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"))`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_source" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "url" text, "name" text NOT NULL, "etag" text, "downloadCount" integer NOT NULL DEFAULT (0), "status" text NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_aec2879321a87e9bb2ed477981a" UNIQUE ("url"))`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"))`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "user_preferences" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "downloadsPath" text, "language" text NOT NULL DEFAULT ('en'), "realDebridApiToken" text, "downloadNotificationsEnabled" boolean NOT NULL DEFAULT (0), "repackUpdatesNotificationsEnabled" boolean NOT NULL DEFAULT (0), "preferQuitInsteadOfHiding" boolean NOT NULL DEFAULT (0), "runAtStartup" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game_shop_cache" ("objectID" text PRIMARY KEY NOT NULL, "shop" text NOT NULL, "serializedData" text, "howLongToBeatSerializedData" text, "language" text, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"))`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "user_auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "userId" text NOT NULL DEFAULT (''), "displayName" text NOT NULL DEFAULT (''), "profileImageUrl" text, "accessToken" text NOT NULL DEFAULT (''), "refreshToken" text NOT NULL DEFAULT (''), "tokenExpirationTimestamp" integer NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"), CONSTRAINT "FK_0c1d6445ad047d9bbd256f961f6" FOREIGN KEY ("repackId") REFERENCES "repack" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); - await queryRunner.query(`INSERT INTO "temporary_game"("id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId") SELECT "id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId" FROM "game"`); - await queryRunner.query(`DROP TABLE "game"`); - await queryRunner.query(`ALTER TABLE "temporary_game" RENAME TO "game"`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); - await queryRunner.query(`INSERT INTO "temporary_repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`); - await queryRunner.query(`DROP TABLE "repack"`); - await queryRunner.query(`ALTER TABLE "temporary_repack" RENAME TO "repack"`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"), CONSTRAINT "FK_aed852c94d9ded617a7a07f5415" FOREIGN KEY ("gameId") REFERENCES "game" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); - await queryRunner.query(`INSERT INTO "temporary_download_queue"("id", "createdAt", "updatedAt", "gameId") SELECT "id", "createdAt", "updatedAt", "gameId" FROM "download_queue"`); - await queryRunner.query(`DROP TABLE "download_queue"`); - await queryRunner.query(`ALTER TABLE "temporary_download_queue" RENAME TO "download_queue"`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "download_queue" RENAME TO "temporary_download_queue"`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"))`); - await queryRunner.query(`INSERT INTO "download_queue"("id", "createdAt", "updatedAt", "gameId") SELECT "id", "createdAt", "updatedAt", "gameId" FROM "temporary_download_queue"`); - await queryRunner.query(`DROP TABLE "temporary_download_queue"`); - await queryRunner.query(`ALTER TABLE "repack" RENAME TO "temporary_repack"`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"))`); - await queryRunner.query(`INSERT INTO "repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`); - await queryRunner.query(`DROP TABLE "temporary_repack"`); - await queryRunner.query(`ALTER TABLE "game" RENAME TO "temporary_game"`); - await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"))`); - await queryRunner.query(`INSERT INTO "game"("id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId") SELECT "id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId" FROM "temporary_game"`); - await queryRunner.query(`DROP TABLE "temporary_game"`); - await queryRunner.query(`DROP TABLE "user_auth"`); - await queryRunner.query(`DROP TABLE "download_queue"`); - await queryRunner.query(`DROP TABLE "game_shop_cache"`); - await queryRunner.query(`DROP TABLE "user_preferences"`); - await queryRunner.query(`DROP TABLE "repack"`); - await queryRunner.query(`DROP TABLE "download_source"`); - await queryRunner.query(`DROP TABLE "game"`); - } - -} diff --git a/src/main/migrations/1724081984535-DowloadsRefactor.ts b/src/main/migrations/1724081984535-DowloadsRefactor.ts deleted file mode 100644 index 3afc8444..00000000 --- a/src/main/migrations/1724081984535-DowloadsRefactor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class DowloadsRefactor1724081984535 implements MigrationInterface { - name = 'DowloadsRefactor1724081984535' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "temporary_repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, "uris" text NOT NULL DEFAULT ('[]'), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); - await queryRunner.query(`INSERT INTO "temporary_repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`); - await queryRunner.query(`DROP TABLE "repack"`); - await queryRunner.query(`ALTER TABLE "temporary_repack" RENAME TO "repack"`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "repack" RENAME TO "temporary_repack"`); - await queryRunner.query(`CREATE TABLE "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); - await queryRunner.query(`INSERT INTO "repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`); - await queryRunner.query(`DROP TABLE "temporary_repack"`); - } - -} diff --git a/src/main/migrations/20240830143811_Hydra_2_0_3.ts b/src/main/migrations/20240830143811_Hydra_2_0_3.ts new file mode 100644 index 00000000..6013f714 --- /dev/null +++ b/src/main/migrations/20240830143811_Hydra_2_0_3.ts @@ -0,0 +1,171 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const Hydra2_0_3: HydraMigration = { + name: "Hydra_2_0_3", + up: async (knex: Knex) => { + const timestamp = new Date().getTime(); + + await knex.schema.hasTable("migrations").then(async (exists) => { + if (exists) { + await knex.schema.dropTable("migrations"); + } + }); + + await knex.schema.hasTable("download_source").then(async (exists) => { + if (!exists) { + await knex.schema.createTable("download_source", (table) => { + table.increments("id").primary(); + table + .text("url") + .unique({ indexName: "download_source_url_unique_" + timestamp }); + table.text("name").notNullable(); + table.text("etag"); + table.integer("downloadCount").notNullable().defaultTo(0); + table.text("status").notNullable().defaultTo(0); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + }); + } + }); + + await knex.schema.hasTable("repack").then(async (exists) => { + if (!exists) { + await knex.schema.createTable("repack", (table) => { + table.increments("id").primary(); + table + .text("title") + .notNullable() + .unique({ indexName: "repack_title_unique_" + timestamp }); + table + .text("magnet") + .notNullable() + .unique({ indexName: "repack_magnet_unique_" + timestamp }); + table.integer("page"); + table.text("repacker").notNullable(); + table.text("fileSize").notNullable(); + table.datetime("uploadDate").notNullable(); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + table + .integer("downloadSourceId") + .references("download_source.id") + .onDelete("CASCADE"); + }); + } + }); + + await knex.schema.hasTable("game").then(async (exists) => { + if (!exists) { + await knex.schema.createTable("game", (table) => { + table.increments("id").primary(); + table + .text("objectID") + .notNullable() + .unique({ indexName: "game_objectID_unique_" + timestamp }); + table + .text("remoteId") + .unique({ indexName: "game_remoteId_unique_" + timestamp }); + table.text("title").notNullable(); + table.text("iconUrl"); + table.text("folderName"); + table.text("downloadPath"); + table.text("executablePath"); + table.integer("playTimeInMilliseconds").notNullable().defaultTo(0); + table.text("shop").notNullable(); + table.text("status"); + table.integer("downloader").notNullable().defaultTo(1); + table.float("progress").notNullable().defaultTo(0); + table.integer("bytesDownloaded").notNullable().defaultTo(0); + table.datetime("lastTimePlayed"); + table.float("fileSize").notNullable().defaultTo(0); + table.text("uri"); + table.boolean("isDeleted").notNullable().defaultTo(0); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + table + .integer("repackId") + .references("repack.id") + .unique("repack_repackId_unique_" + timestamp); + }); + } + }); + + await knex.schema.hasTable("user_preferences").then(async (exists) => { + if (!exists) { + await knex.schema.createTable("user_preferences", (table) => { + table.increments("id").primary(); + table.text("downloadsPath"); + table.text("language").notNullable().defaultTo("en"); + table.text("realDebridApiToken"); + table + .boolean("downloadNotificationsEnabled") + .notNullable() + .defaultTo(0); + table + .boolean("repackUpdatesNotificationsEnabled") + .notNullable() + .defaultTo(0); + table.boolean("preferQuitInsteadOfHiding").notNullable().defaultTo(0); + table.boolean("runAtStartup").notNullable().defaultTo(0); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + }); + } + }); + + await knex.schema.hasTable("game_shop_cache").then(async (exists) => { + if (!exists) { + await knex.schema.createTable("game_shop_cache", (table) => { + table.text("objectID").primary().notNullable(); + table.text("shop").notNullable(); + table.text("serializedData"); + table.text("howLongToBeatSerializedData"); + table.text("language"); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + }); + } + }); + + await knex.schema.hasTable("download_queue").then(async (exists) => { + if (!exists) { + await knex.schema.createTable("download_queue", (table) => { + table.increments("id").primary(); + table + .integer("gameId") + .references("game.id") + .unique("download_queue_gameId_unique_" + timestamp); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + }); + } + }); + + await knex.schema.hasTable("user_auth").then(async (exists) => { + if (!exists) { + await knex.schema.createTable("user_auth", (table) => { + table.increments("id").primary(); + table.text("userId").notNullable().defaultTo(""); + table.text("displayName").notNullable().defaultTo(""); + table.text("profileImageUrl"); + table.text("accessToken").notNullable().defaultTo(""); + table.text("refreshToken").notNullable().defaultTo(""); + table.integer("tokenExpirationTimestamp").notNullable().defaultTo(0); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + }); + } + }); + }, + + down: async (knex: Knex) => { + await knex.schema.dropTableIfExists("game"); + await knex.schema.dropTableIfExists("repack"); + await knex.schema.dropTableIfExists("download_queue"); + await knex.schema.dropTableIfExists("user_auth"); + await knex.schema.dropTableIfExists("game_shop_cache"); + await knex.schema.dropTableIfExists("user_preferences"); + await knex.schema.dropTableIfExists("download_source"); + }, +}; diff --git a/src/main/migrations/20240830143906_RepackUris.ts b/src/main/migrations/20240830143906_RepackUris.ts new file mode 100644 index 00000000..0785d50d --- /dev/null +++ b/src/main/migrations/20240830143906_RepackUris.ts @@ -0,0 +1,58 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const RepackUris: HydraMigration = { + name: "RepackUris", + up: async (knex: Knex) => { + await knex.schema.createTable("temporary_repack", (table) => { + const timestamp = new Date().getTime(); + table.increments("id").primary(); + table + .text("title") + .notNullable() + .unique({ indexName: "repack_title_unique_" + timestamp }); + table + .text("magnet") + .notNullable() + .unique({ indexName: "repack_magnet_unique_" + timestamp }); + table.text("repacker").notNullable(); + table.text("fileSize").notNullable(); + table.datetime("uploadDate").notNullable(); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + table + .integer("downloadSourceId") + .references("download_source.id") + .onDelete("CASCADE"); + table.text("uris").notNullable().defaultTo("[]"); + }); + await knex.raw( + `INSERT INTO "temporary_repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"` + ); + await knex.schema.dropTable("repack"); + await knex.schema.renameTable("temporary_repack", "repack"); + }, + + down: async (knex: Knex) => { + await knex.schema.renameTable("repack", "temporary_repack"); + await knex.schema.createTable("repack", (table) => { + table.increments("id").primary(); + table.text("title").notNullable().unique(); + table.text("magnet").notNullable().unique(); + table.integer("page"); + table.text("repacker").notNullable(); + table.text("fileSize").notNullable(); + table.datetime("uploadDate").notNullable(); + table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); + table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); + table + .integer("downloadSourceId") + .references("download_source.id") + .onDelete("CASCADE"); + }); + await knex.raw( + `INSERT INTO "repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"` + ); + await knex.schema.dropTable("temporary_repack"); + }, +}; diff --git a/src/main/migrations/index.ts b/src/main/migrations/index.ts deleted file mode 100644 index 5546bce0..00000000 --- a/src/main/migrations/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./1724081695967-Hydra_2_0_3"; -export * from "./1724081984535-DowloadsRefactor"; diff --git a/src/main/migrations/migration.stub b/src/main/migrations/migration.stub new file mode 100644 index 00000000..9cb0cbab --- /dev/null +++ b/src/main/migrations/migration.stub @@ -0,0 +1,11 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const MigrationName: HydraMigration = { + name: "MigrationName", + up: async (knex: Knex) => { + await knex.schema.createTable("table_name", (table) => {}); + }, + + down: async (knex: Knex) => {}, +}; diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts index 67e96942..c7164d09 100644 --- a/src/main/services/how-long-to-beat.ts +++ b/src/main/services/how-long-to-beat.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { requestWebPage } from "@main/helpers"; import { HowLongToBeatCategory } from "@types"; import { formatName } from "@shared"; +import { logger } from "./logger"; export interface HowLongToBeatResult { game_id: number; @@ -13,22 +14,27 @@ export interface HowLongToBeatSearchResponse { } export const searchHowLongToBeat = async (gameName: string) => { - const response = await axios.post( - "https://howlongtobeat.com/api/search", - { - searchType: "games", - searchTerms: formatName(gameName).split(" "), - searchPage: 1, - size: 100, - }, - { - headers: { - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", - Referer: "https://howlongtobeat.com/", + const response = await axios + .post( + "https://howlongtobeat.com/api/search", + { + searchType: "games", + searchTerms: formatName(gameName).split(" "), + searchPage: 1, + size: 100, }, - } - ); + { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + Referer: "https://howlongtobeat.com/", + }, + } + ) + .catch((error) => { + logger.error("Error searching HowLongToBeat:", error?.response?.status); + return { data: { data: [] } }; + }); return response.data as HowLongToBeatSearchResponse; }; diff --git a/src/renderer/src/components/header/header.css.ts b/src/renderer/src/components/header/header.css.ts index 0e82aaef..12855986 100644 --- a/src/renderer/src/components/header/header.css.ts +++ b/src/renderer/src/components/header/header.css.ts @@ -104,6 +104,7 @@ export const section = style({ alignItems: "center", gap: `${SPACING_UNIT * 2}px`, height: "100%", + overflow: "hidden", }); export const backButton = recipe({ @@ -136,11 +137,15 @@ export const backButton = recipe({ export const title = recipe({ base: { transition: "all ease 0.2s", + overflow: "hidden", + textOverflow: "ellipsis", + width: "100%", }, variants: { hasBackButton: { true: { transform: "translateX(28px)", + width: "calc(100% - 28px)", }, }, }, diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index d3315098..c37e4b44 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -72,7 +72,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) { isWindows: window.electron.platform === "win32", })} > -
+
- {userDetails && receivedRequests.length > 0 && !gameRunning && ( -
- -
+ {showPendingRequests && ( + )} ); diff --git a/src/renderer/src/pages/user/user-confirm-undo-friendship-modal.tsx b/src/renderer/src/pages/user/user-confirm-undo-friendship-modal.tsx new file mode 100644 index 00000000..cfdb5d06 --- /dev/null +++ b/src/renderer/src/pages/user/user-confirm-undo-friendship-modal.tsx @@ -0,0 +1,40 @@ +import { Button, Modal } from "@renderer/components"; +import * as styles from "./user.css"; +import { useTranslation } from "react-i18next"; + +export interface UserConfirmUndoFriendshipModalProps { + visible: boolean; + displayName: string; + onConfirm: () => void; + onClose: () => void; +} + +export function UserConfirmUndoFriendshipModal({ + visible, + displayName, + onConfirm, + onClose, +}: UserConfirmUndoFriendshipModalProps) { + const { t } = useTranslation("user_profile"); + + return ( + +
+

{t("undo_friendship_modal_text", { displayName })}

+
+ + + +
+
+
+ ); +} diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index c334389e..f4a46ccd 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -34,6 +34,7 @@ import { UserProfileSettingsModal } from "./user-profile-settings-modal"; import { UserSignOutModal } from "./user-sign-out-modal"; import { UserFriendModalTab } from "../shared-modals/user-friend-modal"; import { UserBlockModal } from "./user-block-modal"; +import { UserConfirmUndoFriendshipModal } from "./user-confirm-undo-friendship-modal"; const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; @@ -68,6 +69,7 @@ export function UserContent({ useState(false); const [showSignOutModal, setShowSignOutModal] = useState(false); const [showUserBlockModal, setShowUserBlockModal] = useState(false); + const [showUndoFriendshipModal, setShowUndoFriendshipModal] = useState(false); const [currentGame, setCurrentGame] = useState(null); const { gameRunning } = useAppSelector((state) => state.gameRunning); @@ -213,17 +215,12 @@ export function UserContent({ } if (userProfile.relation.status === "ACCEPTED") { - const userId = - userProfile.relation.AId === userDetails?.id - ? userProfile.relation.BId - : userProfile.relation.AId; - return ( <> @@ -291,6 +288,13 @@ export function UserContent({ displayName={userProfile.displayName} /> + setShowUndoFriendshipModal(false)} + onConfirm={() => handleFriendAction(userProfile.id, "UNDO")} + displayName={userProfile.displayName} + /> +
-

{userProfile.displayName}

+

+ {userProfile.displayName} +

{currentGame && (
setForm({ ...form, displayName: e.target.value })} /> diff --git a/src/renderer/src/pages/user/user.css.ts b/src/renderer/src/pages/user/user.css.ts index 4e1c2139..6bcb30b0 100644 --- a/src/renderer/src/pages/user/user.css.ts +++ b/src/renderer/src/pages/user/user.css.ts @@ -23,6 +23,7 @@ export const profileContentBox = style({ export const profileAvatarContainer = style({ width: "96px", + minWidth: "96px", height: "96px", borderRadius: "50%", display: "flex", @@ -100,6 +101,14 @@ export const profileInformation = style({ alignItems: "flex-start", color: "#c0c1c7", zIndex: 1, + overflow: "hidden", +}); + +export const profileDisplayName = style({ + fontWeight: "bold", + overflow: "hidden", + textOverflow: "ellipsis", + width: "100%", }); export const profileContent = style({ diff --git a/yarn.lock b/yarn.lock index acb17953..6ee3491a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2875,10 +2875,10 @@ bep53-range@^2.0.0: resolved "https://registry.npmjs.org/bep53-range/-/bep53-range-2.0.0.tgz" integrity sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA== -better-sqlite3@^9.5.0: - version "9.6.0" - resolved "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz" - integrity sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ== +better-sqlite3@^11.2.1: + version "11.2.1" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.2.1.tgz#3c6b8a8e2e12444d380e811796b59c8aba012e03" + integrity sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA== dependencies: bindings "^1.5.0" prebuild-install "^7.1.1" @@ -3250,6 +3250,11 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +colorette@2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -3257,6 +3262,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz" @@ -3467,7 +3477,7 @@ dayjs@^1.11.9: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz" integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4106,6 +4116,11 @@ eslint@^8.56.0: strip-ansi "^6.0.1" text-table "^0.2.0" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" @@ -4468,6 +4483,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-stdin@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz" @@ -4494,6 +4514,11 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" +getopts@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" + integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== + git-raw-commits@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz" @@ -4914,6 +4939,11 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz" @@ -5355,6 +5385,26 @@ keyv@^4.0.0, keyv@^4.5.3: dependencies: json-buffer "3.0.1" +knex@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c" + integrity sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw== + dependencies: + colorette "2.0.19" + commander "^10.0.0" + debug "4.3.4" + escalade "^3.1.1" + esm "^3.2.25" + get-package-type "^0.1.0" + getopts "2.3.0" + interpret "^2.2.0" + lodash "^4.17.21" + pg-connection-string "2.6.2" + rechoir "^0.8.0" + resolve-from "^5.0.0" + tarn "^3.0.2" + tildify "2.0.0" + language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" @@ -5494,7 +5544,7 @@ lodash.upperfirst@^4.3.1: resolved "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz" integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== -lodash@^4.17.15: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6119,6 +6169,11 @@ pend@~1.2.0: resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== +pg-connection-string@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.2.tgz#713d82053de4e2bd166fab70cd4f26ad36aab475" + integrity sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" @@ -6462,6 +6517,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + redux-thunk@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz" @@ -6559,7 +6621,7 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.22.1: +resolve@^1.20.0, resolve@^1.22.1: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -7088,6 +7150,11 @@ tar@^6.1.12: mkdirp "^1.0.3" yallist "^4.0.0" +tarn@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" + integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== + temp-file@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz" @@ -7125,6 +7192,11 @@ thenify-all@^1.0.0: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tildify@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" + integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== + tiny-typed-emitter@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz"