From 231ed4bde4fff2734d32c5c32cc246424c93c5e0 Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Fri, 14 Jun 2024 18:14:10 -0400 Subject: [PATCH] feat: create route to store FCM token --- bun.lockb | Bin 165421 -> 166116 bytes drizzle/0002_public_whistler.sql | 1 + drizzle/0003_puzzling_nightmare.sql | 20 +++ drizzle/0004_jittery_black_knight.sql | 20 +++ drizzle/meta/0002_snapshot.json | 99 +++++++++++ drizzle/meta/0003_snapshot.json | 99 +++++++++++ drizzle/meta/0004_snapshot.json | 101 +++++++++++ drizzle/meta/_journal.json | 21 +++ package.json | 2 + src/controllers/token/index.spec.ts | 197 +++++++++++++++++++++ src/controllers/token/index.ts | 70 ++++++++ src/controllers/watch-status/index.spec.ts | 11 +- src/index.ts | 4 + src/models/db.ts | 1 - src/models/schema.ts | 10 +- src/models/token.ts | 8 +- 16 files changed, 650 insertions(+), 14 deletions(-) create mode 100644 drizzle/0002_public_whistler.sql create mode 100644 drizzle/0003_puzzling_nightmare.sql create mode 100644 drizzle/0004_jittery_black_knight.sql create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 src/controllers/token/index.spec.ts create mode 100644 src/controllers/token/index.ts diff --git a/bun.lockb b/bun.lockb index 17366903941b313ffac1f1e154bd2b8c0c550345..2922ad38f282d3e7d868d7e0e72dc00d90b8377f 100755 GIT binary patch delta 25743 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 diff --git a/drizzle/0002_public_whistler.sql b/drizzle/0002_public_whistler.sql new file mode 100644 index 0000000..8453ba5 --- /dev/null +++ b/drizzle/0002_public_whistler.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `token_token_unique` ON `token` (`token`); \ No newline at end of file diff --git a/drizzle/0003_puzzling_nightmare.sql b/drizzle/0003_puzzling_nightmare.sql new file mode 100644 index 0000000..52ac4ad --- /dev/null +++ b/drizzle/0003_puzzling_nightmare.sql @@ -0,0 +1,20 @@ +-- Custom SQL migration file, put you code below! -- +DROP TABLE `watch_status`; +--> statement-breakpoint +DROP TABLE `token`; +--> statement-breakpoint + +CREATE TABLE `token` ( + `device_id` text NOT NULL, + `token` text NOT NULL UNIQUE ON CONFLICT FAIL, + `username` text, + `last_connected_at` text DEFAULT (CURRENT_TIMESTAMP), + PRIMARY KEY(`device_id`) ON CONFLICT REPLACE +); +--> statement-breakpoint +CREATE TABLE `watch_status` ( + `device_id` text NOT NULL, + `title_id` integer 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/0004_jittery_black_knight.sql b/drizzle/0004_jittery_black_knight.sql new file mode 100644 index 0000000..b8a9b7b --- /dev/null +++ b/drizzle/0004_jittery_black_knight.sql @@ -0,0 +1,20 @@ +-- Custom SQL migration file, put you code below! -- +DROP TABLE `watch_status`; +--> statement-breakpoint +DROP TABLE `token`; +--> statement-breakpoint + +CREATE TABLE `device_tokens` ( + `device_id` text NOT NULL, + `token` text NOT NULL UNIQUE ON CONFLICT FAIL, + `username` text, + `last_connected_at` text DEFAULT (CURRENT_TIMESTAMP), + PRIMARY KEY(`device_id`) ON CONFLICT REPLACE +); +--> statement-breakpoint +CREATE TABLE `watch_status` ( + `device_id` text NOT NULL, + `title_id` integer NOT NULL, + PRIMARY KEY(`device_id`, `title_id`), + FOREIGN KEY (`device_id`) REFERENCES `device_tokens`(`device_id`) ON UPDATE no action ON DELETE no action +); \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..46f8920 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,99 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "548c249b-15e5-4a6a-a3e4-c539d8774728", + "prevId": "d5b8fe62-fa26-4e9b-94eb-d3d38701f620", + "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": { + "token_token_unique": { + "name": "token_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "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/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..7383519 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,99 @@ +{ + "id": "3c96b576-9b17-42f7-9dd3-908a83c2a94b", + "prevId": "548c249b-15e5-4a6a-a3e4-c539d8774728", + "version": "6", + "dialect": "sqlite", + "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": { + "token_token_unique": { + "name": "token_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "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", + "columnsFrom": ["device_id"], + "tableTo": "token", + "columnsTo": ["device_id"], + "onUpdate": "no action", + "onDelete": "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": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..dab66a4 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,101 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "223cc621-0232-4499-973a-9013d134b1f9", + "prevId": "3c96b576-9b17-42f7-9dd3-908a83c2a94b", + "tables": { + "device_tokens": { + "name": "device_tokens", + "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": { + "device_tokens_token_unique": { + "name": "device_tokens_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "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_device_tokens_device_id_fk": { + "name": "watch_status_device_id_device_tokens_device_id_fk", + "tableFrom": "watch_status", + "tableTo": "device_tokens", + "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": { + "\"token\"": "\"device_tokens\"" + }, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index cc2ceb2..2960175 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,27 @@ "when": 1718107695989, "tag": "0001_purple_franklin_richards", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1718281079139, + "tag": "0002_public_whistler", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1718394970979, + "tag": "0003_puzzling_nightmare", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1718402777422, + "tag": "0004_jittery_black_knight", + "breakpoints": true } ] } diff --git a/package.json b/package.json index 6454fe6..c83bb48 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,11 @@ "@cloudflare/workers-types": "^4.20240403.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/bun": "^1.1.2", + "@types/luxon": "^3.4.2", "drizzle-kit": "^0.22.6", "husky": "^9.0.11", "lint-staged": "^15.2.7", + "luxon": "^3.4.4", "msw": "^2.3.0", "prettier": "^3.2.5", "prettier-plugin-toml": "^2.0.1", diff --git a/src/controllers/token/index.spec.ts b/src/controllers/token/index.spec.ts new file mode 100644 index 0000000..3a0994b --- /dev/null +++ b/src/controllers/token/index.spec.ts @@ -0,0 +1,197 @@ +import { eq, sql } from "drizzle-orm"; +import { DateTime } from "luxon"; + +import { beforeEach, describe, expect, it } from "bun:test"; + +import app from "~/index"; +import { server } from "~/mocks"; +import { getDb, resetDb } from "~/models/db"; +import { deviceTokensTable } from "~/models/schema"; + +server.listen(); + +describe("requests the /token route", () => { + const db = getDb({ + TURSO_URL: process.env.TURSO_URL ?? "http://127.0.0.1:3000", + TURSO_AUTH_TOKEN: process.env.TURSO_AUTH_TOKEN ?? "asd", + }); + + beforeEach(async () => { + await resetDb(); + }); + + it("should succeed", async () => { + const res = await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "123", deviceId: "123", username: "test" }), + }); + + expect(res.json()).resolves.toEqual({ success: true }); + expect(res.status).toBe(200); + }); + + it("succeeded, db should contain entry", async () => { + const minimumTimestamp = DateTime.now(); + await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "123", deviceId: "123", username: "test" }), + }); + + const row = await db + .select() + .from(deviceTokensTable) + .where(eq(deviceTokensTable.deviceId, "123")) + .get(); + + expect(row).toEqual({ + deviceId: "123", + token: "123", + username: "test", + lastConnectedAt: expect.any(String), + }); + // since SQL timestamp doesn't support milliseconds, compare to nearest second + expect( + +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( + "second", + ), + ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); + }); + + it("with username as null, should succeed", async () => { + const res = await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "123", deviceId: "123", username: null }), + }); + + expect(res.json()).resolves.toEqual({ success: true }); + expect(res.status).toBe(200); + }); + + it("with username as null, db should contain entry", async () => { + const minimumTimestamp = DateTime.now(); + await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "123", deviceId: "123", username: null }), + }); + + const row = await db + .select() + .from(deviceTokensTable) + .where(eq(deviceTokensTable.deviceId, "123")) + .get(); + + expect(row).toEqual({ + deviceId: "123", + token: "123", + username: null, + lastConnectedAt: expect.any(String), + }); + // since SQL timestamp doesn't support milliseconds, compare to nearest second + expect( + +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( + "second", + ), + ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); + }); + + it("device id already exists in db, should succeed", async () => { + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "123" }); + + const res = await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "124", deviceId: "123", username: null }), + }); + + expect(res.json()).resolves.toEqual({ success: true }); + expect(res.status).toBe(200); + }); + + it("device id already exists in db, should contain new token", async () => { + const minimumTimestamp = DateTime.now(); + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "123" }); + await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "124", deviceId: "123", username: null }), + }); + + const row = await db + .select() + .from(deviceTokensTable) + .where(eq(deviceTokensTable.deviceId, "123")) + .get(); + + expect(row).toEqual({ + deviceId: "123", + token: "124", + username: null, + lastConnectedAt: expect.any(String), + }); + // since SQL timestamp doesn't support milliseconds, compare to nearest second + expect( + +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( + "second", + ), + ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); + }); + + it("token already exists in db, should fail", async () => { + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "123" }); + + const res = await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "123", deviceId: "124", username: null }), + }); + + expect(res.json()).resolves.toEqual({ success: false }); + expect(res.status).toBe(412); + }); + + it("token already exists in db, should not insert new entry", async () => { + const minimumTimestamp = DateTime.now(); + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "123" }); + await app.request("/token", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ token: "123", deviceId: "124", username: null }), + }); + + const row = await db + .select() + .from(deviceTokensTable) + .where(eq(deviceTokensTable.deviceId, "124")) + .get(); + + expect(row).toBeUndefined(); + }); +}); diff --git a/src/controllers/token/index.ts b/src/controllers/token/index.ts new file mode 100644 index 0000000..905e07d --- /dev/null +++ b/src/controllers/token/index.ts @@ -0,0 +1,70 @@ +import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; +import { env } from "hono/adapter"; + +import { saveToken } from "~/models/token"; +import type { Env } from "~/types/env"; +import { + ErrorResponse, + SuccessResponse, + SuccessResponseSchema, +} from "~/types/schema"; + +const app = new OpenAPIHono(); + +const SaveTokenRequest = z.object({ + token: z.string(), + deviceId: z.string(), + username: z.string().nullable(), +}); + +const SaveTokenResponse = SuccessResponseSchema(); + +const route = createRoute({ + tags: ["aniplay", "notifications"], + operationId: "saveToken", + summary: "Saves FCM token", + method: "post", + path: "/", + request: { + body: { + content: { + "application/json": { + schema: SaveTokenRequest, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: SaveTokenResponse, + }, + }, + description: "Saved token successfully", + }, + }, +}); + +app.openapi(route, async (c) => { + const { token, deviceId, username } = + await c.req.json(); + + try { + await saveToken(env(c, "workerd"), deviceId, token, username); + } catch (error) { + if ( + error.code === "SQLITE_CONSTRAINT" && + error.message.includes("device_tokens.token") + ) { + return c.json(ErrorResponse, 412); + } + + console.error(new Error("Failed to save token", { cause: error })); + return c.json(ErrorResponse, 500); + } + + return c.json(SuccessResponse); +}); + +export default app; diff --git a/src/controllers/watch-status/index.spec.ts b/src/controllers/watch-status/index.spec.ts index 0907bd4..a753fbb 100644 --- a/src/controllers/watch-status/index.spec.ts +++ b/src/controllers/watch-status/index.spec.ts @@ -3,10 +3,9 @@ import { beforeEach, describe, expect, it } from "bun:test"; import app from "~/index"; import { server } from "~/mocks"; import { getDb, resetDb } from "~/models/db"; -import { tokenTable } from "~/models/schema"; +import { deviceTokensTable } from "~/models/schema"; server.listen(); -console.error = () => {}; describe("requests the /watch-status route", () => { const db = getDb({ @@ -19,7 +18,9 @@ describe("requests the /watch-status route", () => { }); it("saving title, deviceId in db, should succeed", async () => { - await db.insert(tokenTable).values({ deviceId: "123", token: "asd" }); + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "asd" }); const res = await app.request( "/watch-status", @@ -71,7 +72,9 @@ describe("requests the /watch-status route", () => { }); it("saving title, Anilist request fails, should succeed", async () => { - await db.insert(tokenTable).values({ deviceId: "123", token: "asd" }); + await db + .insert(deviceTokensTable) + .values({ deviceId: "123", token: "asd" }); const res = await app.request( "/watch-status", diff --git a/src/index.ts b/src/index.ts index fdfc163..243deaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,10 @@ app.route( (controller) => controller.default, ), ); +app.route( + "/token", + await import("~/controllers/token").then((controller) => controller.default), +); // The OpenAPI documentation will be available at /doc app.doc("/openapi.json", { diff --git a/src/models/db.ts b/src/models/db.ts index 82feac2..bd51b4e 100644 --- a/src/models/db.ts +++ b/src/models/db.ts @@ -1,5 +1,4 @@ import { createClient } from "@libsql/client"; -import { sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/libsql"; import type { Env } from "~/types/env"; diff --git a/src/models/schema.ts b/src/models/schema.ts index df8993d..21a2521 100644 --- a/src/models/schema.ts +++ b/src/models/schema.ts @@ -6,11 +6,11 @@ import { text, } from "drizzle-orm/sqlite-core"; -export const tokenTable = sqliteTable("token", { +export const deviceTokensTable = sqliteTable("device_tokens", { deviceId: text("device_id").primaryKey(), - token: text("token").notNull(), + token: text("token").notNull().unique(), 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 */ + /** 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)`), }); @@ -19,7 +19,7 @@ export const watchStatusTable = sqliteTable( { deviceId: text("device_id") .notNull() - .references(() => tokenTable.deviceId), + .references(() => deviceTokensTable.deviceId), titleId: integer("title_id").notNull(), }, (table) => ({ @@ -27,4 +27,4 @@ export const watchStatusTable = sqliteTable( }), ); -export const tables = [watchStatusTable, tokenTable]; +export const tables = [watchStatusTable, deviceTokensTable]; diff --git a/src/models/token.ts b/src/models/token.ts index 8ee48ad..703a8f0 100644 --- a/src/models/token.ts +++ b/src/models/token.ts @@ -3,7 +3,7 @@ import { eq, sql } from "drizzle-orm"; import type { Env } from "~/types/env"; import { getDb } from "./db"; -import { tokenTable } from "./schema"; +import { deviceTokensTable } from "./schema"; export function saveToken( env: Env, @@ -12,15 +12,15 @@ export function saveToken( username: string | null, ) { return getDb(env) - .insert(tokenTable) + .insert(deviceTokensTable) .values({ deviceId, token, username }) .run(); } export function updateDeviceLastConnectedAt(env: Env, deviceId: string) { return getDb(env) - .update(tokenTable) + .update(deviceTokensTable) .set({ lastConnectedAt: sql`(CURRENT_TIMESTAMP)` }) - .where(eq(tokenTable.deviceId, deviceId)) + .where(eq(deviceTokensTable.deviceId, deviceId)) .run(); }