This commit is contained in:
Thales Euflauzino 2024-09-02 13:27:08 -03:00
commit 74d3aa10cb
28 changed files with 575 additions and 147 deletions

View File

@ -2,3 +2,4 @@ node_modules
dist
out
.gitignore
migration.stub

View File

@ -5,4 +5,3 @@ pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
src/main/migrations

68
SECURITY.md Normal file
View File

@ -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.

View File

@ -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"

View File

@ -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}}"
}
}

View File

@ -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}}"
}
}

View File

@ -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,
});

View File

@ -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;

BIN
src/main/hydra.dev.db Normal file

Binary file not shown.

View File

@ -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);
}
};

29
src/main/knex-client.ts Normal file
View File

@ -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<HydraMigration> {
getMigrations(): Promise<HydraMigration[]> {
return Promise.resolve([Hydra2_0_3, RepackUris]);
}
getMigrationName(migration: HydraMigration): string {
return migration.name;
}
getMigration(migration: HydraMigration): Promise<Knex.Migration> {
return Promise.resolve(migration);
}
}
export const knexClient = knex({
client: "better-sqlite3",
connection: {
filename: databasePath,
},
});
export const migrationConfig: Knex.MigratorConfig = {
migrationSource: new MigrationSource(),
};

10
src/main/knexfile.ts Normal file
View File

@ -0,0 +1,10 @@
const config = {
development: {
migrations: {
extension: "ts",
stub: "migrations/migration.stub",
},
},
};
export default config;

View File

@ -1,50 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Hydra2031724081695967 implements MigrationInterface {
name = 'Hydra2031724081695967'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@ -1,20 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class DowloadsRefactor1724081984535 implements MigrationInterface {
name = 'DowloadsRefactor1724081984535'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@ -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");
},
};

View File

@ -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");
},
};

View File

@ -1,2 +0,0 @@
export * from "./1724081695967-Hydra_2_0_3";
export * from "./1724081984535-DowloadsRefactor";

View File

@ -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) => {},
};

View File

@ -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;
};

View File

@ -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)",
},
},
},

View File

@ -72,7 +72,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
isWindows: window.electron.platform === "win32",
})}
>
<section className={styles.section}>
<section className={styles.section} style={{ flex: 1 }}>
<button
type="button"
className={styles.backButton({

View File

@ -7,22 +7,24 @@ export const profileContainerBackground = createVar();
export const profileContainer = style({
background: profileContainerBackground,
position: "relative",
display: "flex",
gap: `${SPACING_UNIT}px`,
cursor: "pointer",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
borderBottom: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
});
export const profileButton = style({
display: "flex",
cursor: "pointer",
transition: "all ease 0.1s",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
color: vars.color.muted,
borderBottom: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
width: "100%",
zIndex: "10",
overflow: "hidden",
});
export const profileButtonContent = style({
@ -75,16 +77,6 @@ export const profileButtonTitle = style({
whiteSpace: "nowrap",
});
export const friendRequestContainer = style({
position: "absolute",
padding: "8px",
right: `${SPACING_UNIT}px`,
display: "flex",
top: 0,
bottom: 0,
alignItems: "center",
});
export const friendRequestButton = style({
color: vars.color.success,
cursor: "pointer",

View File

@ -41,6 +41,9 @@ export function SidebarProfile() {
return undefined;
}, [profileBackground]);
const showPendingRequests =
userDetails && receivedRequests.length > 0 && !gameRunning;
return (
<div
className={styles.profileContainer}
@ -88,19 +91,17 @@ export function SidebarProfile() {
)}
</div>
</button>
{userDetails && receivedRequests.length > 0 && !gameRunning && (
<div className={styles.friendRequestContainer}>
<button
type="button"
className={styles.friendRequestButton}
onClick={() =>
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
}
>
<PersonAddIcon size={24} />
{receivedRequests.length}
</button>
</div>
{showPendingRequests && (
<button
type="button"
className={styles.friendRequestButton}
onClick={() =>
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
}
>
<PersonAddIcon size={24} />
{receivedRequests.length}
</button>
)}
</div>
);

View File

@ -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 (
<Modal
visible={visible}
title={t("sign_out_modal_title")}
onClose={onClose}
>
<div className={styles.signOutModalContent}>
<p>{t("undo_friendship_modal_text", { displayName })}</p>
<div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger">
{t("undo_friendship")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
</Button>
</div>
</div>
</Modal>
);
}

View File

@ -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<GameRunning | null>(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 (
<>
<Button
theme="outline"
className={styles.cancelRequestButton}
onClick={() => handleFriendAction(userId, "UNDO")}
onClick={() => setShowUndoFriendshipModal(true)}
>
<XCircleIcon size={28} /> {t("undo_friendship")}
</Button>
@ -291,6 +288,13 @@ export function UserContent({
displayName={userProfile.displayName}
/>
<UserConfirmUndoFriendshipModal
visible={showUndoFriendshipModal}
onClose={() => setShowUndoFriendshipModal(false)}
onConfirm={() => handleFriendAction(userProfile.id, "UNDO")}
displayName={userProfile.displayName}
/>
<section
className={styles.profileContentBox}
style={{
@ -328,7 +332,9 @@ export function UserContent({
</div>
<div className={styles.profileInformation}>
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
<h2 className={styles.profileDisplayName}>
{userProfile.displayName}
</h2>
{currentGame && (
<div
style={{

View File

@ -126,6 +126,7 @@ export const UserEditProfile = ({
value={form.displayName}
required
minLength={3}
maxLength={50}
containerProps={{ style: { width: "100%" } }}
onChange={(e) => setForm({ ...form, displayName: e.target.value })}
/>

View File

@ -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({

View File

@ -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"