feat: create route to store FCM token
This commit is contained in:
1
drizzle/0002_public_whistler.sql
Normal file
1
drizzle/0002_public_whistler.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE UNIQUE INDEX `token_token_unique` ON `token` (`token`);
|
||||
20
drizzle/0003_puzzling_nightmare.sql
Normal file
20
drizzle/0003_puzzling_nightmare.sql
Normal file
@@ -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
|
||||
);
|
||||
20
drizzle/0004_jittery_black_knight.sql
Normal file
20
drizzle/0004_jittery_black_knight.sql
Normal file
@@ -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
|
||||
);
|
||||
99
drizzle/meta/0002_snapshot.json
Normal file
99
drizzle/meta/0002_snapshot.json
Normal file
@@ -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": {}
|
||||
}
|
||||
}
|
||||
99
drizzle/meta/0003_snapshot.json
Normal file
99
drizzle/meta/0003_snapshot.json
Normal file
@@ -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": {}
|
||||
}
|
||||
}
|
||||
101
drizzle/meta/0004_snapshot.json
Normal file
101
drizzle/meta/0004_snapshot.json
Normal file
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
197
src/controllers/token/index.spec.ts
Normal file
197
src/controllers/token/index.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
70
src/controllers/token/index.ts
Normal file
70
src/controllers/token/index.ts
Normal file
@@ -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<Env>();
|
||||
|
||||
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<typeof SaveTokenRequest._type>();
|
||||
|
||||
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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user