diff --git a/.gitignore b/.gitignore index b4cc876..50a83bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ # directories -node_modules/ dist/ +node_modules/ +prisma/ # files -.env* -version -user/config.yml -user/database.sqlite +.env +*.db *.log *.lock -*-lock.* \ No newline at end of file +*-lock.* +user/config.yml \ No newline at end of file diff --git a/README.md b/README.md index 3a0707a..7f13b31 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ DISCORD_SECRET= DISCORD_TOKEN= DB_CONNECTION_URL="mysql://test:password@localhost/tickets0" +DB_PROVIDER=mysql ENCRYPTION_KEY= HTTP_BIND=8080 HTTP_EXTERNAL=http://localhost:8080 PORTAL=http://localhost:3000 -SUPER= \ No newline at end of file +SUPER= + + +https://www.prisma.io/docs/reference/database-reference/supported-databases \ No newline at end of file diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma new file mode 100644 index 0000000..8e2b303 --- /dev/null +++ b/db/mysql/schema.prisma @@ -0,0 +1,248 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DB_CONNECTION_URL") +} + +model ArchivedChannel { + channelId String @db.VarChar(19) + createdAt DateTime @default(now()) + name String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, channelId]) + @@unique([ticketId, channelId]) + @@map("archivedChannels") +} + +model ArchivedMessage { + author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) + authorId String @db.VarChar(19) + content String @db.Text + createdAt DateTime @default(now()) + deleted Boolean @default(false) + edited Boolean @default(false) + id String @id @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@map("archivedMessages") +} + +model ArchivedRole { + archivedUsers ArchivedUser[] + colour String @default("7289DA") @db.Char(6) + createdAt DateTime @default(now()) + name String + roleId String @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, roleId]) + @@unique([ticketId, roleId]) + @@map("archivedRoles") +} + +model ArchivedUser { + archivedMessages ArchivedMessage[] + avatar String + bot Boolean @default(false) + createdAt DateTime @default(now()) + discriminator String @db.Char(4) + displayName String @db.Text + role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + userId String @db.VarChar(19) + username String @db.Text + + @@id([ticketId, userId]) + @@unique([ticketId, userId]) + @@map("archivedUsers") +} + +model Category { + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String @db.VarChar(19) + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String @db.Text + pingRoles Json @default("[]") + questions Question[] + ratelimit Int? + requiredRoles Json @default("[]") + requireTopic Boolean @default(false) + staffRoles Json + tickets Ticket[] + totalLimit Int @default(-1) + + @@map("categories") +} + +model Feedback { + comment String? @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + rating Int + ticket Ticket @relation(fields: [ticketId], references: [id]) + ticketId String @unique @db.VarChar(19) + user User? @relation(fields: [userId], references: [id]) + userId String? @db.VarChar(19) + + @@map("feedback") +} + +model Guild { + autoClose Int? + autoTag Json @default("[]") + archive Boolean @default(true) + blocklist Json @default("[]") + categories Category[] + claimButton Boolean @default(false) + closeButton Boolean @default(false) + createdAt DateTime @default(now()) + errorColour String @default("Red") + feedback Feedback[] + footer String? @default("Discord Tickets by eartharoid") + id String @id @db.VarChar(19) + locale String @default("en-GB") + logChannel String? @db.VarChar(19) + primaryColour String @default("#009999") + staleAfter Int? + successColour String @default("Green") + tags Tag[] + tickets Ticket[] + workingHours Json @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]") + + @@map("guilds") +} + +model Question { + answers QuestionAnswer[] + createdAt DateTime @default(now()) + id String @id @default(uuid()) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int + label String @db.VarChar(45) + maxLength Int? @default(4000) + minLength Int? @default(0) + options Json @default("[]") + order Int + placeholder String? @db.VarChar(100) + required Boolean @default(true) + style Int @default(2) + type QuestionType @default(TEXT) + value String? @db.Text + + @@map("questions") +} + +model QuestionAnswer { + createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + questionId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @db.VarChar(19) + value String? @db.Text + + @@map("questionAnswers") +} + +model Tag { + content String @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + name String + regex String? + + @@unique([guildId, name]) + @@map("tags") +} + +model Ticket { + archivedChannels ArchivedChannel[] + archivedMessages ArchivedMessage[] + archivedRoles ArchivedRole[] + archivedUsers ArchivedUser[] + category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) + categoryId Int? + claimedBy User @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) + claimedById String @db.VarChar(19) + closedAt DateTime? + closedBy User @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) + closedById String @db.VarChar(19) + closedReason String? + createdAt DateTime @default(now()) + createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) + createdById String @db.VarChar(19) + feedback Feedback? + feedbackId Int? + firstResponseAt DateTime? + deleted Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id String @id @db.VarChar(19) + lastMessageAt DateTime? + messageCount Int? + number Int + open Boolean @default(true) + openingMessage String @db.VarChar(19) + pinnedMessages Json @default("[]") + priority TicketPriority? + referencedBy Ticket[] @relation("TicketsReferencedByTicket") + referencesMessageId String @db.VarChar(19) + referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) + referencesTicketId String? @db.VarChar(19) + topic String? + questionAnswers QuestionAnswer[] + + @@unique([guildId, number]) + @@map("tickets") +} + +model User { + createdAt DateTime @default(now()) + feedback Feedback[] + id String @id @db.VarChar(19) + messageCount Int @default(0) + ticketsCreated Ticket[] @relation("TicketsCreatedByUser") + ticketsClosed Ticket[] @relation("TicketsClosedByUser") + ticketsClaimed Ticket[] @relation("TicketsClaimedByUser") + questionAnswers QuestionAnswer[] + + @@map("users") +} + +enum TicketPriority { + LOW + MEDIUM + HIGH +} + +enum QuestionType { + MENU + TEXT +} diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma new file mode 100644 index 0000000..9b47b9a --- /dev/null +++ b/db/postgresql/schema.prisma @@ -0,0 +1,248 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DB_CONNECTION_URL") +} + +model ArchivedChannel { + channelId String @db.VarChar(19) + createdAt DateTime @default(now()) + name String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, channelId]) + @@unique([ticketId, channelId]) + @@map("archivedChannels") +} + +model ArchivedMessage { + author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) + authorId String @db.VarChar(19) + content String @db.Text + createdAt DateTime @default(now()) + deleted Boolean @default(false) + edited Boolean @default(false) + id String @id @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@map("archivedMessages") +} + +model ArchivedRole { + archivedUsers ArchivedUser[] + colour String @default("7289DA") @db.Char(6) + createdAt DateTime @default(now()) + name String + roleId String @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + + @@id([ticketId, roleId]) + @@unique([ticketId, roleId]) + @@map("archivedRoles") +} + +model ArchivedUser { + archivedMessages ArchivedMessage[] + avatar String + bot Boolean @default(false) + createdAt DateTime @default(now()) + discriminator String @db.Char(4) + displayName String @db.Text + role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String @db.VarChar(19) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + userId String @db.VarChar(19) + username String @db.Text + + @@id([ticketId, userId]) + @@unique([ticketId, userId]) + @@map("archivedUsers") +} + +model Category { + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String @db.VarChar(19) + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String @db.Text + pingRoles Json @default("[]") + questions Question[] + ratelimit Int? + requiredRoles Json @default("[]") + requireTopic Boolean @default(false) + staffRoles Json + tickets Ticket[] + totalLimit Int @default(-1) + + @@map("categories") +} + +model Feedback { + comment String? @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + rating Int + ticket Ticket @relation(fields: [ticketId], references: [id]) + ticketId String @unique @db.VarChar(19) + user User? @relation(fields: [userId], references: [id]) + userId String? @db.VarChar(19) + + @@map("feedback") +} + +model Guild { + autoClose Int? + autoTag Json @default("[]") + archive Boolean @default(true) + blocklist Json @default("[]") + categories Category[] + claimButton Boolean @default(false) + closeButton Boolean @default(false) + createdAt DateTime @default(now()) + errorColour String @default("Red") + feedback Feedback[] + footer String? @default("Discord Tickets by eartharoid") + id String @id @db.VarChar(19) + locale String @default("en-GB") + logChannel String? @db.VarChar(19) + primaryColour String @default("#009999") + staleAfter Int? + successColour String @default("Green") + tags Tag[] + tickets Ticket[] + workingHours Json @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]") + + @@map("guilds") +} + +model Question { + answers QuestionAnswer[] + createdAt DateTime @default(now()) + id String @id @default(uuid()) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int + label String @db.VarChar(45) + maxLength Int? @default(4000) + minLength Int? @default(0) + options Json @default("[]") + order Int + placeholder String? @db.VarChar(100) + required Boolean @default(true) + style Int @default(2) + type QuestionType @default(TEXT) + value String? @db.Text + + @@map("questions") +} + +model QuestionAnswer { + createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String @db.VarChar(19) + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + questionId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @db.VarChar(19) + value String? @db.Text + + @@map("questionAnswers") +} + +model Tag { + content String @db.Text + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id Int @id @default(autoincrement()) + name String + regex String? + + @@unique([guildId, name]) + @@map("tags") +} + +model Ticket { + archivedChannels ArchivedChannel[] + archivedMessages ArchivedMessage[] + archivedRoles ArchivedRole[] + archivedUsers ArchivedUser[] + category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) + categoryId Int? + claimedBy User @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) + claimedById String @db.VarChar(19) + closedAt DateTime? + closedBy User @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) + closedById String @db.VarChar(19) + closedReason String? + createdAt DateTime @default(now()) + createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) + createdById String @db.VarChar(19) + feedback Feedback? + feedbackId Int? + firstResponseAt DateTime? + deleted Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String @db.VarChar(19) + id String @id @db.VarChar(19) + lastMessageAt DateTime? + messageCount Int? + number Int + open Boolean @default(true) + openingMessage String @db.VarChar(19) + pinnedMessages Json @default("[]") + priority TicketPriority? + referencedBy Ticket[] @relation("TicketsReferencedByTicket") + referencesMessageId String @db.VarChar(19) + referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) + referencesTicketId String? @db.VarChar(19) + topic String? + questionAnswers QuestionAnswer[] + + @@unique([guildId, number]) + @@map("tickets") +} + +model User { + createdAt DateTime @default(now()) + feedback Feedback[] + id String @id @db.VarChar(19) + messageCount Int @default(0) + ticketsCreated Ticket[] @relation("TicketsCreatedByUser") + ticketsClosed Ticket[] @relation("TicketsClosedByUser") + ticketsClaimed Ticket[] @relation("TicketsClaimedByUser") + questionAnswers QuestionAnswer[] + + @@map("users") +} + +enum TicketPriority { + LOW + MEDIUM + HIGH +} + +enum QuestionType { + MENU + TEXT +} diff --git a/db/sqlite/schema.prisma b/db/sqlite/schema.prisma new file mode 100644 index 0000000..86a1222 --- /dev/null +++ b/db/sqlite/schema.prisma @@ -0,0 +1,237 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DB_CONNECTION_URL") +} + +model ArchivedChannel { + channelId String + createdAt DateTime @default(now()) + name String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + + @@id([ticketId, channelId]) + @@unique([ticketId, channelId]) + @@map("archivedChannels") +} + +model ArchivedMessage { + author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) + authorId String + content String + createdAt DateTime @default(now()) + deleted Boolean @default(false) + edited Boolean @default(false) + id String @id + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + + @@map("archivedMessages") +} + +model ArchivedRole { + archivedUsers ArchivedUser[] + colour String @default("7289DA") + createdAt DateTime @default(now()) + name String + roleId String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + + @@id([ticketId, roleId]) + @@unique([ticketId, roleId]) + @@map("archivedRoles") +} + +model ArchivedUser { + archivedMessages ArchivedMessage[] + avatar String + bot Boolean @default(false) + createdAt DateTime @default(now()) + discriminator String + displayName String + role ArchivedRole @relation(fields: [ticketId, roleId], references: [ticketId, roleId], onDelete: Cascade) + roleId String + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + userId String + username String + + @@id([ticketId, userId]) + @@unique([ticketId, userId]) + @@map("archivedUsers") +} + +model Category { + channelName String + claiming Boolean @default(false) + createdAt DateTime @default(now()) + cooldown Int? + customTopic String? + description String + discordCategory String + emoji String + enableFeedback Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id Int @id @default(autoincrement()) + image String? + memberLimit Int @default(1) + name String + openingMessage String + pingRoles String @default("[]") + questions Question[] + ratelimit Int? + requiredRoles String @default("[]") + requireTopic Boolean @default(false) + staffRoles String + tickets Ticket[] + totalLimit Int @default(-1) + + @@map("categories") +} + +model Feedback { + comment String? + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id Int @id @default(autoincrement()) + rating Int + ticket Ticket @relation(fields: [ticketId], references: [id]) + ticketId String @unique + user User? @relation(fields: [userId], references: [id]) + userId String? + + @@map("feedback") +} + +model Guild { + autoClose Int? + autoTag String @default("[]") + archive Boolean @default(true) + blocklist String @default("[]") + categories Category[] + claimButton Boolean @default(false) + closeButton Boolean @default(false) + createdAt DateTime @default(now()) + errorColour String @default("Red") + feedback Feedback[] + footer String? @default("Discord Tickets by eartharoid") + id String @id + locale String @default("en-GB") + logChannel String? + primaryColour String @default("#009999") + staleAfter Int? + successColour String @default("Green") + tags Tag[] + tickets Ticket[] + workingHours String @default("[\"UTC\", [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"], [\"00:00\",\"23:59\"]]") + + @@map("guilds") +} + +model Question { + answers QuestionAnswer[] + createdAt DateTime @default(now()) + id String @id @default(uuid()) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId Int + label String + maxLength Int? @default(4000) + minLength Int? @default(0) + options String @default("[]") + order Int + placeholder String? + required Boolean @default(true) + style Int @default(2) + type String @default("TEXT") + value String? + + @@map("questions") +} + +model QuestionAnswer { + createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + ticketId String + question Question @relation(fields: [questionId], references: [id], onDelete: Cascade) + questionId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + value String? + + @@map("questionAnswers") +} + +model Tag { + content String + createdAt DateTime @default(now()) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id Int @id @default(autoincrement()) + name String + regex String? + + @@unique([guildId, name]) + @@map("tags") +} + +model Ticket { + archivedChannels ArchivedChannel[] + archivedMessages ArchivedMessage[] + archivedRoles ArchivedRole[] + archivedUsers ArchivedUser[] + category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) + categoryId Int? + claimedBy User @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) + claimedById String + closedAt DateTime? + closedBy User @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) + closedById String + closedReason String? + createdAt DateTime @default(now()) + createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) + createdById String + feedback Feedback? + feedbackId Int? + firstResponseAt DateTime? + deleted Boolean @default(false) + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + id String @id + lastMessageAt DateTime? + messageCount Int? + number Int + open Boolean @default(true) + openingMessage String + pinnedMessages String @default("[]") + priority String? + referencedBy Ticket[] @relation("TicketsReferencedByTicket") + referencesMessageId String + referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) + referencesTicketId String? + topic String? + questionAnswers QuestionAnswer[] + + @@unique([guildId, number]) + @@map("tickets") +} + +model User { + createdAt DateTime @default(now()) + feedback Feedback[] + id String @id + messageCount Int @default(0) + ticketsCreated Ticket[] @relation("TicketsCreatedByUser") + ticketsClosed Ticket[] @relation("TicketsClosedByUser") + ticketsClaimed Ticket[] @relation("TicketsClaimedByUser") + questionAnswers QuestionAnswer[] + + @@map("users") +} diff --git a/package.json b/package.json index 7be000b..8103942 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "contributors:generate": "all-contributors generate", "keygen": "node scripts/keygen", "lint": "eslint src scripts --ext mjs --fix", - "start": "node .", + "prisma": "node scripts/prisma", + "start": "npm run prisma && node .", "studio": "npx prisma studio", "test": "echo \"There's nothing to test\" && exit 1" }, @@ -45,6 +46,7 @@ "dotenv": "^16.0.1", "fastify": "^4.2.1", "figlet": "^1.5.2", + "fs-extra": "^10.1.0", "keyv": "^4.3.3", "leeks.js": "^0.2.4", "leekslazylogger": "^4.1.7", diff --git a/scripts/prisma.js b/scripts/prisma.js new file mode 100644 index 0000000..21db72c --- /dev/null +++ b/scripts/prisma.js @@ -0,0 +1,22 @@ +require('dotenv').config(); +const fs = require('fs-extra'); +// const { promisify } = require('util'); +// const exec = promisify(require('child_process').exec); +const { spawnSync } = require('child_process'); + +const providers = ['mysql', 'postgresql', 'sqlite']; +const provider = process.env.DB_PROVIDER; +if (!providers.includes(provider)) throw new Error(`DB_PROVIDER must be one of: ${providers}`); + +if (!fs.existsSync('./prisma')) fs.mkdirSync('./prisma'); +fs.copySync(`./db/${provider}`, './prisma'); // copy schema & migrations + +npx('prisma generate'); +npx('prisma migrate deploy'); + +function npx(cmd) { + const child = spawnSync('npx', cmd.split(/\s/)); + if (child.stdout) console.log(child.stdout.toString()); + if (child.stderr) console.log(child.stderr.toString()); + if (child.status) process.exit(child.status); +} \ No newline at end of file diff --git a/src/client.js b/src/client.js index 4467174..6cb7249 100644 --- a/src/client.js +++ b/src/client.js @@ -6,6 +6,8 @@ const I18n = require('@eartharoid/i18n'); const fs = require('fs'); const { join } = require('path'); const YAML = require('yaml'); +const encryptionMiddleware = require('./lib/middleware/prisma-encryption'); +const typesMiddleware = require('./lib/middleware/prisma-types'); module.exports = class Client extends FrameworkClient { constructor(config, log) { @@ -36,6 +38,8 @@ module.exports = class Client extends FrameworkClient { async login(token) { /** @type {PrismaClient} */ this.prisma = new PrismaClient(); + this.prisma.$use(encryptionMiddleware); + this.prisma.$use(typesMiddleware); this.keyv = new Keyv(); return super.login(token); } diff --git a/src/lib/middleware/prisma-encryption.js b/src/lib/middleware/prisma-encryption.js new file mode 100644 index 0000000..138b793 --- /dev/null +++ b/src/lib/middleware/prisma-encryption.js @@ -0,0 +1,49 @@ +const Cryptr = require('cryptr'); +const cryptr = new Cryptr(process.env.ENCRYPTION_KEY); +const encryptedFields = [ + // 'name', + 'content', + 'username', + 'displayName', + // 'channelName', + // 'openingMessage', + // 'description', + 'value', + // 'placeholder', + 'closedReason', + 'topic', + 'comment', + // 'label', + // 'regex', +]; + +const encrypt = obj => { + for (const prop in obj) { + if (typeof obj[prop] === 'string' && obj[prop].length !== 0 && encryptedFields.includes(prop)) { + obj[prop] = cryptr.encrypt(obj[prop]); + } else if (typeof obj[prop] === 'object') { + obj[prop] = encrypt(obj[prop]); + } + } + return obj; +}; + +const decrypt = obj => { + for (const prop in obj) { + if (typeof obj[prop] === 'string' && obj[prop].length !== 0 && encryptedFields.includes(prop)) { + obj[prop] = cryptr.decrypt(obj[prop]); + } else if (typeof obj[prop] === 'object') { + obj[prop] = decrypt(obj[prop]); + } + } + return obj; +}; + +module.exports = async (params, next) => { + if (params.args.create) params.args.create = encrypt(params.args.create); + if (params.args.data) params.args.data = encrypt(params.args.data); + if (params.args.update) params.args.update = encrypt(params.args.update); + let result = await next(params); + if (result) result = decrypt(result); + return result; +}; \ No newline at end of file diff --git a/src/lib/middleware/prisma-types.js b/src/lib/middleware/prisma-types.js new file mode 100644 index 0000000..0718f14 --- /dev/null +++ b/src/lib/middleware/prisma-types.js @@ -0,0 +1,35 @@ +const jsonFields = [ + 'pingRoles', + 'requiredRoles', + 'staffRoles', + 'autoTag', + 'blocklist', + 'workingHours', + 'options', + 'pinnedMessages', +]; + +const traverse = (obj, func) => { + for (const prop in obj) { + console.log(prop, typeof obj[prop], obj[prop]); + if (jsonFields.includes(prop) && obj[prop] !== null && obj[prop] !== undefined) { + obj[prop] = func(obj[prop]); + } else if (typeof obj[prop] === 'object') { + obj[prop] = traverse(obj[prop], func); + } + } + return obj; +}; + +module.exports = async (params, next) => { + if (process.env.DB_PROVIDER === 'sqlite') { + if (params.args.create) params.args.create = traverse(params.args.create, val => JSON.stringify(val)); + if (params.args.data) params.args.data = traverse(params.args.data, val => JSON.stringify(val)); + if (params.args.update) params.args.update = traverse(params.args.update, val => JSON.stringify(val)); + let result = await next(params); + if (result) result = traverse(result, val => JSON.parse(val)); + return result; + } else { + return await next(params); + } +}; \ No newline at end of file diff --git a/src/routes/api/admin/guilds/[guild]/categories/index.js b/src/routes/api/admin/guilds/[guild]/categories/index.js index ec9a5a0..b95dee7 100644 --- a/src/routes/api/admin/guilds/[guild]/categories/index.js +++ b/src/routes/api/admin/guilds/[guild]/categories/index.js @@ -78,7 +78,8 @@ module.exports.post = fastify => ({ data: { guild: { connect: { id: guild.id } }, ...data, - questions: { createMany: { data: data.questions ?? [] } }, + // questions: { createMany: { data: data.questions ?? [] } }, + questions: { create: { data: data.questions ?? [] } }, // sqlite doesn't support createMany? }, });