implement collections page

This commit is contained in:
2025-05-31 22:58:00 +01:00
parent 525d768235
commit 4bc5630922
21 changed files with 1244 additions and 9 deletions

View File

@@ -0,0 +1,59 @@
import type { User } from "@markone/core"
import type { Collection } from "@markone/core"
import { db } from "~/database.js"
function findCollections(user: User): Collection[] {
const collectionsQuery = db.query<Collection, { userId: string }>(`
SELECT id, name, description
FROM collections
WHERE user_id = $userId
`)
return collectionsQuery.all({ userId: user.id })
}
function insertCollection(collection: Collection, user: User): void {
const query = db.query(`
INSERT INTO collections (id, user_id, name, description)
VALUES ($id, $userId, $name, $description)
`)
query.run({
id: collection.id,
userId: user.id,
name: collection.name,
description: collection.description,
})
}
function updateCollection(collection: Collection, user: User): void {
db.query(`
UPDATE collections
SET name = $name, description = $description
WHERE id = $id AND user_id = $userId
`).run({
id: collection.id,
userId: user.id,
name: collection.name,
description: collection.description,
})
}
function deleteCollection(collectionId: string, user: User): void {
db.query(`
DELETE FROM collections
WHERE id = $id AND user_id = $userId
`).run({
id: collectionId,
userId: user.id,
})
}
function findCollectionById(collectionId: string, user: User): Collection | null {
return db.query<Collection, { id: string; userId: string }>(`
SELECT *
FROM collections
WHERE id = $id AND user_id = $userId
`).get({ id: collectionId, userId: user.id })
}
export { findCollections, insertCollection, updateCollection, deleteCollection, findCollectionById }

View File

@@ -0,0 +1,82 @@
import { type Collection, DEMO_USER, type User } from "@markone/core"
import { type } from "arktype"
import { ulid } from "ulid"
import { HttpError } from "~/error.ts"
import { findCollections, insertCollection, deleteCollection, updateCollection, findCollectionById } from "./collection.ts"
const AddCollectionRequestBody = type({
title: "string",
description: "string",
})
const UpdateCollectionRequestBody = type({
title: "string",
description: "string",
})
async function createCollection(request: Bun.BunRequest<"/api/collections">, user: User) {
if (user.id === DEMO_USER.id) {
return Response.json(undefined, { status: 204 })
}
const body = AddCollectionRequestBody(
await request.json().catch(() => {
throw new HttpError(400)
}),
)
if (body instanceof type.errors) {
throw new HttpError(400, "", body.summary)
}
const collection: Collection = {
id: ulid(),
name: body.title,
description: body.description,
bookmarks: [],
}
insertCollection(collection, user)
return Response.json(collection, { status: 200 })
}
async function listUserCollections(request: Bun.BunRequest<"/api/collections">, user: User) {
const collections = findCollections(user)
return Response.json(collections, { status: 200 })
}
async function deleteUserCollection(request: Bun.BunRequest<"/api/collections/:id">, user: User) {
if (user.id !== DEMO_USER.id) {
deleteCollection(request.params.id, user)
}
return Response.json(undefined, { status: 204 })
}
async function updateUserCollection(request: Bun.BunRequest<"/api/collections/:id">, user: User) {
if (user.id === DEMO_USER.id) {
return Response.json(undefined, { status: 204 })
}
const bodyJson = await request.json().catch(() => {
throw new HttpError(400)
})
const body = UpdateCollectionRequestBody(bodyJson)
if (body instanceof type.errors) {
throw new HttpError(400, "", body.summary)
}
const existingCollection = findCollectionById(request.params.id, user)
if (!existingCollection) {
throw new HttpError(404)
}
existingCollection.name = body.title
existingCollection.description = body.description
updateCollection(existingCollection, user)
return Response.json(existingCollection, { status: 200 })
}
export { createCollection, listUserCollections, deleteUserCollection, updateUserCollection }

View File

@@ -54,6 +54,19 @@ CREATE TABLE IF NOT EXISTS auth_tokens(
user_id TEXT NOT NULL,
expires_at_unix_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS collections(
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
user_id TEXT NOT NULL,
description TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS bookmark_collections(
collection_id TEXT NOT NULL,
bookmark_id TEXT NOT NULL,
PRIMARY KEY (collection_id, bookmark_id)
);
`,
]

View File

@@ -1,3 +1,4 @@
import { createCollection, deleteUserCollection, listUserCollections } from "~/collection/handlers.js"
import { authenticated, login, logout, signUp, startBackgroundAuthTokenCleanup } from "./auth/auth.ts"
import { startBackgroundSessionCleanup } from "./auth/session.ts"
import { insertDemoBookmarks } from "./bookmark/bookmark.ts"
@@ -53,6 +54,17 @@ async function main() {
GET: authenticated(listUserTags),
POST: authenticated(createUserTag),
},
"/api/collections": {
GET: authenticated(listUserCollections),
POST: authenticated(createCollection),
},
"/api/collections/:id": {
DELETE: authenticated(deleteUserCollection),
OPTIONS: preflightHandler({
allowedMethods: ["GET", "POST", "DELETE", "PATCH", "OPTIONS"],
allowedHeaders: ["Accept"],
}),
},
},
port: 8080,