feat: set up Drizzle

This commit is contained in:
2024-06-12 09:29:58 -04:00
parent 5843dfdeb2
commit 06bb8f65fb
13 changed files with 403 additions and 5 deletions

BIN
bun.lockb

Binary file not shown.

12
drizzle.config.ts Normal file
View File

@@ -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,
},
});

View File

@@ -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
);

View File

@@ -0,0 +1 @@
ALTER TABLE `watch_status` DROP COLUMN `watch_status`;

View File

@@ -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": {}
}
}

View File

@@ -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": {}
}
}

View File

@@ -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
}
]
}

View File

@@ -4,18 +4,23 @@
"description": "API for Aniplay", "description": "API for Aniplay",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"dev:cloudflare": "wrangler dev src/index.ts --port 8080", "dev:cloudflare": "TURSO_URL=http://127.0.0.1:3000 TURSO_AUTH_TOKEN=123 wrangler dev src/index.ts --port 8080",
"dev:server": "bun run --watch src/index.ts", "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", "prod:server": "bun run src/index.ts",
"deploy": "wrangler deploy --minify src/index.ts", "deploy": "wrangler deploy --minify src/index.ts",
"env:generate": "bun src/scripts/generateEnv.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": { "dependencies": {
"@consumet/extensions": "github:consumet/consumet.ts#2bcd9287dc1471ed081bc23333e7629779924e0e", "@consumet/extensions": "github:consumet/consumet.ts#2bcd9287dc1471ed081bc23333e7629779924e0e",
"@haverstack/axios-fetch-adapter": "^0.12.0", "@haverstack/axios-fetch-adapter": "^0.12.0",
"@hono/swagger-ui": "^0.2.2", "@hono/swagger-ui": "^0.2.2",
"@hono/zod-openapi": "^0.12.0", "@hono/zod-openapi": "^0.12.0",
"@libsql/client": "^0.6.2",
"drizzle-orm": "^0.31.2",
"gql.tada": "^1.7.5", "gql.tada": "^1.7.5",
"graphql-request": "^7.0.1", "graphql-request": "^7.0.1",
"hono": "^4.3.6", "hono": "^4.3.6",
@@ -26,11 +31,13 @@
"@cloudflare/workers-types": "^4.20240403.0", "@cloudflare/workers-types": "^4.20240403.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/bun": "^1.1.2", "@types/bun": "^1.1.2",
"drizzle-kit": "^0.22.6",
"msw": "^2.3.0", "msw": "^2.3.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-toml": "^2.0.1", "prettier-plugin-toml": "^2.0.1",
"ts-morph": "^22.0.0", "ts-morph": "^22.0.0",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"wrangler": "^3.47.0" "wrangler": "^3.47.0",
"zx": "^8.1.2"
} }
} }

36
src/models/db.ts Normal file
View File

@@ -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<typeof createDb>;
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);
}

30
src/models/schema.ts Normal file
View File

@@ -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];

26
src/models/token.ts Normal file
View File

@@ -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();
}

59
src/models/watchStatus.ts Normal file
View File

@@ -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),
),
);
}

View File

@@ -5,7 +5,7 @@ compatibility_date = "2023-12-01"
node_compat = true node_compat = true
[vars] [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" QSTASH_URL = "https://qstash.upstash.io/v2/publish"
ENABLE_ANIFY = false ENABLE_ANIFY = false