From 06bb8f65fb3d205976504c80f5c4cac61528b2ab Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Wed, 12 Jun 2024 09:29:58 -0400 Subject: [PATCH] feat: set up Drizzle --- bun.lockb | Bin 118537 -> 149568 bytes drizzle.config.ts | 12 +++ drizzle/0000_left_reavers.sql | 14 +++ drizzle/0001_purple_franklin_richards.sql | 1 + drizzle/meta/0000_snapshot.json | 100 ++++++++++++++++++++++ drizzle/meta/0001_snapshot.json | 93 ++++++++++++++++++++ drizzle/meta/_journal.json | 20 +++++ package.json | 15 +++- src/models/db.ts | 36 ++++++++ src/models/schema.ts | 30 +++++++ src/models/token.ts | 26 ++++++ src/models/watchStatus.ts | 59 +++++++++++++ wrangler.toml | 2 +- 13 files changed, 403 insertions(+), 5 deletions(-) create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_left_reavers.sql create mode 100644 drizzle/0001_purple_franklin_richards.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 src/models/db.ts create mode 100644 src/models/schema.ts create mode 100644 src/models/token.ts create mode 100644 src/models/watchStatus.ts diff --git a/bun.lockb b/bun.lockb index 0a448f7c4b7be2efca2a05a3409abfda4f9bc633..0e7db5d0ac21f2753902ba72c086d98c31bd9571 100755 GIT binary patch delta 38974 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 diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..3144e97 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/models/schema.ts", + out: "./drizzle", + driver: "turso", + dialect: "sqlite", + dbCredentials: { + url: process.env.TURSO_URL, + authToken: process.env.TURSO_AUTH_TOKEN, + }, +}); diff --git a/drizzle/0000_left_reavers.sql b/drizzle/0000_left_reavers.sql new file mode 100644 index 0000000..42eb358 --- /dev/null +++ b/drizzle/0000_left_reavers.sql @@ -0,0 +1,14 @@ +CREATE TABLE `token` ( + `device_id` text PRIMARY KEY NOT NULL, + `token` text NOT NULL, + `username` text, + `last_connected_at` text DEFAULT (CURRENT_TIMESTAMP) +); +--> statement-breakpoint +CREATE TABLE `watch_status` ( + `device_id` text NOT NULL, + `title_id` integer NOT NULL, + `watch_status` text NOT NULL, + PRIMARY KEY(`device_id`, `title_id`), + FOREIGN KEY (`device_id`) REFERENCES `token`(`device_id`) ON UPDATE no action ON DELETE no action +); diff --git a/drizzle/0001_purple_franklin_richards.sql b/drizzle/0001_purple_franklin_richards.sql new file mode 100644 index 0000000..d360f2d --- /dev/null +++ b/drizzle/0001_purple_franklin_richards.sql @@ -0,0 +1 @@ +ALTER TABLE `watch_status` DROP COLUMN `watch_status`; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..134fd37 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,100 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4ecf912c-2ffc-4a17-924b-8694ced4d7b6", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "token": { + "name": "token", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_connected_at": { + "name": "last_connected_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "watch_status": { + "name": "watch_status", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title_id": { + "name": "title_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "watch_status": { + "name": "watch_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "watch_status_device_id_token_device_id_fk": { + "name": "watch_status_device_id_token_device_id_fk", + "tableFrom": "watch_status", + "tableTo": "token", + "columnsFrom": ["device_id"], + "columnsTo": ["device_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "watch_status_device_id_title_id_pk": { + "columns": ["device_id", "title_id"], + "name": "watch_status_device_id_title_id_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..4460b53 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,93 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d5b8fe62-fa26-4e9b-94eb-d3d38701f620", + "prevId": "4ecf912c-2ffc-4a17-924b-8694ced4d7b6", + "tables": { + "token": { + "name": "token", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_connected_at": { + "name": "last_connected_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "watch_status": { + "name": "watch_status", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title_id": { + "name": "title_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "watch_status_device_id_token_device_id_fk": { + "name": "watch_status_device_id_token_device_id_fk", + "tableFrom": "watch_status", + "tableTo": "token", + "columnsFrom": ["device_id"], + "columnsTo": ["device_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "watch_status_device_id_title_id_pk": { + "columns": ["device_id", "title_id"], + "name": "watch_status_device_id_title_id_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..cc2ceb2 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1718106156111, + "tag": "0000_left_reavers", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1718107695989, + "tag": "0001_purple_franklin_richards", + "breakpoints": true + } + ] +} diff --git a/package.json b/package.json index bb6719e..92380ae 100644 --- a/package.json +++ b/package.json @@ -4,18 +4,23 @@ "description": "API for Aniplay", "main": "src/index.ts", "scripts": { - "dev:cloudflare": "wrangler dev src/index.ts --port 8080", - "dev:server": "bun run --watch src/index.ts", + "dev:cloudflare": "TURSO_URL=http://127.0.0.1:3000 TURSO_AUTH_TOKEN=123 wrangler dev src/index.ts --port 8080", + "dev:server": "TURSO_URL=http://127.0.0.1:3000 TURSO_AUTH_TOKEN=123 bun run --watch src/index.ts", "prod:server": "bun run src/index.ts", "deploy": "wrangler deploy --minify src/index.ts", "env:generate": "bun src/scripts/generateEnv.ts", - "env:verify": "bun src/scripts/verifyEnv.ts" + "env:verify": "bun src/scripts/verifyEnv.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "test": "bun src/testRunner.ts" }, "dependencies": { "@consumet/extensions": "github:consumet/consumet.ts#2bcd9287dc1471ed081bc23333e7629779924e0e", "@haverstack/axios-fetch-adapter": "^0.12.0", "@hono/swagger-ui": "^0.2.2", "@hono/zod-openapi": "^0.12.0", + "@libsql/client": "^0.6.2", + "drizzle-orm": "^0.31.2", "gql.tada": "^1.7.5", "graphql-request": "^7.0.1", "hono": "^4.3.6", @@ -26,11 +31,13 @@ "@cloudflare/workers-types": "^4.20240403.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/bun": "^1.1.2", + "drizzle-kit": "^0.22.6", "msw": "^2.3.0", "prettier": "^3.2.5", "prettier-plugin-toml": "^2.0.1", "ts-morph": "^22.0.0", "typescript": "^5.4.5", - "wrangler": "^3.47.0" + "wrangler": "^3.47.0", + "zx": "^8.1.2" } } diff --git a/src/models/db.ts b/src/models/db.ts new file mode 100644 index 0000000..82feac2 --- /dev/null +++ b/src/models/db.ts @@ -0,0 +1,36 @@ +import { createClient } from "@libsql/client"; +import { sql } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/libsql"; + +import type { Env } from "~/types/env"; + +import { tables } from "./schema"; + +type Db = ReturnType; +let db: Db | null = null; + +export function getDb(env: Env): Db { + if (db) { + return db; + } + + db = createDb(env); + return db; +} + +export async function resetDb() { + if (!db) return; + + for (const table of tables) { + await db.delete(table); + } +} + +function createDb(env: Env) { + const client = createClient({ + url: env.TURSO_URL, + authToken: env.TURSO_AUTH_TOKEN, + }); + + return drizzle(client); +} diff --git a/src/models/schema.ts b/src/models/schema.ts new file mode 100644 index 0000000..df8993d --- /dev/null +++ b/src/models/schema.ts @@ -0,0 +1,30 @@ +import { sql } from "drizzle-orm"; +import { + integer, + primaryKey, + sqliteTable, + text, +} from "drizzle-orm/sqlite-core"; + +export const tokenTable = sqliteTable("token", { + deviceId: text("device_id").primaryKey(), + token: text("token").notNull(), + username: text("username"), + /** Used to determine if a device hasn't been used in a while. Should start to ignore tokens where the device hasn't connected in about a month */ + lastConnectedAt: text("last_connected_at").default(sql`(CURRENT_TIMESTAMP)`), +}); + +export const watchStatusTable = sqliteTable( + "watch_status", + { + deviceId: text("device_id") + .notNull() + .references(() => tokenTable.deviceId), + titleId: integer("title_id").notNull(), + }, + (table) => ({ + pk: primaryKey({ columns: [table.deviceId, table.titleId] }), + }), +); + +export const tables = [watchStatusTable, tokenTable]; diff --git a/src/models/token.ts b/src/models/token.ts new file mode 100644 index 0000000..8ee48ad --- /dev/null +++ b/src/models/token.ts @@ -0,0 +1,26 @@ +import { eq, sql } from "drizzle-orm"; + +import type { Env } from "~/types/env"; + +import { getDb } from "./db"; +import { tokenTable } from "./schema"; + +export function saveToken( + env: Env, + deviceId: string, + token: string, + username: string | null, +) { + return getDb(env) + .insert(tokenTable) + .values({ deviceId, token, username }) + .run(); +} + +export function updateDeviceLastConnectedAt(env: Env, deviceId: string) { + return getDb(env) + .update(tokenTable) + .set({ lastConnectedAt: sql`(CURRENT_TIMESTAMP)` }) + .where(eq(tokenTable.deviceId, deviceId)) + .run(); +} diff --git a/src/models/watchStatus.ts b/src/models/watchStatus.ts new file mode 100644 index 0000000..a32ac6f --- /dev/null +++ b/src/models/watchStatus.ts @@ -0,0 +1,59 @@ +import { and, count, eq } from "drizzle-orm"; + +import type { Env } from "~/types/env"; +import { WatchStatusValues } from "~/types/title/watchStatus"; + +import { getDb } from "./db"; +import { watchStatusTable } from "./schema"; + +/** If watch status is "CURRENT", the title will be added to the watch status table. Otherwise, it will be removed. + * + * @returns an object with the following properties: + * - wasAdded: whether the title was set as watching for the first time + * - wasDeleted: whether any users are still watching the title + */ +export function setWatchStatus( + env: Env, + deviceId: string, + titleId: number, + watchStatus: (typeof WatchStatusValues)[number], +) { + let dbAction; + const isSavingTitle = watchStatus === "CURRENT"; + if (isSavingTitle) { + dbAction = saveTitle(env, deviceId, titleId); + } else { + dbAction = removeTitle(env, deviceId, titleId); + } + + return dbAction + .then(() => + getDb(env) + .select({ count: count(watchStatusTable.titleId) }) + .from(watchStatusTable) + .where(eq(watchStatusTable.titleId, titleId)) + .get(), + ) + .then((result) => ({ + wasAdded: isSavingTitle && result?.count === 1, + wasDeleted: !isSavingTitle && result?.count === 0, + })); +} + +function saveTitle(env: Env, deviceId: string, titleId: number) { + return getDb(env) + .insert(watchStatusTable) + .values({ deviceId, titleId }) + .onConflictDoNothing(); +} + +function removeTitle(env: Env, deviceId: string, titleId: number) { + return getDb(env) + .delete(watchStatusTable) + .where( + and( + eq(watchStatusTable.deviceId, deviceId), + eq(watchStatusTable.titleId, titleId), + ), + ); +} diff --git a/wrangler.toml b/wrangler.toml index 28d9499..317e491 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -5,7 +5,7 @@ compatibility_date = "2023-12-01" node_compat = true [vars] -TURSO_URL = "libsql://humble-argent-silverandroid.turso.io" +TURSO_URL = "http://127.0.0.1:8080" QSTASH_URL = "https://qstash.upstash.io/v2/publish" ENABLE_ANIFY = false