From 24ec13c2ab76f73d4e18f1f4cc9ea369439b105d Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Tue, 12 Mar 2024 21:23:45 +0100 Subject: [PATCH] feat: add mysql support (#212) * feat: add mysql support * fix: lockfile broken * fix: ci issues * fix: ci issues * fix: ci issues --- .env.example | 15 +- apps/nestjs/tsconfig.json | 2 +- apps/nextjs/src/env.mjs | 21 ++- packages/db/driver.ts | 52 +++++++ packages/db/index.ts | 10 +- packages/db/package.json | 3 +- packages/db/schema/mysql.ts | 260 ++++++++++++++++++++++++++++++++ packages/db/test/schema.spec.ts | 152 +++++++++++++++++++ pnpm-lock.yaml | 64 +++++++- 9 files changed, 565 insertions(+), 14 deletions(-) create mode 100644 packages/db/driver.ts create mode 100644 packages/db/schema/mysql.ts create mode 100644 packages/db/test/schema.spec.ts diff --git a/.env.example b/.env.example index 568c29638..dc82ea23a 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,22 @@ # This file will be committed to version control, so make sure not to have any secrets in it. # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. -# The database URL is used to connect to your PlanetScale database. +# This is how you can use the sqlite driver: +DB_DRIVER='better-sqlite3' DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE' +# Those are the two ways to use the mysql2 driver: +# 1. Using the URL format: +# DB_DRIVER='mysql2' +# DB_URL='mysql://user:password@host:port/database' +# 2. Using the connection options format: +# DB_DRIVER='mysql2' +# DB_HOST='localhost' +# DB_PORT='3306' +# DB_USER='username' +# DB_PASSWORD='password' +# DB_NAME='name-of-database' + # @see https://next-auth.js.org/configuration/options#nextauth_url AUTH_URL='http://localhost:3000' diff --git a/apps/nestjs/tsconfig.json b/apps/nestjs/tsconfig.json index 6e7becceb..575ce9cc4 100644 --- a/apps/nestjs/tsconfig.json +++ b/apps/nestjs/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "commonjs", + "module": "CommonJS", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, diff --git a/apps/nextjs/src/env.mjs b/apps/nextjs/src/env.mjs index c4d2d4e1e..7442d5130 100644 --- a/apps/nextjs/src/env.mjs +++ b/apps/nextjs/src/env.mjs @@ -1,6 +1,10 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; +const isUsingDbUrl = Boolean(process.env.DB_URL); +const isUsingDbHost = Boolean(process.env.DB_HOST); +const isUsingDbCredentials = process.env.DB_DRIVER === "mysql2"; + export const env = createEnv({ shared: { VERCEL_URL: z @@ -14,7 +18,16 @@ export const env = createEnv({ * built with invalid env vars. */ server: { - DB_URL: z.string(), + DB_DRIVER: z.enum(["better-sqlite3", "mysql2"]).default("better-sqlite3"), + // If the DB_HOST is set, the DB_URL is optional + DB_URL: isUsingDbHost ? z.string().optional() : z.string(), + DB_HOST: isUsingDbUrl ? z.string().optional() : z.string(), + DB_PORT: isUsingDbUrl + ? z.number().optional() + : z.number().min(1).default(3306), + DB_USER: isUsingDbCredentials ? z.string() : z.string().optional(), + DB_PASSWORD: isUsingDbCredentials ? z.string() : z.string().optional(), + DB_NAME: isUsingDbUrl ? z.string().optional() : z.string(), }, /** * Specify your client-side environment variables schema here. @@ -30,6 +43,12 @@ export const env = createEnv({ VERCEL_URL: process.env.VERCEL_URL, PORT: process.env.PORT, DB_URL: process.env.DB_URL, + DB_HOST: process.env.DB_HOST, + DB_USER: process.env.DB_USER, + DB_PASSWORD: process.env.DB_PASSWORD, + DB_NAME: process.env.DB_NAME, + DB_PORT: process.env.DB_PORT, + DB_DRIVER: process.env.DB_DRIVER, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, skipValidation: diff --git a/packages/db/driver.ts b/packages/db/driver.ts new file mode 100644 index 000000000..37d924d37 --- /dev/null +++ b/packages/db/driver.ts @@ -0,0 +1,52 @@ +import Database from "better-sqlite3"; +import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; +import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3"; +import { drizzle as drizzleMysql } from "drizzle-orm/mysql2"; +import mysql from "mysql2"; + +import * as mysqlSchema from "./schema/mysql"; +import * as sqliteSchema from "./schema/sqlite"; + +type HomarrDatabase = BetterSQLite3Database; + +const init = () => { + if (!connection) { + switch (process.env.DB_DRIVER) { + case "mysql2": + initMySQL2(); + break; + default: + initBetterSqlite(); + break; + } + } +}; + +export let connection: Database.Database | mysql.Connection; +export let database: HomarrDatabase; + +const initBetterSqlite = () => { + connection = new Database(process.env.DB_URL); + database = drizzleSqlite(connection, { schema: sqliteSchema }); +}; + +const initMySQL2 = () => { + if (process.env.DB_URL) { + connection = mysql.createConnection({ uri: process.env.DB_URL }); + } else { + connection = mysql.createConnection({ + host: process.env.DB_HOST!, + database: process.env.DB_NAME!, + port: Number(process.env.DB_PORT), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + }); + } + + database = drizzleMysql(connection, { + schema: mysqlSchema, + mode: "default", + }) as unknown as HomarrDatabase; +}; + +init(); diff --git a/packages/db/index.ts b/packages/db/index.ts index 54ce46b92..beec11606 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -1,17 +1,15 @@ import Database from "better-sqlite3"; -import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; -import { drizzle } from "drizzle-orm/better-sqlite3"; +import { database } from "./driver"; import * as sqliteSchema from "./schema/sqlite"; +// Export only the types from the sqlite schema as we're using that. export const schema = sqliteSchema; export * from "drizzle-orm"; -export const sqlite = new Database(process.env.DB_URL); +export const db = database; -export const db = drizzle(sqlite, { schema }); - -export type Database = BetterSQLite3Database; +export type Database = typeof db; export { createId } from "@paralleldrive/cuid2"; diff --git a/packages/db/package.json b/packages/db/package.json index a3bbf9f0b..7092febdd 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -26,7 +26,8 @@ "@homarr/definitions": "workspace:^0.1.0", "@paralleldrive/cuid2": "^2.2.2", "better-sqlite3": "^9.4.3", - "drizzle-orm": "^0.30.1" + "drizzle-orm": "^0.30.1", + "mysql2": "^3.9.2" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts new file mode 100644 index 000000000..52da4f0e0 --- /dev/null +++ b/packages/db/schema/mysql.ts @@ -0,0 +1,260 @@ +import type { AdapterAccount } from "@auth/core/adapters"; +import { relations } from "drizzle-orm"; +import { + boolean, + index, + int, + mysqlTable, + primaryKey, + text, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; + +import type { + BackgroundImageAttachment, + BackgroundImageRepeat, + BackgroundImageSize, + IntegrationKind, + IntegrationSecretKind, + SectionKind, + WidgetKind, +} from "@homarr/definitions"; +import { + backgroundImageAttachments, + backgroundImageRepeats, + backgroundImageSizes, +} from "@homarr/definitions"; + +export const users = mysqlTable("user", { + id: varchar("id", { length: 256 }).notNull().primaryKey(), + name: text("name"), + email: text("email"), + emailVerified: timestamp("emailVerified"), + image: text("image"), + password: text("password"), + salt: text("salt"), +}); + +export const accounts = mysqlTable( + "account", + { + userId: varchar("userId", { length: 256 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").$type().notNull(), + provider: varchar("provider", { length: 256 }).notNull(), + providerAccountId: varchar("providerAccountId", { length: 256 }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: int("expires_at"), + token_type: text("token_type"), + scope: text("scope"), + id_token: text("id_token"), + session_state: text("session_state"), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("userId_idx").on(account.userId), + }), +); + +export const sessions = mysqlTable( + "session", + { + sessionToken: varchar("sessionToken", { length: 512 }) + .notNull() + .primaryKey(), + userId: varchar("userId", { length: 256 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires: timestamp("expires").notNull(), + }, + (session) => ({ + userIdIdx: index("user_id_idx").on(session.userId), + }), +); + +export const verificationTokens = mysqlTable( + "verificationToken", + { + identifier: varchar("identifier", { length: 256 }).notNull(), + token: varchar("token", { length: 512 }).notNull(), + expires: timestamp("expires").notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }), +); + +export const integrations = mysqlTable( + "integration", + { + id: varchar("id", { length: 256 }).notNull().primaryKey(), + name: text("name").notNull(), + url: text("url").notNull(), + kind: varchar("kind", { length: 128 }).$type().notNull(), + }, + (i) => ({ + kindIdx: index("integration__kind_idx").on(i.kind), + }), +); + +export const integrationSecrets = mysqlTable( + "integrationSecret", + { + kind: varchar("kind", { length: 16 }) + .$type() + .notNull(), + value: text("value").$type<`${string}.${string}`>().notNull(), + updatedAt: timestamp("updated_at").notNull(), + integrationId: varchar("integration_id", { length: 256 }) + .notNull() + .references(() => integrations.id, { onDelete: "cascade" }), + }, + (is) => ({ + compoundKey: primaryKey({ + columns: [is.integrationId, is.kind], + }), + kindIdx: index("integration_secret__kind_idx").on(is.kind), + updatedAtIdx: index("integration_secret__updated_at_idx").on(is.updatedAt), + }), +); + +export const boards = mysqlTable("board", { + id: varchar("id", { length: 256 }).notNull().primaryKey(), + name: varchar("name", { length: 256 }).unique().notNull(), + isPublic: boolean("is_public").default(false).notNull(), + pageTitle: text("page_title"), + metaTitle: text("meta_title"), + logoImageUrl: text("logo_image_url"), + faviconImageUrl: text("favicon_image_url"), + backgroundImageUrl: text("background_image_url"), + backgroundImageAttachment: text("background_image_attachment") + .$type() + .default(backgroundImageAttachments.defaultValue) + .notNull(), + backgroundImageRepeat: text("background_image_repeat") + .$type() + .default(backgroundImageRepeats.defaultValue) + .notNull(), + backgroundImageSize: text("background_image_size") + .$type() + .default(backgroundImageSizes.defaultValue) + .notNull(), + primaryColor: text("primary_color").default("#fa5252").notNull(), + secondaryColor: text("secondary_color").default("#fd7e14").notNull(), + opacity: int("opacity").default(100).notNull(), + customCss: text("custom_css"), + columnCount: int("column_count").default(10).notNull(), +}); + +export const sections = mysqlTable("section", { + id: varchar("id", { length: 256 }).notNull().primaryKey(), + boardId: varchar("board_id", { length: 256 }) + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + kind: text("kind").$type().notNull(), + position: int("position").notNull(), + name: text("name"), +}); + +export const items = mysqlTable("item", { + id: varchar("id", { length: 256 }).notNull().primaryKey(), + sectionId: varchar("section_id", { length: 256 }) + .notNull() + .references(() => sections.id, { onDelete: "cascade" }), + kind: text("kind").$type().notNull(), + xOffset: int("x_offset").notNull(), + yOffset: int("y_offset").notNull(), + width: int("width").notNull(), + height: int("height").notNull(), + options: text("options").default('{"json": {}}').notNull(), // empty superjson object +}); + +export const apps = mysqlTable("app", { + id: varchar("id", { length: 256 }).notNull().primaryKey(), + name: text("name").notNull(), + description: text("description"), + iconUrl: text("icon_url").notNull(), + href: text("href"), +}); + +export const integrationItems = mysqlTable( + "integration_item", + { + itemId: varchar("item_id", { length: 256 }) + .notNull() + .references(() => items.id, { onDelete: "cascade" }), + integrationId: varchar("integration_id", { length: 256 }) + .notNull() + .references(() => integrations.id, { onDelete: "cascade" }), + }, + (table) => ({ + compoundKey: primaryKey({ + columns: [table.itemId, table.integrationId], + }), + }), +); + +export const accountRelations = relations(accounts, ({ one }) => ({ + user: one(users, { + fields: [accounts.userId], + references: [users.id], + }), +})); + +export const userRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), +})); + +export const integrationRelations = relations(integrations, ({ many }) => ({ + secrets: many(integrationSecrets), + items: many(integrationItems), +})); + +export const integrationSecretRelations = relations( + integrationSecrets, + ({ one }) => ({ + integration: one(integrations, { + fields: [integrationSecrets.integrationId], + references: [integrations.id], + }), + }), +); + +export const boardRelations = relations(boards, ({ many }) => ({ + sections: many(sections), +})); + +export const sectionRelations = relations(sections, ({ many, one }) => ({ + items: many(items), + board: one(boards, { + fields: [sections.boardId], + references: [boards.id], + }), +})); + +export const itemRelations = relations(items, ({ one, many }) => ({ + section: one(sections, { + fields: [items.sectionId], + references: [sections.id], + }), + integrations: many(integrationItems), +})); + +export const integrationItemRelations = relations( + integrationItems, + ({ one }) => ({ + integration: one(integrations, { + fields: [integrationItems.integrationId], + references: [integrations.id], + }), + item: one(items, { + fields: [integrationItems.itemId], + references: [items.id], + }), + }), +); diff --git a/packages/db/test/schema.spec.ts b/packages/db/test/schema.spec.ts new file mode 100644 index 000000000..77074734c --- /dev/null +++ b/packages/db/test/schema.spec.ts @@ -0,0 +1,152 @@ +import type { Column, InferSelectModel } from "drizzle-orm"; +import type { + ForeignKey as MysqlForeignKey, + MySqlTableWithColumns, +} from "drizzle-orm/mysql-core"; +import type { + ForeignKey as SqliteForeignKey, + SQLiteTableWithColumns, +} from "drizzle-orm/sqlite-core"; +import { expect, expectTypeOf, test } from "vitest"; + +import { objectEntries } from "@homarr/common"; + +import * as mysqlSchema from "../schema/mysql"; +import * as sqliteSchema from "../schema/sqlite"; + +test("schemas should match", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + + objectEntries(sqliteSchema).forEach(([tableName, sqliteTable]) => { + Object.entries(sqliteTable).forEach( + ([columnName, sqliteColumn]: [string, object]) => { + if (!("isUnique" in sqliteColumn)) return; + if (!("uniqueName" in sqliteColumn)) return; + if (!("primary" in sqliteColumn)) return; + + const mysqlTable = mysqlSchema[tableName]; + + const mysqlColumn = mysqlTable[ + columnName as keyof typeof mysqlTable + ] as object; + if (!("isUnique" in mysqlColumn)) return; + if (!("uniqueName" in mysqlColumn)) return; + if (!("primary" in mysqlColumn)) return; + + expect( + sqliteColumn.isUnique, + `expect unique of column ${columnName} in table ${tableName} to be the same for both schemas`, + ).toEqual(mysqlColumn.isUnique); + expect( + sqliteColumn.uniqueName, + `expect uniqueName of column ${columnName} in table ${tableName} to be the same for both schemas`, + ).toEqual(mysqlColumn.uniqueName); + expect( + sqliteColumn.primary, + `expect primary of column ${columnName} in table ${tableName} to be the same for both schemas`, + ).toEqual(mysqlColumn.primary); + }, + ); + + const mysqlTable = mysqlSchema[tableName as keyof typeof mysqlSchema]; + const sqliteForeignKeys = sqliteTable[ + Symbol.for("drizzle:SQLiteInlineForeignKeys") as keyof typeof sqliteTable + ] as SqliteForeignKey[] | undefined; + const mysqlForeignKeys = mysqlTable[ + Symbol.for("drizzle:MySqlInlineForeignKeys") as keyof typeof mysqlTable + ] as MysqlForeignKey[] | undefined; + + if (!sqliteForeignKeys && !mysqlForeignKeys) return; + + expect( + mysqlForeignKeys, + `mysql foreign key for ${tableName} to be defined`, + ).toBeDefined(); + expect( + sqliteForeignKeys, + `sqlite foreign key for ${tableName} to be defined`, + ).toBeDefined(); + + expect( + sqliteForeignKeys!.length, + `expect number of foreign keys in table ${tableName} to be the same for both schemas`, + ).toEqual(mysqlForeignKeys!.length); + + sqliteForeignKeys?.forEach((sqliteForeignKey) => { + sqliteForeignKey.getName(); + const mysqlForeignKey = mysqlForeignKeys!.find( + (key) => key.getName() === sqliteForeignKey.getName(), + ); + expect( + mysqlForeignKey, + `expect foreign key ${sqliteForeignKey.getName()} to be defined in mysql schema`, + ).toBeDefined(); + + expect( + sqliteForeignKey.onDelete, + `expect foreign key (${sqliteForeignKey.getName()}) onDelete to be the same for both schemas`, + ).toEqual(mysqlForeignKey!.onDelete); + + expect( + sqliteForeignKey.onUpdate, + `expect foreign key (${sqliteForeignKey.getName()}) onUpdate to be the same for both schemas`, + ).toEqual(mysqlForeignKey!.onUpdate); + + sqliteForeignKey.reference().foreignColumns.forEach((column) => { + expect( + mysqlForeignKey!.reference().foreignColumns.map((x) => x.name), + `expect foreign key (${sqliteForeignKey.getName()}) columns to be the same for both schemas`, + ).toContainEqual(column.name); + }); + + expect( + Object.keys(sqliteForeignKey.reference().foreignTable), + `expect foreign key (${sqliteForeignKey.getName()}) table to be the same for both schemas`, + ).toEqual(Object.keys(mysqlForeignKey!.reference().foreignTable)); + }); + }); +}); + +type SqliteTables = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof typeof sqliteSchema]: (typeof sqliteSchema)[K] extends SQLiteTableWithColumns + ? InferSelectModel<(typeof sqliteSchema)[K]> + : never; +}; +type MysqlTables = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof typeof mysqlSchema]: (typeof mysqlSchema)[K] extends MySqlTableWithColumns + ? InferSelectModel<(typeof mysqlSchema)[K]> + : never; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type InferColumnConfig> = + T extends Column + ? Omit + : never; + +type SqliteConfig = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof typeof sqliteSchema]: (typeof sqliteSchema)[K] extends SQLiteTableWithColumns + ? { + [C in keyof (typeof sqliteSchema)[K]["_"]["config"]["columns"]]: InferColumnConfig< + (typeof sqliteSchema)[K]["_"]["config"]["columns"][C] + >; + } + : never; +}; + +type MysqlConfig = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof typeof mysqlSchema]: (typeof mysqlSchema)[K] extends MySqlTableWithColumns + ? { + [C in keyof (typeof mysqlSchema)[K]["_"]["config"]["columns"]]: InferColumnConfig< + (typeof mysqlSchema)[K]["_"]["config"]["columns"][C] + >; + } + : never; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1a85134e..38d934a79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,7 +428,10 @@ importers: version: 9.4.3 drizzle-orm: specifier: ^0.30.1 - version: 0.30.1(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(react@17.0.2) + version: 0.30.1(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(mysql2@3.9.2)(react@17.0.2) + mysql2: + specifier: ^3.9.2 + version: 3.9.2 devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -4637,6 +4640,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -4768,7 +4776,7 @@ packages: - supports-color dev: true - /drizzle-orm@0.30.1(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(react@17.0.2): + /drizzle-orm@0.30.1(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.3)(mysql2@3.9.2)(react@17.0.2): resolution: {integrity: sha512-5P6CXl4XyWtDDiYOX/jYOJp1HTUmBlXRAwaq+muUOgaSykMEy5sJesCxceMT0oCGvxeWkKfSXo5owLnfKwCIdw==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' @@ -4844,6 +4852,7 @@ packages: dependencies: '@types/better-sqlite3': 7.6.9 better-sqlite3: 9.4.3 + mysql2: 3.9.2 react: 17.0.2 dev: false @@ -5766,6 +5775,12 @@ packages: wide-align: 1.1.5 dev: false + /generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + dependencies: + is-property: 1.0.2 + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6109,7 +6124,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - dev: true /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6396,6 +6410,10 @@ packages: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: true + /is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + dev: false + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -6821,6 +6839,10 @@ packages: triple-beam: 1.4.1 dev: false + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -6861,7 +6883,11 @@ packages: /lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - dev: true + + /lru-cache@8.0.5: + resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} + engines: {node: '>=16.14'} + dev: false /lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} @@ -7170,6 +7196,27 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /mysql2@3.9.2: + resolution: {integrity: sha512-3Cwg/UuRkAv/wm6RhtPE5L7JlPB877vwSF6gfLAS68H+zhH+u5oa3AieqEd0D0/kC3W7qIhYbH419f7O9i/5nw==} + engines: {node: '>= 8.0'} + dependencies: + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru-cache: 8.0.5 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + dev: false + + /named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + dependencies: + lru-cache: 7.18.3 + dev: false + /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -8538,6 +8585,10 @@ packages: upper-case-first: 1.1.2 dev: true + /seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + dev: false + /serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} dependencies: @@ -8716,6 +8767,11 @@ packages: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} dev: true + /sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + dev: false + /stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} dev: false