From 45dfe1b35941c04e93149e960d35b50ea7d85bac Mon Sep 17 00:00:00 2001 From: Jack LaFond Date: Sat, 15 Jun 2024 22:51:12 -0400 Subject: [PATCH] migrate to Drizzle ORM Co-authored-by: Nebula Co-authored-by: Aarnav Tale --- bun.lockb | Bin 211373 -> 239348 bytes drizzle.config.ts | 9 + next.config.js | 2 +- package.json | 5 +- src/db/index.ts | 11 ++ src/db/migration.ts | 14 ++ src/db/schema.ts | 37 ++++ src/drizzle/0000_spooky_arachne.sql | 37 ++++ src/drizzle/meta/0000_snapshot.json | 265 ++++++++++++++++++++++++++++ src/drizzle/meta/_journal.json | 13 ++ src/pages/api/index.ts | 11 +- src/pages/api/oauth/callback.ts | 78 +++++--- src/pages/api/saves/[playerId].ts | 57 +++--- src/pages/api/saves/index.ts | 94 +++++----- tsconfig.json | 6 +- 15 files changed, 512 insertions(+), 127 deletions(-) create mode 100644 drizzle.config.ts create mode 100644 src/db/index.ts create mode 100644 src/db/migration.ts create mode 100644 src/db/schema.ts create mode 100644 src/drizzle/0000_spooky_arachne.sql create mode 100644 src/drizzle/meta/0000_snapshot.json create mode 100644 src/drizzle/meta/_journal.json diff --git a/bun.lockb b/bun.lockb index 6f4f9b530f5ff439874c173e56852539f773bdaa..b64c0e0d0b3c0be114199b7c442ce50706203d14 100755 GIT binary patch delta 57662 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 D7o912 diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..ce18d87d --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "drizzle-kit"; +export default defineConfig({ + schema: "./src/db/schema.ts", + out: "./src/drizzle", + dialect: "mysql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/next.config.js b/next.config.js index bc9a8d62..7b82d340 100644 --- a/next.config.js +++ b/next.config.js @@ -47,7 +47,7 @@ const nextConfig = { pathname: "/**", }, ], - }, + } }; module.exports = withBundleAnalyzer(nextConfig); diff --git a/package.json b/package.json index 1b1fd30b..47e4506e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "start": "next start", "lint": "next lint" }, - "packageManager": "bun@1.0.22", "dependencies": { "@heroicons/react": "^2.0.18", "@hookform/resolvers": "^3.2.0", @@ -40,13 +39,16 @@ "@types/react": "18.2.14", "@types/react-dom": "18.2.6", "autoprefixer": "10.4.14", + "axios": "^1.7.2", "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", "cmdk": "^0.2.0", "cookies-next": "^2.1.2", + "drizzle-orm": "^0.31.2", "eslint": "8.44.0", "eslint-config-next": "13.4.12", "fast-xml-parser": "^4.2.6", + "mysql2": "^3.10.1", "next": "13.4.12", "next-themes": "^0.2.1", "postcss": "8.4.24", @@ -66,6 +68,7 @@ "vaul": "^0.8.0" }, "devDependencies": { + "drizzle-kit": "^0.22.7", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.11" } diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 00000000..0eadd39f --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,11 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import { createConnection } from "mysql2"; +import * as schema from "./schema"; + +export const connection = createConnection({ + uri: process.env.DATABASE_URL!, + queueLimit: 0, + connectionLimit: 10, +}); + +export const db = drizzle(connection, { schema, mode: "default" }); diff --git a/src/db/migration.ts b/src/db/migration.ts new file mode 100644 index 00000000..33fcc477 --- /dev/null +++ b/src/db/migration.ts @@ -0,0 +1,14 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import { migrate } from "drizzle-orm/mysql2/migrator"; +import { createConnection } from "mysql2"; +import * as schema from "./schema"; + +export const connection = createConnection({ + uri: process.env.DATABASE_URL!, + queueLimit: 0, + connectionLimit: 10, +}); + +export const db = drizzle(connection, { schema, mode: "default" }); +migrate(db, { migrationsFolder: "../drizzle" }); +await connection.end(); diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 00000000..956d246c --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,37 @@ +import { index, json, mysqlTable, varchar } from "drizzle-orm/mysql-core"; + +export const users = mysqlTable('Users', { + id: varchar("id", { length: 64}).notNull().unique().primaryKey(), + discord_id: varchar("discord_id", { length: 191}).notNull().unique(), + cookie_secret: varchar("cookie_secret", { length: 191}).notNull().unique(), + discord_avatar: varchar("discord_avatar", { length: 64}).notNull(), + discord_name: varchar("discord_name", { length: 64}).notNull(), +}, (table) => { + return { + id: index("Users_id").on(table.id), + discord_id: index("Users_discord_id").on(table.discord_id), + } +}) + +export const saves = mysqlTable('Saves', { + _id: varchar("_id", { length: 64}).notNull().unique().primaryKey(), + user_id: varchar("user_id", { length: 64}).notNull().unique(), + general: json("general").notNull().default({}), + fishing: json("fishing").notNull().default({}), + cooking: json("cooking").notNull().default({}), + crafting: json("crafting").notNull().default({}), + shipping: json("shipping").notNull().default({}), + museum: json("museum").notNull().default({}), + social: json("social").notNull().default({}), + monsters: json("monsters").notNull().default({}), + walnuts: json("walnuts").notNull().default({}), + notes: json("notes").notNull().default({}), + scraps: json("scraps").notNull().default({}), + perfection: json("perfection").notNull().default({}), + powers: json("powers").notNull().default({}), + bundles: json("bundles").notNull().default([]), +}, (table) => { + return { + user_id: index("Saves_user_id").on(table.user_id), + } +}) diff --git a/src/drizzle/0000_spooky_arachne.sql b/src/drizzle/0000_spooky_arachne.sql new file mode 100644 index 00000000..0ce55102 --- /dev/null +++ b/src/drizzle/0000_spooky_arachne.sql @@ -0,0 +1,37 @@ +CREATE TABLE `Saves` ( + `_id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `general` json NOT NULL DEFAULT ('{}'), + `fishing` json NOT NULL DEFAULT ('{}'), + `cooking` json NOT NULL DEFAULT ('{}'), + `crafting` json NOT NULL DEFAULT ('{}'), + `shipping` json NOT NULL DEFAULT ('{}'), + `museum` json NOT NULL DEFAULT ('{}'), + `social` json NOT NULL DEFAULT ('{}'), + `monsters` json NOT NULL DEFAULT ('{}'), + `walnuts` json NOT NULL DEFAULT ('{}'), + `notes` json NOT NULL DEFAULT ('{}'), + `scraps` json NOT NULL DEFAULT ('{}'), + `perfection` json NOT NULL DEFAULT ('{}'), + `powers` json NOT NULL DEFAULT ('{}'), + `bundles` json NOT NULL DEFAULT ('[]'), + CONSTRAINT `Saves__id` PRIMARY KEY(`_id`), + CONSTRAINT `Saves__id_unique` UNIQUE(`_id`), + CONSTRAINT `Saves_user_id_unique` UNIQUE(`user_id`) +); +--> statement-breakpoint +CREATE TABLE `Users` ( + `id` varchar(64) NOT NULL, + `discord_id` varchar(191) NOT NULL, + `cookie_secret` varchar(191) NOT NULL, + `discord_avatar` varchar(64) NOT NULL, + `discord_name` varchar(64) NOT NULL, + CONSTRAINT `Users_id` PRIMARY KEY(`id`), + CONSTRAINT `Users_id_unique` UNIQUE(`id`), + CONSTRAINT `Users_discord_id_unique` UNIQUE(`discord_id`), + CONSTRAINT `Users_cookie_secret_unique` UNIQUE(`cookie_secret`) +); +--> statement-breakpoint +CREATE INDEX `Saves_user_id` ON `Saves` (`user_id`);--> statement-breakpoint +CREATE INDEX `Users_id` ON `Users` (`id`);--> statement-breakpoint +CREATE INDEX `Users_discord_id` ON `Users` (`discord_id`); \ No newline at end of file diff --git a/src/drizzle/meta/0000_snapshot.json b/src/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..89eb50fa --- /dev/null +++ b/src/drizzle/meta/0000_snapshot.json @@ -0,0 +1,265 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a6a83411-889a-45fd-8488-d0b207285e6b", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "Saves": { + "name": "Saves", + "columns": { + "_id": { + "name": "_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "fishing": { + "name": "fishing", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "cooking": { + "name": "cooking", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "crafting": { + "name": "crafting", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "shipping": { + "name": "shipping", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "museum": { + "name": "museum", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "social": { + "name": "social", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "monsters": { + "name": "monsters", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "walnuts": { + "name": "walnuts", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "notes": { + "name": "notes", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "scraps": { + "name": "scraps", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "perfection": { + "name": "perfection", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "powers": { + "name": "powers", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "bundles": { + "name": "bundles", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('[]')" + } + }, + "indexes": { + "Saves_user_id": { + "name": "Saves_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "Saves__id": { + "name": "Saves__id", + "columns": [ + "_id" + ] + } + }, + "uniqueConstraints": { + "Saves__id_unique": { + "name": "Saves__id_unique", + "columns": [ + "_id" + ] + }, + "Saves_user_id_unique": { + "name": "Saves_user_id_unique", + "columns": [ + "user_id" + ] + } + } + }, + "Users": { + "name": "Users", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discord_id": { + "name": "discord_id", + "type": "varchar(191)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cookie_secret": { + "name": "cookie_secret", + "type": "varchar(191)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discord_avatar": { + "name": "discord_avatar", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discord_name": { + "name": "discord_name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "Users_id": { + "name": "Users_id", + "columns": [ + "id" + ], + "isUnique": false + }, + "Users_discord_id": { + "name": "Users_discord_id", + "columns": [ + "discord_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "Users_id": { + "name": "Users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "Users_id_unique": { + "name": "Users_id_unique", + "columns": [ + "id" + ] + }, + "Users_discord_id_unique": { + "name": "Users_discord_id_unique", + "columns": [ + "discord_id" + ] + }, + "Users_cookie_secret_unique": { + "name": "Users_cookie_secret_unique", + "columns": [ + "cookie_secret" + ] + } + } + } + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/src/drizzle/meta/_journal.json b/src/drizzle/meta/_journal.json new file mode 100644 index 00000000..35a1b268 --- /dev/null +++ b/src/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1718504417580, + "tag": "0000_spooky_arachne", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index 8b0ced79..f8ed51c3 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -1,14 +1,13 @@ -import { conn, getUID } from "@/pages/api/saves"; +import * as schema from '$drizzle/schema'; +import { db } from "@/db"; +import { eq } from 'drizzle-orm'; import type { NextApiRequest, NextApiResponse } from "next"; +import { getUID } from "./saves"; async function get(req: NextApiRequest, res: NextApiResponse) { const uid = await getUID(req, res); - if (!uid) return res.status(401).end(); - - const user = (await conn.execute("SELECT * FROM Users WHERE id = ?", [uid])) - .rows?.[0]; - + const [user] = await db.select().from(schema.users).where(eq(schema.users.id, uid)).limit(1); return res.json(user); } diff --git a/src/pages/api/oauth/callback.ts b/src/pages/api/oauth/callback.ts index 239336e7..d68eccbf 100644 --- a/src/pages/api/oauth/callback.ts +++ b/src/pages/api/oauth/callback.ts @@ -1,7 +1,10 @@ +import * as schema from '$drizzle/schema'; +import { db } from '@/db'; import { getCookie, setCookie } from "cookies-next"; import crypto from "crypto"; +import { eq } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; -import { SqlUser, conn, createToken } from "../saves"; +import { createToken } from '../saves'; type Data = Record; @@ -19,7 +22,7 @@ export default async function handler( } const uid = getCookie("uid", { req }); - if (!uid) { + if (!uid || typeof uid !== "string") { res.status(400).end(); res.redirect("/"); console.log("[OAuth] No UID cookie"); @@ -77,17 +80,12 @@ export default async function handler( const discordUserData = await discordUser.json(); - let user = ( - await conn.execute("SELECT * FROM Users WHERE id = ? LIMIT 1", [uid]) - )?.rows[0] as SqlUser | undefined; + let [user] = await db.select().from(schema.users).where(eq(schema.users.id, uid)).limit(1); + let cookieSecret = user?.cookie_secret ?? crypto.randomBytes(16).toString("hex"); if (!user) { - let discordUser = ( - await conn.execute("SELECT * FROM Users WHERE discord_id = ? LIMIT 1", [ - discordUserData.id, - ]) - )?.rows[0] as SqlUser | undefined; + let [discordUser] = await db.select().from(schema.users).where(eq(schema.users.id, discordData.uid)).limit(1); if (discordUser) { user = discordUser; @@ -95,32 +93,54 @@ export default async function handler( // update discord name if it has changed if (discordUser.discord_name !== discordUserData.username) { - const r = await conn.execute( - "UPDATE Users SET discord_name = ? WHERE discord_id = ?", - [discordUserData.username, discordUserData.id], - ); + await db + .update(schema.users) + .set({ discord_name: discordUserData.username }) + .where(eq(schema.saves.user_id, discordUserData.id)); + // const r = await conn.execute( + // "UPDATE Users SET discord_name = ? WHERE discord_id = ?", + // [discordUserData.username, discordUserData.id], + // ); } // update discord avatar if the avatar hash changed if (discordUser.discord_avatar !== discordUserData.avatar) { - const r = await conn.execute( - "UPDATE Users SET discord_avatar = ? WHERE discord_id = ?", - [discordUserData.avatar, discordUserData.id], - ); + await db + .update(schema.users) + .set({ discord_avatar: discordUserData.avatar }) + .where(eq(schema.saves.user_id, discordUserData.id)); + // const r = await conn.execute( + // "UPDATE Users SET discord_avatar = ? WHERE discord_id = ?", + // [discordUserData.avatar, discordUserData.id], + // ); } } else { - await conn.execute( - "INSERT INTO Users (id, discord_id, discord_name, discord_avatar, cookie_secret) VALUES (?, ?, ?, ?, ?)", - [ - uid as string, - discordUserData.id, - discordUserData.username, - discordUserData.avatar, - cookieSecret, - ], - ); + await db.insert(schema.users).values({ + id: uid, + discord_id: discordUserData.id, + discord_name: discordUserData.username, + discord_avatar: discordUserData.avatar, + cookie_secret: cookieSecret + }).onDuplicateKeyUpdate({ + set: { + discord_id: discordUserData.id, + discord_name: discordUserData.username, + discord_avatar: discordUserData.avatar, + cookie_secret: cookieSecret + } + }); + // await conn.execute( + // "INSERT INTO Users (id, discord_id, discord_name, discord_avatar, cookie_secret) VALUES (?, ?, ?, ?, ?)", + // [ + // uid as string, + // discordUserData.id, + // discordUserData.username, + // discordUserData.avatar, + // cookieSecret, + // ], + // ); user = { - id: uid as string, + id: uid, discord_id: discordUserData.id, discord_name: discordUserData.username, discord_avatar: discordUserData.avatar, diff --git a/src/pages/api/saves/[playerId].ts b/src/pages/api/saves/[playerId].ts index 132cdaeb..c0b495a8 100644 --- a/src/pages/api/saves/[playerId].ts +++ b/src/pages/api/saves/[playerId].ts @@ -1,5 +1,7 @@ +import { db } from "$db"; +import { sql } from "drizzle-orm"; import { NextApiRequest, NextApiResponse } from "next"; -import { Player, conn, getUID } from "."; +import { Player, getUID } from "."; async function patch(req: NextApiRequest, res: NextApiResponse) { const playerId = req.query.playerId as string | undefined; @@ -10,46 +12,29 @@ async function patch(req: NextApiRequest, res: NextApiResponse) { if (!player) return res.status(400).end(); try { - await conn.execute( - ` + await db.execute( + sql` UPDATE Saves SET - general=JSON_MERGE_PATCH(general, ?), - bundles=JSON_MERGE_PATCH(bundles, ?), - fishing=JSON_MERGE_PATCH(fishing, ?), - cooking=JSON_MERGE_PATCH(cooking, ?), - crafting=JSON_MERGE_PATCH(crafting, ?), - shipping=JSON_MERGE_PATCH(shipping, ?), - museum=JSON_MERGE_PATCH(museum, ?), - social=JSON_MERGE_PATCH(social, ?), - monsters=JSON_MERGE_PATCH(monsters, ?), - walnuts=JSON_MERGE_PATCH(walnuts, ?), - notes=JSON_MERGE_PATCH(notes, ?), - scraps=JSON_MERGE_PATCH(scraps, ?), - perfection=JSON_MERGE_PATCH(perfection, ?), - powers=JSON_MERGE_PATCH(powers, ?) - WHERE _id = ? AND user_id = ? + general=JSON_MERGE_PATCH(general, ${player.general ? JSON.stringify(player.general) : "{}"}), + bundles=JSON_MERGE_PATCH(bundles, ${player.bundles ? JSON.stringify(player.bundles) : "[]"}), + fishing=JSON_MERGE_PATCH(fishing, ${player.fishing ? JSON.stringify(player.fishing) : "{}"}), + cooking=JSON_MERGE_PATCH(cooking, ${player.cooking ? JSON.stringify(player.cooking) : "{}"}), + crafting=JSON_MERGE_PATCH(crafting, ${player.crafting ? JSON.stringify(player.crafting) : "{}"}), + shipping=JSON_MERGE_PATCH(shipping, ${player.shipping ? JSON.stringify(player.shipping) : "{}"}), + museum=JSON_MERGE_PATCH(museum, ${player.museum ? JSON.stringify(player.museum) : "{}"}), + social=JSON_MERGE_PATCH(social, ${player.social ? JSON.stringify(player.social) : "{}"}), + monsters=JSON_MERGE_PATCH(monsters, ${player.monsters ? JSON.stringify(player.monsters) : "{}"}), + walnuts=JSON_MERGE_PATCH(walnuts, ${player.walnuts ? JSON.stringify(player.walnuts) : "{}"}), + notes=JSON_MERGE_PATCH(notes, ${player.notes ? JSON.stringify(player.notes) : "{}"}), + scraps=JSON_MERGE_PATCH(scraps, ${player.scraps ? JSON.stringify(player.scraps) : "{}"}), + perfection=JSON_MERGE_PATCH(perfection, ${player.perfection ? JSON.stringify(player.perfection) : "{}"}), + powers=JSON_MERGE_PATCH(powers, ${player.powers ? JSON.stringify(player.powers) : "{}"}) + WHERE _id = ${playerId} AND user_id = ${uid} `, - [ - player.general ? JSON.stringify(player.general) : "{}", - player.bundles ? JSON.stringify(player.bundles) : "[]", - player.fishing ? JSON.stringify(player.fishing) : "{}", - player.cooking ? JSON.stringify(player.cooking) : "{}", - player.crafting ? JSON.stringify(player.crafting) : "{}", - player.shipping ? JSON.stringify(player.shipping) : "{}", - player.museum ? JSON.stringify(player.museum) : "{}", - player.social ? JSON.stringify(player.social) : "{}", - player.monsters ? JSON.stringify(player.monsters) : "{}", - player.walnuts ? JSON.stringify(player.walnuts) : "{}", - player.notes ? JSON.stringify(player.notes) : "{}", - player.scraps ? JSON.stringify(player.scraps) : "{}", - player.perfection ? JSON.stringify(player.perfection) : "{}", - player.powers ? JSON.stringify(player.powers) : "{}", - playerId, - uid, - ], ); res.status(200).end(); } catch (e) { + console.log(e); res.status(500).end(); } } diff --git a/src/pages/api/saves/index.ts b/src/pages/api/saves/index.ts index 87917e6c..b8edac74 100644 --- a/src/pages/api/saves/index.ts +++ b/src/pages/api/saves/index.ts @@ -1,13 +1,10 @@ -import { Client } from "@planetscale/database"; +import { db } from '$db'; +import * as schema from '$drizzle/schema'; import { getCookie, setCookie } from "cookies-next"; import crypto from "crypto"; +import { and, eq } from "drizzle-orm"; import { NextApiRequest, NextApiResponse } from "next"; -const client = new Client({ - url: process.env.DATABASE_URL, -}); - -export const conn = client.connection(); type Data = Record; @@ -44,12 +41,14 @@ export async function getUID( // console.log("Getting UID from cookie..."); let uid = getCookie("uid", { req, res }); // console.log("UID: ", uid); - if (uid) { + if (uid && typeof uid === "string") { // console.log("Found UID..."); // uids can be anonymous, so we need to check if the user exists - const user = ( - await conn.execute("SELECT * FROM Users WHERE id = ? LIMIT 1", [uid]) - )?.rows[0] as SqlUser | undefined; + + + // yeah this is correct now but eq needs to come from drizzle-orm/mysql-core or sm no its fine its cuz cookies are weird + // one secn + const [user] = await db.select().from(schema.users).where(eq(schema.users.id, uid)).limit(1); if (user) { // user exists, so we check if the user is authenticated @@ -121,10 +120,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { // console.log("Getting..."); const uid = await getUID(req, res); // console.log("uid: ", uid); - const players = ( - await conn.execute("SELECT * FROM Saves WHERE user_id = ?", [uid]) - )?.rows as any[] | undefined; - + const players = await db.select().from(schema.saves).where(eq(schema.saves.user_id, uid)); res.json(players); } @@ -135,31 +131,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const players = JSON.parse(req.body) as Player[]; for (const player of players) { try { - const r = await conn.execute( - ` - REPLACE INTO Saves (_id, user_id, general, bundles, fishing, cooking, crafting, shipping, museum, social, monsters, walnuts, notes, scraps, perfection, powers) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - [ - player._id, - uid, - player.general ? JSON.stringify(player.general) : "{}", - player.bundles ? JSON.stringify(player.bundles) : "[]", - player.fishing ? JSON.stringify(player.fishing) : "{}", - player.cooking ? JSON.stringify(player.cooking) : "{}", - player.crafting ? JSON.stringify(player.crafting) : "{}", - player.shipping ? JSON.stringify(player.shipping) : "{}", - player.museum ? JSON.stringify(player.museum) : "{}", - player.social ? JSON.stringify(player.social) : "{}", - player.monsters ? JSON.stringify(player.monsters) : "{}", - player.walnuts ? JSON.stringify(player.walnuts) : "{}", - player.notes ? JSON.stringify(player.notes) : "{}", - player.scraps ? JSON.stringify(player.scraps) : "{}", - player.perfection ? JSON.stringify(player.perfection) : "{}", - player.powers ? JSON.stringify(player.powers) : "{}", - ], - ); - // console.log(r); + if (player._id) { + await db.insert(schema.saves).values({ + _id: player._id, + user_id: uid, + ...player + }).onDuplicateKeyUpdate({ set: player }); + } res.status(200).end(); } catch (e) { // console.log(e); @@ -174,9 +152,10 @@ async function _delete(req: NextApiRequest, res: NextApiResponse) { if (!req.body) { // delete all players - const result = await conn.execute("DELETE FROM Saves WHERE user_id = ?", [ - uid, - ]); + await db.delete(schema.saves).where(eq(schema.saves.user_id, uid)); + // const result = await conn.execute("DELETE FROM Saves WHERE user_id = ?", [ + // uid, + // ]); // console.log("[DEBUG:SAVES] DELETE | deleted all players with uid =", uid); } else { // console.log("[DEBUG:SAVES] DELETE | req.body =", req.body); @@ -185,22 +164,33 @@ async function _delete(req: NextApiRequest, res: NextApiResponse) { if (type === "player") { // delete a single player const { _id } = JSON.parse(req.body); - const result = await conn.execute( - "DELETE FROM Saves WHERE user_id = ? AND _id = ?", - [uid, _id], - ); + await db.delete(schema.saves) + .where( + and( + eq(schema.saves.user_id, uid), + eq(schema.saves._id, _id) + ) + ); + + // const result = await conn.execute( + // "DELETE FROM Saves WHERE user_id = ? AND _id = ?", + // [uid, _id], + // ); // console.log("[DEBUG:SAVES] DELETE | deleted one player with id =", _id); } else { // delete entire account // delete players - const result = await conn.execute("DELETE FROM Saves WHERE user_id = ?", [ - uid, - ]); + await db.delete(schema.saves).where(eq(schema.saves.user_id, uid)); + // const result = await conn.execute("DELETE FROM Saves WHERE user_id = ?", [ + // uid, + // ]); // delete user - const result2 = await conn.execute("DELETE FROM Users WHERE id = ?", [ - uid, - ]); + await db.delete(schema.users).where(eq(schema.users.id, uid)); + + // const result2 = await conn.execute("DELETE FROM Users WHERE id = ?", [ + // uid, + // ]); // console.log("[DEBUG:SAVES] DELETE | deleted account with uid =", uid); } } diff --git a/tsconfig.json b/tsconfig.json index 61c19abd..c6bb1c55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "esnext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -15,7 +15,9 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "$drizzle/*": ["./src/db/*"], + "$db": ["./src/db"], } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],