WIP. A little custom music server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

use prefixed ulids for ids + play around with branded ids

+60 -6
+1
backend/package.json
··· 37 37 "drizzle-orm": "^0.44.7", 38 38 "effect": "^3.18.4", 39 39 "elysia": "^1.4.13", 40 + "ulid": "^3.0.1", 40 41 "zod": "^4.1.12" 41 42 } 42 43 }
+7 -6
backend/src/db/schema.ts
··· 1 1 import { relations } from "drizzle-orm"; 2 2 import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; 3 + import { ulid } from "ulid"; 3 4 4 - const id = () => 5 + const id = (prefix?: string) => 5 6 text() 6 7 .primaryKey() 7 - .$defaultFn(() => Bun.randomUUIDv7()); 8 + .$defaultFn(() => (prefix ? `${prefix}_${ulid()}` : ulid())); 8 9 9 10 export const fileTable = sqliteTable("file", { 10 - id: id(), 11 + id: id("file"), 11 12 path: text().notNull().unique(), 12 13 }); 13 14 export const songTable = sqliteTable("song", { 14 - id: id(), 15 + id: id("song"), 15 16 title: text().notNull(), 16 17 fileId: text() 17 18 .notNull() ··· 44 45 export const artistTable = sqliteTable( 45 46 "artist", 46 47 { 47 - id: id(), 48 + id: id("artist"), 48 49 name: text().notNull(), 49 50 }, 50 51 (t) => [unique().on(t.name)], ··· 53 54 export const albumTable = sqliteTable( 54 55 "album", 55 56 { 56 - id: id(), 57 + id: id("album"), 57 58 title: text().notNull(), 58 59 }, 59 60 (t) => [unique().on(t.title)],
+49
backend/src/types.ts
··· 1 + import { Schema } from "effect"; 2 + import { isValid } from "ulid"; 3 + 4 + const ArtistIdSymbol = Symbol.for("ArtistId"); 5 + const ArtistId = Schema.NonEmptyString.pipe( 6 + Schema.brand(ArtistIdSymbol), 7 + Schema.startsWith("artist_"), 8 + Schema.filter((str) => { 9 + const [, id] = str.split("_"); 10 + return isValid(id as string) ? true : `Extpected ArtistID to end with valid ulid but recieved (${id})`; 11 + }), 12 + ); 13 + const Artist = Schema.TaggedStruct("Artist", { 14 + id: ArtistId, 15 + name: Schema.NonEmptyString, 16 + }); 17 + 18 + const SongIdSymbol = Symbol.for("SongId"); 19 + const SongId = Schema.NonEmptyString.pipe( 20 + Schema.brand(SongIdSymbol), 21 + Schema.startsWith("song_"), 22 + Schema.filter((str) => { 23 + const [, id] = str.split("_"); 24 + return isValid(id as string) ? true : `Extpected SongID to end with valid ulid but recieved (${id})`; 25 + }), 26 + ); 27 + const Song = Schema.TaggedStruct("Song", { 28 + id: SongId, 29 + title: Schema.NonEmptyString, 30 + artists: Schema.Array(Artist), 31 + }); 32 + 33 + const AlbumIdSymbol = Symbol.for("AlbumId"); 34 + const AlbumId = Schema.NonEmptyString.pipe( 35 + Schema.brand(AlbumIdSymbol), 36 + Schema.startsWith("album_"), 37 + Schema.filter((str) => { 38 + const [, id] = str.split("_"); 39 + return isValid(id as string) ? true : `Extpected AlbumID to end with valid ulid but recieved (${id})`; 40 + }), 41 + ); 42 + const Album = Schema.TaggedStruct("Album", { 43 + id: AlbumId, 44 + title: Schema.NonEmptyString, 45 + artists: Schema.Array(Artist), 46 + songs: Schema.Array(Song), 47 + }); 48 + 49 + export { Song, Album, Artist };
+3
bun.lock
··· 23 23 "drizzle-orm": "^0.44.7", 24 24 "effect": "^3.18.4", 25 25 "elysia": "^1.4.13", 26 + "ulid": "^3.0.1", 26 27 "zod": "^4.1.12", 27 28 }, 28 29 "devDependencies": { ··· 1025 1026 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 1026 1027 1027 1028 "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], 1029 + 1030 + "ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="], 1028 1031 1029 1032 "undici-types": ["undici-types@7.11.0", "", {}, "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA=="], 1030 1033