From 51d378e4cb4584196b14132b4d330b8d370f8fb3 Mon Sep 17 00:00:00 2001 From: Peerawas Archavanuntakun Date: Thu, 6 Jan 2022 03:30:32 +0700 Subject: [PATCH] Setup and migrate to Prisma (#456) --- CHANGELOG.md | 2 + .../migration.sql | 17 ++ .../migration.sql | 72 ++++++++ .../migration.sql | 6 + migrations/migration_lock.toml | 3 + nodemon.json | 2 +- package.json | 11 +- schema.prisma | 49 +++++ src/bot.ts | 52 ++++-- src/commands/config.ts | 38 +++- src/commands/help.ts | 8 +- src/commands/play.ts | 13 +- src/commands/shortcuts.ts | 43 ++++- src/events/guild-create.ts | 25 ++- src/index.ts | 22 +-- src/models/file-cache.ts | 14 -- src/models/index.ts | 11 -- src/models/key-value-cache.ts | 15 -- src/models/settings.ts | 22 --- src/models/shortcut.ts | 23 --- src/scripts/migrate-and-start.ts | 83 +++++++++ src/scripts/run-with-database-url.ts | 13 ++ src/scripts/start.ts | 9 + src/services/config.ts | 1 + src/services/file-cache.ts | 85 ++++++--- src/services/key-value-cache.ts | 33 +++- src/utils/create-database-url.ts | 5 + src/utils/db.ts | 13 +- src/utils/log-banner.ts | 16 ++ yarn.lock | 172 +++++++++--------- 30 files changed, 605 insertions(+), 273 deletions(-) create mode 100644 migrations/20220101155430_migrate_from_sequelize/migration.sql create mode 100644 migrations/20220102125208_datatype_and_nullable/migration.sql create mode 100644 migrations/20220102176527_datetime_casting/migration.sql create mode 100644 migrations/migration_lock.toml create mode 100644 schema.prisma delete mode 100644 src/models/file-cache.ts delete mode 100644 src/models/index.ts delete mode 100644 src/models/key-value-cache.ts delete mode 100644 src/models/settings.ts delete mode 100644 src/models/shortcut.ts create mode 100644 src/scripts/migrate-and-start.ts create mode 100644 src/scripts/run-with-database-url.ts create mode 100644 src/scripts/start.ts create mode 100644 src/utils/create-database-url.ts create mode 100644 src/utils/log-banner.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aeaafe..6a49a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Migrated from Sequelize to Prisma. (#456) ## [0.2.1] - 2021-12-18 ### Added diff --git a/migrations/20220101155430_migrate_from_sequelize/migration.sql b/migrations/20220101155430_migrate_from_sequelize/migration.sql new file mode 100644 index 0000000..2a0809a --- /dev/null +++ b/migrations/20220101155430_migrate_from_sequelize/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE `FileCaches` (`hash` VARCHAR(255) UNIQUE PRIMARY KEY, `bytes` INTEGER, `accessedAt` DATETIME, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL); + +-- CreateTable +CREATE TABLE `KeyValueCaches` (`key` VARCHAR(255) UNIQUE PRIMARY KEY, `value` TEXT, `expiresAt` DATETIME, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL); + +-- CreateTable +CREATE TABLE `Settings` (`guildId` VARCHAR(255) UNIQUE PRIMARY KEY, `prefix` VARCHAR(255), `channel` VARCHAR(255), `finishedSetup` TINYINT(1) DEFAULT 0, `playlistLimit` INTEGER DEFAULT '50', `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL); + +-- CreateTable +CREATE TABLE `Shortcuts` (`id` INTEGER PRIMARY KEY, `guildId` VARCHAR(255), `authorId` VARCHAR(255), `shortcut` VARCHAR(255), `command` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "shortcuts_shortcut" ON "Shortcuts"("shortcut"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "shortcuts_guild_id" ON "Shortcuts"("guildId"); diff --git a/migrations/20220102125208_datatype_and_nullable/migration.sql b/migrations/20220102125208_datatype_and_nullable/migration.sql new file mode 100644 index 0000000..6105093 --- /dev/null +++ b/migrations/20220102125208_datatype_and_nullable/migration.sql @@ -0,0 +1,72 @@ +/* + Warnings: + + - You are about to alter the column `finishedSetup` on the `Settings` table. The data in that column could be lost. The data in that column will be cast from `Unsupported("tinyint(1)")` to `Boolean`. + - Made the column `expiresAt` on table `KeyValueCaches` required. This step will fail if there are existing NULL values in that column. + - Made the column `key` on table `KeyValueCaches` required. This step will fail if there are existing NULL values in that column. + - Made the column `value` on table `KeyValueCaches` required. This step will fail if there are existing NULL values in that column. + - Made the column `authorId` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column. + - Made the column `command` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column. + - Made the column `guildId` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column. + - Made the column `id` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column. + - Made the column `shortcut` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column. + - Made the column `accessedAt` on table `FileCaches` required. This step will fail if there are existing NULL values in that column. + - Made the column `bytes` on table `FileCaches` required. This step will fail if there are existing NULL values in that column. + - Made the column `hash` on table `FileCaches` required. This step will fail if there are existing NULL values in that column. + - Made the column `guildId` on table `Settings` required. This step will fail if there are existing NULL values in that column. + - Made the column `prefix` on table `Settings` required. This step will fail if there are existing NULL values in that column. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_KeyValueCaches" ( + "key" TEXT NOT NULL PRIMARY KEY, + "value" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_KeyValueCaches" ("createdAt", "expiresAt", "key", "updatedAt", "value") SELECT "createdAt", "expiresAt", "key", "updatedAt", "value" FROM "KeyValueCaches"; +DROP TABLE "KeyValueCaches"; +ALTER TABLE "new_KeyValueCaches" RENAME TO "KeyValueCache"; +CREATE TABLE "new_Shortcuts" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "guildId" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "shortcut" TEXT NOT NULL, + "command" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Shortcuts" ("authorId", "command", "createdAt", "guildId", "id", "shortcut", "updatedAt") SELECT "authorId", "command", "createdAt", "guildId", "id", "shortcut", "updatedAt" FROM "Shortcuts"; +DROP TABLE "Shortcuts"; +ALTER TABLE "new_Shortcuts" RENAME TO "Shortcut"; +CREATE INDEX "shortcuts_shortcut" ON "Shortcut"("shortcut"); +CREATE INDEX "shortcuts_guild_id" ON "Shortcut"("guildId"); +CREATE TABLE "new_FileCaches" ( + "hash" TEXT NOT NULL PRIMARY KEY, + "bytes" INTEGER NOT NULL, + "accessedAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_FileCaches" ("accessedAt", "bytes", "createdAt", "hash", "updatedAt") SELECT "accessedAt", "bytes", "createdAt", "hash", "updatedAt" FROM "FileCaches"; +DROP TABLE "FileCaches"; +ALTER TABLE "new_FileCaches" RENAME TO "FileCache"; +CREATE TABLE "new_Settings" ( + "guildId" TEXT NOT NULL PRIMARY KEY, + "prefix" TEXT NOT NULL, + "channel" TEXT, + "finishedSetup" BOOLEAN NOT NULL DEFAULT false, + "playlistLimit" INTEGER NOT NULL DEFAULT 50, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Settings" ("channel", "createdAt", "finishedSetup", "guildId", "playlistLimit", "prefix", "updatedAt") SELECT "channel", "createdAt", coalesce("finishedSetup", false) AS "finishedSetup", "guildId", coalesce("playlistLimit", 50) AS "playlistLimit", "prefix", "updatedAt" FROM "Settings"; +DROP TABLE "Settings"; +ALTER TABLE "new_Settings" RENAME TO "Setting"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; + +-- CreateIndex +CREATE INDEX "Shortcut_guildId_shortcut_idx" ON "Shortcut"("guildId", "shortcut"); \ No newline at end of file diff --git a/migrations/20220102176527_datetime_casting/migration.sql b/migrations/20220102176527_datetime_casting/migration.sql new file mode 100644 index 0000000..69f4c7f --- /dev/null +++ b/migrations/20220102176527_datetime_casting/migration.sql @@ -0,0 +1,6 @@ +-- Manual migration to cast DateTime Column from "2021-12-29 20:16:54.221 +00:00" format to timestamp + +UPDATE FileCache SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT), accessedAt = CAST(strftime('%s', accessedAt) AS INT); +UPDATE KeyValueCache SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT), expiresAt = CAST(strftime('%s', expiresAt) AS INT); +UPDATE Setting SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT); +UPDATE Shortcut SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT); \ No newline at end of file diff --git a/migrations/migration_lock.toml b/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index ee60dc6..da6a545 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,6 +1,6 @@ { "ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"], "watch": ["dist"], - "exec": "npm start", + "exec": "npm run env:set-database-url -- node --experimental-json-modules dist/src/scripts/start.js", "ext": "js" } diff --git a/package.json b/package.json index afb7fe3..cc0bf7e 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,12 @@ "build": "tsc", "build:watch": "tsc --watch", "prepack": "npm run clean && npm run build", - "start": "node --experimental-json-modules dist/src/index.js", + "start": "npm run env:set-database-url -- node --experimental-json-modules dist/src/scripts/migrate-and-start.js", "dev": "concurrently nodemon 'tsc --watch'", + "migrations:generate": "npm run prisma migrate dev", + "migrations:run": "npm run prisma migrate deploy", + "prisma": "npm run env:set-database-url prisma", + "env:set-database-url": "node dist/src/scripts/run-with-database-url.js", "release": "release-it" }, "devDependencies": { @@ -47,6 +51,7 @@ "eslint-config-xo-typescript": "^0.44.0", "husky": "^4.3.8", "nodemon": "^2.0.7", + "prisma": "^3.7.0", "release-it": "^14.11.8", "ts-node": "^10.4.0", "type-fest": "^2.8.0", @@ -77,12 +82,14 @@ "dependencies": { "@discordjs/opus": "^0.7.0", "@discordjs/voice": "^0.7.5", + "@prisma/client": "^3.7.0", "@types/libsodium-wrappers": "^0.7.9", "array-shuffle": "^3.0.0", "debug": "^4.3.3", "delay": "^5.0.0", "discord.js": "^13.3.0", "dotenv": "^8.5.1", + "execa": "^6.0.0", "fluent-ffmpeg": "^2.1.2", "fs-capacitor": "^7.0.1", "get-youtube-id": "^1.0.1", @@ -99,8 +106,6 @@ "p-limit": "^4.0.0", "p-queue": "^7.1.0", "reflect-metadata": "^0.1.13", - "sequelize": "^6.11.0", - "sequelize-typescript": "^2.1.1", "spotify-uri": "^2.2.0", "spotify-web-api-node": "^5.0.2", "sqlite3": "^5.0.2", diff --git a/schema.prisma b/schema.prisma new file mode 100644 index 0000000..95ce596 --- /dev/null +++ b/schema.prisma @@ -0,0 +1,49 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model FileCache { + hash String @id + bytes Int + accessedAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model KeyValueCache { + key String @id + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Setting { + guildId String @id + prefix String + channel String? + finishedSetup Boolean @default(false) + playlistLimit Int @default(50) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Shortcut { + id Int @id @default(autoincrement()) + guildId String + authorId String + shortcut String + command String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + + @@index([shortcut], map: "shortcuts_shortcut") + @@index([guildId], map: "shortcuts_guild_id") + @@index([guildId, shortcut]) +} diff --git a/src/bot.ts b/src/bot.ts index 5260ae0..4a97f6e 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -2,7 +2,7 @@ import {Client, Message, Collection} from 'discord.js'; import {inject, injectable} from 'inversify'; import ora from 'ora'; import {TYPES} from './types.js'; -import {Settings, Shortcut} from './models/index.js'; +import {prisma} from './utils/db.js'; import container from './inversify.config.js'; import Command from './commands/index.js'; import debug from './utils/debug.js'; @@ -21,7 +21,11 @@ export default class { private readonly token: string; private readonly commands!: Collection; - constructor(@inject(TYPES.Client) client: Client, @inject(TYPES.Services.NaturalLanguage) naturalLanguage: NaturalLanguage, @inject(TYPES.Config) config: Config) { + constructor( + @inject(TYPES.Client) client: Client, + @inject(TYPES.Services.NaturalLanguage) naturalLanguage: NaturalLanguage, + @inject(TYPES.Config) config: Config, + ) { this.client = client; this.naturalLanguage = naturalLanguage; this.token = config.DISCORD_TOKEN; @@ -33,7 +37,9 @@ export default class { container.getAll(TYPES.Command).forEach(command => { const commandNames = [command.name, ...command.aliases]; - commandNames.forEach(commandName => this.commands.set(commandName, command)); + commandNames.forEach(commandName => + this.commands.set(commandName, command), + ); }); this.client.on('messageCreate', async (msg: Message) => { @@ -42,7 +48,11 @@ export default class { return; } - const settings = await Settings.findByPk(msg.guild.id); + const settings = await prisma.setting.findUnique({ + where: { + guildId: msg.guild.id, + }, + }); if (!settings) { // Got into a bad state, send owner welcome message @@ -52,27 +62,42 @@ export default class { const {prefix, channel} = settings; - if (!msg.content.startsWith(prefix) && !msg.author.bot && msg.channel.id === channel && await this.naturalLanguage.execute(msg)) { + if ( + !msg.content.startsWith(prefix) + && !msg.author.bot + && msg.channel.id === channel + && (await this.naturalLanguage.execute(msg)) + ) { // Natural language command handled message return; } - if (!msg.content.startsWith(prefix) || msg.author.bot || msg.channel.id !== channel) { + if ( + !msg.content.startsWith(prefix) + || msg.author.bot + || msg.channel.id !== channel + ) { return; } let args = msg.content.slice(prefix.length).split(/ +/); const command = args.shift()!.toLowerCase(); - // Get possible shortcut - const shortcut = await Shortcut.findOne({where: {guildId: msg.guild.id, shortcut: command}}); + const shortcut = await prisma.shortcut.findFirst({ + where: { + guildId: msg.guild.id, + shortcut: command, + }, + }); let handler: Command; if (this.commands.has(command)) { handler = this.commands.get(command)!; } else if (shortcut) { - const possibleHandler = this.commands.get(shortcut.command.split(' ')[0]); + const possibleHandler = this.commands.get( + shortcut.command.split(' ')[0], + ); if (possibleHandler) { handler = possibleHandler; @@ -93,7 +118,9 @@ export default class { await handler.execute(msg, args); } catch (error: unknown) { debug(error); - await msg.channel.send(errorMsg((error as Error).message.toLowerCase())); + await msg.channel.send( + errorMsg((error as Error).message.toLowerCase()), + ); } }); @@ -102,7 +129,10 @@ export default class { this.client.on('ready', () => { debug(generateDependencyReport()); - spinner.succeed(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot&permissions=36752448`); + spinner.succeed( + `Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? '' + }&scope=bot&permissions=36752448`, + ); }); this.client.on('error', console.error); diff --git a/src/commands/config.ts b/src/commands/config.ts index dbe0949..20dafed 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,8 +1,8 @@ import {TextChannel, Message, GuildChannel, ThreadChannel} from 'discord.js'; import {injectable} from 'inversify'; -import {Settings} from '../models/index.js'; import errorMsg from '../utils/error-msg.js'; import Command from '.'; +import {prisma} from '../utils/db.js'; @injectable() export default class implements Command { @@ -17,9 +17,13 @@ export default class implements Command { public async execute(msg: Message, args: string []): Promise { if (args.length === 0) { // Show current settings - const settings = await Settings.findByPk(msg.guild!.id); + const settings = await prisma.setting.findUnique({ + where: { + guildId: msg.guild!.id, + }, + }); - if (settings) { + if (settings?.channel) { let response = `prefix: \`${settings.prefix}\`\n`; // eslint-disable-next-line @typescript-eslint/no-base-to-string response += `channel: ${msg.guild!.channels.cache.get(settings.channel)!.toString()}\n`; @@ -47,7 +51,14 @@ export default class implements Command { case 'prefix': { const newPrefix = args[1]; - await Settings.update({prefix: newPrefix}, {where: {guildId: msg.guild!.id}}); + await prisma.setting.update({ + where: { + guildId: msg.guild!.id, + }, + data: { + prefix: newPrefix, + }, + }); await msg.channel.send(`👍 prefix updated to \`${newPrefix}\``); break; @@ -63,7 +74,14 @@ export default class implements Command { } if (channel && channel.type === 'GUILD_TEXT') { - await Settings.update({channel: channel.id}, {where: {guildId: msg.guild!.id}}); + await prisma.setting.update({ + where: { + guildId: msg.guild!.id, + }, + data: { + channel: channel.id, + }, + }); await Promise.all([ (channel as TextChannel).send('hey apparently I\'m bound to this channel now'), @@ -83,7 +101,15 @@ export default class implements Command { return; } - await Settings.update({playlistLimit}, {where: {guildId: msg.guild!.id}}); + await prisma.setting.update({ + where: { + guildId: msg.guild!.id, + }, + data: { + playlistLimit, + }, + }); + await msg.channel.send(`👍 playlist-limit updated to ${playlistLimit}`); break; } diff --git a/src/commands/help.ts b/src/commands/help.ts index 7058efd..ebac00b 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -2,8 +2,8 @@ import {Message, Util} from 'discord.js'; import {injectable} from 'inversify'; import Command from '.'; import {TYPES} from '../types.js'; -import {Settings} from '../models/index.js'; import container from '../inversify.config.js'; +import {prisma} from '../utils/db.js'; @injectable() export default class implements Command { @@ -21,7 +21,11 @@ export default class implements Command { this.commands = container.getAll(TYPES.Command); } - const settings = await Settings.findOne({where: {guildId: msg.guild!.id}}); + const settings = await prisma.setting.findUnique({ + where: { + guildId: msg.guild!.id, + }, + }); if (!settings) { return; diff --git a/src/commands/play.ts b/src/commands/play.ts index 6561fa9..710bb06 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -10,7 +10,7 @@ import LoadingMessage from '../utils/loading-message.js'; import errorMsg from '../utils/error-msg.js'; import Command from '.'; import GetSongs from '../services/get-songs.js'; -import Settings from '../models/settings.js'; +import {prisma} from '../utils/db.js'; @injectable() export default class implements Command { @@ -41,8 +41,15 @@ export default class implements Command { // eslint-disable-next-line complexity public async execute(msg: Message, args: string[]): Promise { const [targetVoiceChannel] = getMemberVoiceChannel(msg.member!) ?? getMostPopularVoiceChannel(msg.guild!); - const settings = await Settings.findByPk(msg.guild!.id); - const {playlistLimit} = settings!; + const setting = await prisma.setting.findUnique({ + where: { + guildId: msg.guild!.id, + }}); + if (!setting) { + throw new Error(`Couldn't find settings for guild ${msg.guild!.id}`); + } + + const {playlistLimit} = setting; const res = new LoadingMessage(msg.channel as TextChannel); await res.start(); diff --git a/src/commands/shortcuts.ts b/src/commands/shortcuts.ts index 929a5c1..e40d10a 100644 --- a/src/commands/shortcuts.ts +++ b/src/commands/shortcuts.ts @@ -1,8 +1,8 @@ import {Message} from 'discord.js'; import {injectable} from 'inversify'; -import {Shortcut, Settings} from '../models/index.js'; import errorMsg from '../utils/error-msg.js'; import Command from '.'; +import {prisma} from '../utils/db.js'; @injectable() export default class implements Command { @@ -18,7 +18,11 @@ export default class implements Command { public async execute(msg: Message, args: string []): Promise { if (args.length === 0) { // Get shortcuts for guild - const shortcuts = await Shortcut.findAll({where: {guildId: msg.guild!.id}}); + const shortcuts = await prisma.shortcut.findMany({ + where: { + guildId: msg.guild!.id, + }, + }); if (shortcuts.length === 0) { await msg.channel.send('no shortcuts exist'); @@ -26,7 +30,11 @@ export default class implements Command { } // Get prefix for guild - const settings = await Settings.findOne({where: {guildId: msg.guild!.id}}); + const settings = await prisma.setting.findUnique({ + where: { + guildId: msg.guild!.id, + }, + }); if (!settings) { return; @@ -48,7 +56,12 @@ export default class implements Command { switch (action) { case 'set': { - const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}}); + const shortcut = await prisma.shortcut.findFirst({ + where: { + guildId: msg.guild!.id, + shortcut: shortcutName, + }, + }); const command = args.slice(2).join(' '); @@ -60,10 +73,15 @@ export default class implements Command { return; } - await shortcut.update(newShortcut); + await prisma.shortcut.update({ + where: { + id: shortcut.id, + }, + data: newShortcut, + }); await msg.channel.send('shortcut updated'); } else { - await Shortcut.create(newShortcut); + await prisma.shortcut.create({data: newShortcut}); await msg.channel.send('shortcut created'); } @@ -72,7 +90,12 @@ export default class implements Command { case 'delete': { // Check if shortcut exists - const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}}); + const shortcut = await prisma.shortcut.findFirst({ + where: { + guildId: msg.guild!.id, + shortcut: shortcutName, + }, + }); if (!shortcut) { await msg.channel.send(errorMsg('shortcut doesn\'t exist')); @@ -85,7 +108,11 @@ export default class implements Command { return; } - await shortcut.destroy(); + await prisma.shortcut.delete({ + where: { + id: shortcut.id, + }, + }); await msg.channel.send('shortcut deleted'); diff --git a/src/events/guild-create.ts b/src/events/guild-create.ts index 01f0910..9a26b2a 100644 --- a/src/events/guild-create.ts +++ b/src/events/guild-create.ts @@ -1,13 +1,24 @@ import {Guild, TextChannel, Message, MessageReaction, User} from 'discord.js'; import emoji from 'node-emoji'; import pEvent from 'p-event'; -import {Settings} from '../models/index.js'; import {chunk} from '../utils/arrays.js'; +import {prisma} from '../utils/db.js'; const DEFAULT_PREFIX = '!'; export default async (guild: Guild): Promise => { - await Settings.upsert({guildId: guild.id, prefix: DEFAULT_PREFIX}); + await prisma.setting.upsert({ + where: { + guildId: guild.id, + }, + create: { + guildId: guild.id, + prefix: DEFAULT_PREFIX, + }, + update: { + prefix: DEFAULT_PREFIX, + }, + }); const owner = await guild.client.users.fetch(guild.ownerId); @@ -70,7 +81,15 @@ export default async (guild: Guild): Promise => { const prefixCharacter = prefixResponses.first()!.content; // Save settings - await Settings.update({prefix: prefixCharacter, channel: chosenChannel.id}, {where: {guildId: guild.id}}); + await prisma.setting.update({ + where: { + guildId: guild.id, + }, + data: { + channel: chosenChannel.id, + prefix: prefixCharacter, + }, + }); // Send welcome const boundChannel = guild.client.channels.cache.get(chosenChannel.id) as TextChannel; diff --git a/src/index.ts b/src/index.ts index a6b6d35..e6df09d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,14 @@ import makeDir from 'make-dir'; import path from 'path'; -import {makeLines} from 'nodesplash'; import container from './inversify.config.js'; import {TYPES} from './types.js'; import Bot from './bot.js'; -import {sequelize} from './utils/db.js'; import Config from './services/config.js'; import FileCacheProvider from './services/file-cache.js'; -import metadata from '../package.json'; const bot = container.get(TYPES.Bot); -(async () => { - // Banner - console.log(makeLines({ - user: 'codetheweb', - repository: 'muse', - version: metadata.version, - paypalUser: 'codetheweb', - githubSponsor: 'codetheweb', - madeByPrefix: 'Made with 🎶 by ', - }).join('\n')); - console.log('\n'); - +const startBot = async () => { // Create data directories if necessary const config = container.get(TYPES.Config); @@ -30,9 +16,9 @@ const bot = container.get(TYPES.Bot); await makeDir(config.CACHE_DIR); await makeDir(path.join(config.CACHE_DIR, 'tmp')); - await sequelize.sync({alter: true}); - await container.get(TYPES.FileCache).cleanup(); await bot.listen(); -})(); +}; + +export {startBot}; diff --git a/src/models/file-cache.ts b/src/models/file-cache.ts deleted file mode 100644 index dbac6d3..0000000 --- a/src/models/file-cache.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {Table, Column, PrimaryKey, Model} from 'sequelize-typescript'; - -@Table -export default class FileCache extends Model { - @PrimaryKey - @Column - hash!: string; - - @Column - bytes!: number; - - @Column - accessedAt!: Date; -} diff --git a/src/models/index.ts b/src/models/index.ts deleted file mode 100644 index e3f7c0a..0000000 --- a/src/models/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import FileCache from './file-cache.js'; -import KeyValueCache from './key-value-cache.js'; -import Settings from './settings.js'; -import Shortcut from './shortcut.js'; - -export { - FileCache, - KeyValueCache, - Settings, - Shortcut, -}; diff --git a/src/models/key-value-cache.ts b/src/models/key-value-cache.ts deleted file mode 100644 index 5072538..0000000 --- a/src/models/key-value-cache.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Table, Column, PrimaryKey, Model} from 'sequelize-typescript'; -import sequelize from 'sequelize'; - -@Table -export default class KeyValueCache extends Model { - @PrimaryKey - @Column - key!: string; - - @Column(sequelize.TEXT) - value!: string; - - @Column - expiresAt!: Date; -} diff --git a/src/models/settings.ts b/src/models/settings.ts deleted file mode 100644 index f9756d3..0000000 --- a/src/models/settings.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Table, Column, PrimaryKey, Model, Default} from 'sequelize-typescript'; - -@Table -export default class Settings extends Model { - @PrimaryKey - @Column - guildId!: string; - - @Column - prefix!: string; - - @Column - channel!: string; - - @Default(false) - @Column - finishedSetup!: boolean; - - @Default(50) - @Column - playlistLimit!: number; -} diff --git a/src/models/shortcut.ts b/src/models/shortcut.ts deleted file mode 100644 index 4ec88ed..0000000 --- a/src/models/shortcut.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {Table, Column, PrimaryKey, Model, AutoIncrement, Index} from 'sequelize-typescript'; - -@Table -export default class Shortcut extends Model { - @PrimaryKey - @AutoIncrement - @Column - id!: number; - - @Column - @Index - guildId!: string; - - @Column - authorId!: string; - - @Column - @Index - shortcut!: string; - - @Column - command!: string; -} diff --git a/src/scripts/migrate-and-start.ts b/src/scripts/migrate-and-start.ts new file mode 100644 index 0000000..d971745 --- /dev/null +++ b/src/scripts/migrate-and-start.ts @@ -0,0 +1,83 @@ +// This script applies Prisma migrations +// and then starts Muse. +import dotenv from 'dotenv'; +dotenv.config(); + +import {execa, ExecaError} from 'execa'; +import {promises as fs} from 'fs'; +import Prisma from '@prisma/client'; +import ora from 'ora'; +import {startBot} from '../index.js'; +import logBanner from '../utils/log-banner.js'; +import {createDatabasePath} from '../utils/create-database-url.js'; +import {DATA_DIR} from '../services/config.js'; + +const client = new Prisma.PrismaClient(); + +const migrateFromSequelizeToPrisma = async () => { + await execa('prisma', ['migrate', 'resolve', '--applied', '20220101155430_migrate_from_sequelize'], {preferLocal: true}); +}; + +const doesUserHaveExistingDatabase = async () => { + try { + await fs.access(createDatabasePath(DATA_DIR)); + + return true; + } catch { + return false; + } +}; + +const hasDatabaseBeenMigratedToPrisma = async () => { + try { + await client.$queryRaw`SELECT COUNT(id) FROM _prisma_migrations`; + } catch (error: unknown) { + if (error instanceof Prisma.Prisma.PrismaClientKnownRequestError && error.code === 'P2010') { + // Table doesn't exist + return false; + } + + throw error; + } + + return true; +}; + +(async () => { + // Banner + logBanner(); + + const spinner = ora('Applying database migrations...').start(); + + if (await doesUserHaveExistingDatabase()) { + if (!(await hasDatabaseBeenMigratedToPrisma())) { + try { + await migrateFromSequelizeToPrisma(); + } catch (error) { + if ((error as ExecaError).stderr) { + spinner.fail('Failed to apply database migrations (going from Sequelize to Prisma):'); + console.error((error as ExecaError).stderr); + process.exit(1); + } else { + throw error; + } + } + } + } + + try { + await execa('prisma', ['migrate', 'deploy'], {preferLocal: true}); + } catch (error: unknown) { + if ((error as ExecaError).stderr) { + spinner.fail('Failed to apply database migrations:'); + console.error((error as ExecaError).stderr); + process.exit(1); + } else { + throw error; + } + } + + spinner.succeed('Database migrations applied.'); + + await startBot(); +})(); diff --git a/src/scripts/run-with-database-url.ts b/src/scripts/run-with-database-url.ts new file mode 100644 index 0000000..b7ae3a7 --- /dev/null +++ b/src/scripts/run-with-database-url.ts @@ -0,0 +1,13 @@ +import {DATA_DIR} from '../services/config.js'; +import createDatabaseUrl from '../utils/create-database-url.js'; +import {execa} from 'execa'; + +process.env.DATABASE_URL = createDatabaseUrl(DATA_DIR); + +(async () => { + await execa(process.argv[2], process.argv.slice(3), { + preferLocal: true, + stderr: process.stderr, + stdout: process.stdout, + }); +})(); diff --git a/src/scripts/start.ts b/src/scripts/start.ts new file mode 100644 index 0000000..a9e294e --- /dev/null +++ b/src/scripts/start.ts @@ -0,0 +1,9 @@ +// This script is mainly used during development. +// Starts Muse without applying database migrations. +import {startBot} from '../index.js'; +import logBanner from '../utils/log-banner.js'; + +(async () => { + logBanner(); + await startBot(); +})(); diff --git a/src/services/config.ts b/src/services/config.ts index 96b161c..e96e371 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,4 +1,5 @@ import dotenv from 'dotenv'; +import 'reflect-metadata'; import {injectable} from 'inversify'; import path from 'path'; import xbytes from 'xbytes'; diff --git a/src/services/file-cache.ts b/src/services/file-cache.ts index 1ffb824..98a079c 100644 --- a/src/services/file-cache.ts +++ b/src/services/file-cache.ts @@ -1,12 +1,12 @@ import {promises as fs, createWriteStream} from 'fs'; import path from 'path'; import {inject, injectable} from 'inversify'; -import sequelize from 'sequelize'; -import {FileCache} from '../models/index.js'; import {TYPES} from '../types.js'; import Config from './config.js'; import PQueue from 'p-queue'; import debug from '../utils/debug.js'; +import {prisma} from '../utils/db.js'; +import {FileCache} from '@prisma/client'; @injectable() export default class FileCacheProvider { @@ -23,7 +23,11 @@ export default class FileCacheProvider { * @param hash lookup key */ async getPathFor(hash: string): Promise { - const model = await FileCache.findByPk(hash); + const model = await prisma.fileCache.findUnique({ + where: { + hash, + }, + }); if (!model) { throw new Error('File is not cached'); @@ -34,12 +38,23 @@ export default class FileCacheProvider { try { await fs.access(resolvedPath); } catch (_: unknown) { - await FileCache.destroy({where: {hash}}); + await prisma.fileCache.delete({ + where: { + hash, + }, + }); throw new Error('File is not cached'); } - await model.update({accessedAt: new Date()}); + await prisma.fileCache.update({ + where: { + hash, + }, + data: { + accessedAt: new Date(), + }, + }); return resolvedPath; } @@ -64,7 +79,13 @@ export default class FileCacheProvider { try { await fs.rename(tmpPath, finalPath); - await FileCache.create({hash, bytes: stats.size, accessedAt: new Date()}); + await prisma.fileCache.create({ + data: { + hash, + accessedAt: new Date(), + bytes: stats.size, + }, + }); } catch (error) { debug('Errored when moving a finished cache file:', error); } @@ -100,14 +121,19 @@ export default class FileCacheProvider { // Continue to evict until we're under the limit /* eslint-disable no-await-in-loop */ while (totalSizeBytes > this.config.CACHE_LIMIT_IN_BYTES) { - const oldest = await FileCache.findOne({ - order: [ - ['accessedAt', 'ASC'], - ], + const oldest = await prisma.fileCache.findFirst({ + orderBy: { + accessedAt: 'asc', + }, + }); if (oldest) { - await oldest.destroy(); + await prisma.fileCache.delete({ + where: { + hash: oldest.hash, + }, + }); await fs.unlink(path.join(this.config.CACHE_DIR, oldest.hash)); debug(`${oldest.hash} has been evicted`); numOfEvictedFiles++; @@ -128,7 +154,11 @@ export default class FileCacheProvider { // Check filesystem direction (do files exist on the disk but not in the database?) for await (const dirent of await fs.opendir(this.config.CACHE_DIR)) { if (dirent.isFile()) { - const model = await FileCache.findByPk(dirent.name); + const model = await prisma.fileCache.findUnique({ + where: { + hash: dirent.name, + }, + }); if (!model) { debug(`${dirent.name} was present on disk but was not in the database. Removing from disk.`); @@ -145,7 +175,11 @@ export default class FileCacheProvider { await fs.access(filePath); } catch { debug(`${model.hash} was present in database but was not on disk. Removing from database.`); - await model.destroy(); + await prisma.fileCache.delete({ + where: { + hash: model.hash, + }, + }); } } } @@ -156,11 +190,12 @@ export default class FileCacheProvider { * @returns the total size of the cache in bytes */ private async getDiskUsageInBytes() { - const [{dataValues: {totalSizeBytes}}] = await FileCache.findAll({ - attributes: [ - [sequelize.fn('sum', sequelize.col('bytes')), 'totalSizeBytes'], - ], - }) as unknown as [{dataValues: {totalSizeBytes: number}}]; + const data = await prisma.fileCache.aggregate({ + _sum: { + bytes: true, + }, + }); + const totalSizeBytes = data._sum.bytes ?? 0; return totalSizeBytes; } @@ -176,24 +211,26 @@ export default class FileCacheProvider { let models: FileCache[] = []; const fetchNextBatch = async () => { - let where = {}; + let where; if (previousCreatedAt) { where = { createdAt: { - [sequelize.Op.gt]: previousCreatedAt, + gt: previousCreatedAt, }, }; } - models = await FileCache.findAll({ + models = await prisma.fileCache.findMany({ where, - limit, - order: ['createdAt'], + orderBy: { + createdAt: 'asc', + }, + take: limit, }); if (models.length > 0) { - previousCreatedAt = models[models.length - 1].createdAt as Date; + previousCreatedAt = models[models.length - 1].createdAt; } }; diff --git a/src/services/key-value-cache.ts b/src/services/key-value-cache.ts index 7f1164d..bfe4f8c 100644 --- a/src/services/key-value-cache.ts +++ b/src/services/key-value-cache.ts @@ -1,5 +1,5 @@ import {injectable} from 'inversify'; -import {KeyValueCache} from '../models/index.js'; +import {prisma} from '../utils/db.js'; import debug from '../utils/debug.js'; type Seconds = number; @@ -29,7 +29,11 @@ export default class KeyValueCacheProvider { throw new Error(`Cache key ${key} is too short.`); } - const cachedResult = await KeyValueCache.findByPk(key); + const cachedResult = await prisma.keyValueCache.findUnique({ + where: { + key, + }, + }); if (cachedResult) { if (new Date() < cachedResult.expiresAt) { @@ -37,7 +41,11 @@ export default class KeyValueCacheProvider { return JSON.parse(cachedResult.value) as F; } - await cachedResult.destroy(); + await prisma.keyValueCache.delete({ + where: { + key, + }, + }); } debug(`Cache miss: ${key}`); @@ -45,10 +53,21 @@ export default class KeyValueCacheProvider { const result = await func(...options as any[]); // Save result - await KeyValueCache.upsert({ - key, - value: JSON.stringify(result), - expiresAt: futureTimeToDate(expiresIn), + const value = JSON.stringify(result); + const expiresAt = futureTimeToDate(expiresIn); + await prisma.keyValueCache.upsert({ + where: { + key, + }, + update: { + value, + expiresAt, + }, + create: { + key, + value, + expiresAt, + }, }); return result; diff --git a/src/utils/create-database-url.ts b/src/utils/create-database-url.ts new file mode 100644 index 0000000..cab398e --- /dev/null +++ b/src/utils/create-database-url.ts @@ -0,0 +1,5 @@ +export const createDatabasePath = (directory: string) => `${directory}/db.sqlite`; + +const createDatabaseUrl = (directory: string) => `file:${createDatabasePath(directory)}`; + +export default createDatabaseUrl; diff --git a/src/utils/db.ts b/src/utils/db.ts index 15a2d79..b630320 100644 --- a/src/utils/db.ts +++ b/src/utils/db.ts @@ -1,12 +1,3 @@ -import {Sequelize} from 'sequelize-typescript'; -import path from 'path'; -import {DATA_DIR} from '../services/config.js'; -import {FileCache, KeyValueCache, Settings, Shortcut} from '../models/index.js'; +import Prisma from '@prisma/client'; -export const sequelize = new Sequelize({ - dialect: 'sqlite', - database: 'muse', - storage: path.join(DATA_DIR, 'db.sqlite'), - models: [FileCache, KeyValueCache, Settings, Shortcut], - logging: false, -}); +export const prisma = new Prisma.PrismaClient(); diff --git a/src/utils/log-banner.ts b/src/utils/log-banner.ts new file mode 100644 index 0000000..0ad6466 --- /dev/null +++ b/src/utils/log-banner.ts @@ -0,0 +1,16 @@ +import {makeLines} from 'nodesplash'; +import metadata from '../../package.json'; + +const logBanner = () => { + console.log(makeLines({ + user: 'codetheweb', + repository: 'muse', + version: metadata.version, + paypalUser: 'codetheweb', + githubSponsor: 'codetheweb', + madeByPrefix: 'Made with 🎶 by ', + }).join('\n')); + console.log('\n'); +}; + +export default logBanner; diff --git a/yarn.lock b/yarn.lock index 4778b27..b66aadc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -258,6 +258,23 @@ dependencies: "@octokit/openapi-types" "^11.2.0" +"@prisma/client@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.7.0.tgz#9cafc105f12635c95e9b7e7b18e8fbf52cf3f18a" + integrity sha512-fUJMvBOX5C7JPc0e3CJD6Gbelbu4dMJB4ScYpiht8HMUnRShw20ULOipTopjNtl6ekHQJ4muI7pXlQxWS9nMbw== + dependencies: + "@prisma/engines-version" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + +"@prisma/engines-version@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": + version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#055f36ac8b06c301332c14963cd0d6c795942c90" + integrity sha512-+qx2b+HK7BKF4VCa0LZ/t1QCXsu6SmvhUQyJkOD2aPpmOzket4fEnSKQZSB0i5tl7rwCDsvAiSeK8o7rf+yvwg== + +"@prisma/engines@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": + version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#12f28d5b78519fbd84c89a5bdff457ff5095e7a2" + integrity sha512-W549ub5NlgexNhR8EFstA/UwAWq3Zq0w9aNkraqsozVCt2CsX+lK4TK7IW5OZVSnxHwRjrgEAt3r9yPy8nZQRg== + "@release-it/keep-a-changelog@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@release-it/keep-a-changelog/-/keep-a-changelog-2.3.0.tgz#dfe888f97604a13c161e78f67509b296fb91ebc5" @@ -613,11 +630,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -any-promise@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -1312,11 +1324,6 @@ dotenv@^8.5.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== -dottie@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.2.tgz#cc91c0726ce3a054ebf11c55fbc92a7f266dd154" - integrity sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg== - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -1546,6 +1553,21 @@ execa@^4.0.2: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-6.0.0.tgz#598b46f09ae44f5d8097a30cfb1681d0f0371503" + integrity sha512-m4wU9j4Z9nXXoqT8RSfl28JSwmMNLFF69OON8H/lL3NeU0tNpGz313bcOfYoBBHokB0dC2tMl3VUcKgHELhL2Q== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^3.0.1" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.0.1" + onetime "^6.0.0" + signal-exit "^3.0.5" + strip-final-newline "^3.0.0" + extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -1869,7 +1891,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@7.2.0, glob@^7.0.0, glob@^7.0.3, glob@^7.1.3: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.3: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -2071,6 +2093,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" + integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ== + husky@^4.3.8: version "4.3.8" resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" @@ -2153,11 +2180,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -inflection@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.13.1.tgz#c5cadd80888a90cf84c2e96e340d7edc85d5f0cb" - integrity sha512-dldYtl2WlN0QDkIDtg8+xFwOS2Tbmp12t1cHa5/YClU6ZQjTFm7B66UcVbh9NQB+HvT5BAd2t5+yKsBkw5pcqA== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2333,6 +2355,11 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -2629,6 +2656,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -2698,18 +2730,6 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment-timezone@^0.5.31: - version "0.5.34" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" - integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.9.0", moment@^2.26.0: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -2904,6 +2924,13 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.0.1.tgz#748dd68ed7de377bb1f7132c7dafe657be5ab400" + integrity sha512-ybBJQUSyFwEEhqO2lXmyKOl9ucHtyZBWVM0h0FiMfT/+WKxCUZFa95qAR2X3w/w6oigN3B0b2UNHZbD+kdfD5w== + dependencies: + path-key "^4.0.0" + "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -2958,6 +2985,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + open@7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" @@ -3181,6 +3215,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -3196,11 +3235,6 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -pg-connection-string@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" - integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== - picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -3235,6 +3269,13 @@ prism-media@^1.3.2: resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.2.tgz#a1f04423ec15d22f3d62b1987b6a25dc49aad13b" integrity sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g== +prisma@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.7.0.tgz#9c73eeb2f16f767fdf523d0f4cc4c749734d62e2" + integrity sha512-pzgc95msPLcCHqOli7Hnabu/GRfSGSUWl5s2P6N13T/rgMB+NNeKbxCmzQiZT2yLOeLEPivV6YrW1oeQIwJxcg== + dependencies: + "@prisma/engines" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -3506,13 +3547,6 @@ restore-cursor@^4.0.0: onetime "^5.1.0" signal-exit "^3.0.2" -retry-as-promised@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543" - integrity sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg== - dependencies: - any-promise "^1.3.0" - retry@0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -3622,38 +3656,6 @@ semver@~5.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= -sequelize-pool@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-6.1.0.tgz#caaa0c1e324d3c2c3a399fed2c7998970925d668" - integrity sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg== - -sequelize-typescript@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/sequelize-typescript/-/sequelize-typescript-2.1.1.tgz#92445632062db868b760cd20215406403da737a2" - integrity sha512-4am/5O6dlAvtR/akH2KizcECm4rRAjWr+oc5mo9vFVMez8hrbOhQlDNzk0H6VMOQASCN7yBF+qOnSEN60V6/vA== - dependencies: - glob "7.2.0" - -sequelize@^6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.11.0.tgz#11620b02ab2ca02af59ec652dd22bef7ccd348af" - integrity sha512-+j3N5lr+FR1eicMRGR3bRsGOl9HMY0UGb2PyB2i1yZ64XBgsz3xejMH0UD45LcUitj40soDGIa9CyvZG0dfzKg== - dependencies: - debug "^4.1.1" - dottie "^2.0.0" - inflection "1.13.1" - lodash "^4.17.20" - moment "^2.26.0" - moment-timezone "^0.5.31" - pg-connection-string "^2.5.0" - retry-as-promised "^3.2.0" - semver "^7.3.2" - sequelize-pool "^6.0.0" - toposort-class "^1.0.1" - uuid "^8.1.0" - validator "^13.7.0" - wkx "^0.5.0" - set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -3694,7 +3696,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== -signal-exit@^3.0.3: +signal-exit@^3.0.3, signal-exit@^3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== @@ -3843,6 +3845,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -3970,11 +3977,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toposort-class@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" - integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg= - touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" @@ -4167,7 +4169,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@8.3.2, uuid@^8.1.0: +uuid@8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -4187,11 +4189,6 @@ vali-date@^1.0.0: resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6" integrity sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY= -validator@^13.7.0: - version "13.7.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" - integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -4261,13 +4258,6 @@ windows-release@^4.0.0: dependencies: execa "^4.0.2" -wkx@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" - integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== - dependencies: - "@types/node" "*" - word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"