WIP. A little custom music server
0
fork

Configure Feed

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

feat: update endpoints to use branded types

+77 -21
+2 -1
api-testing/get-album-by-id.bru
··· 11 11 } 12 12 13 13 params:path { 14 - id: 019a215f-3f4e-7000-b79e-b0f965f0a75c 14 + id: album_01K97906F8S08EY11STX0DY7K4 15 15 } 16 16 17 17 settings { 18 18 encodeUrl: true 19 + timeout: 0 19 20 }
+43 -15
backend/src/api.ts
··· 1 - import { Data, Effect, Layer, ManagedRuntime } from "effect"; 1 + import { Data, Effect, Layer, ManagedRuntime, Schema } from "effect"; 2 2 import { Elysia, status, StatusMap, t, type HTTPHeaders } from "elysia"; 3 3 import { DatabaseLive } from "./db"; 4 4 import { BunContext } from "@effect/platform-bun"; ··· 8 8 import { openapi } from "@elysiajs/openapi"; 9 9 import { pipe } from "effect"; 10 10 import type { ElysiaCookie } from "elysia/cookies"; 11 + import { Album, Artist, Song } from "./types"; 11 12 12 13 class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{ 13 14 message: string; ··· 65 66 ), 66 67 ); 67 68 68 - const artists = album.artists.map((a) => a.artist); 69 - const songs = album.songs.map((s) => ({ 70 - ...s, 71 - artists: s.artists.map((a) => a.artist), 72 - })); 69 + const artists = yield* Effect.all( 70 + album.artists.map(({ artist }) => Schema.decodeUnknown(Artist)(artist)), 71 + { concurrency: "unbounded" }, 72 + ); 73 73 74 - return { 75 - ...album, 76 - artists, 77 - songs, 78 - }; 74 + const songs = yield* Effect.all( 75 + album.songs.map((song) => 76 + Effect.gen(function* () { 77 + const songArtists = yield* Effect.all( 78 + song.artists.map(({ artist }) => Schema.decodeUnknown(Artist)(artist)), 79 + ); 80 + return yield* Schema.decodeUnknown(Song)({ 81 + ...song, 82 + artists: songArtists, 83 + }); 84 + }), 85 + ), 86 + ); 87 + 88 + const result = yield* Schema.decodeUnknown(Album)({ 89 + id: album.id, 90 + title: album.title, 91 + artists: artists, 92 + songs: songs, 93 + }); 94 + 95 + return result; 79 96 }); 80 97 81 98 const getAlbumList = Effect.fn("getAlbumList")(function* () { ··· 91 108 }) 92 109 .pipe(Effect.catchAll((_) => Effect.succeed([]))); 93 110 94 - return rows.map((r) => ({ 95 - ...r, 96 - artists: r.artists.map((a) => a.artist), 97 - })); 111 + const result = rows.map((row) => 112 + Effect.gen(function* () { 113 + const artists = yield* Effect.all( 114 + row.artists.map(({ artist }) => Schema.decodeUnknown(Artist)(artist)), 115 + { concurrency: "unbounded" }, 116 + ); 117 + 118 + return yield* Schema.decodeUnknown(Album)({ 119 + ...row, 120 + artists, 121 + }); 122 + }), 123 + ); 124 + 125 + return yield* Effect.all(result); 98 126 }); 99 127 100 128 const getFileById = Effect.fn("getFileById")(function* (id: string, set: ElysiaSet) {
+32 -5
backend/src/types.ts
··· 10 10 return isValid(id as string) ? true : `Extpected ArtistID to end with valid ulid but recieved (${id})`; 11 11 }), 12 12 ); 13 - const Artist = Schema.TaggedStruct("Artist", { 13 + const Artist = Schema.Struct({ 14 14 id: ArtistId, 15 15 name: Schema.NonEmptyString, 16 + }).annotations({ 17 + title: "artists", 18 + identifier: "Artist", 19 + }); 20 + 21 + const AudioFileIdSymbol = Symbol.for("AudioFileId"); 22 + const AudioFileId = Schema.NonEmptyString.pipe( 23 + Schema.brand(AudioFileIdSymbol), 24 + Schema.startsWith("file_"), 25 + Schema.filter((str) => { 26 + const [, id] = str.split("_"); 27 + return isValid(id as string) ? true : `Extpected AudioFileID to end with valid ulid but recieved (${id})`; 28 + }), 29 + ); 30 + const AudioFile = Schema.Struct({ 31 + id: AudioFileId, 32 + filePath: Schema.NonEmptyString, 33 + }).annotations({ 34 + title: "audiofiles", 35 + identifier: "AudioFile", 16 36 }); 17 37 18 38 const SongIdSymbol = Symbol.for("SongId"); ··· 24 44 return isValid(id as string) ? true : `Extpected SongID to end with valid ulid but recieved (${id})`; 25 45 }), 26 46 ); 27 - const Song = Schema.TaggedStruct("Song", { 47 + const Song = Schema.Struct({ 28 48 id: SongId, 29 49 title: Schema.NonEmptyString, 30 50 artists: Schema.Array(Artist), 51 + fileId: AudioFileId, 52 + }).annotations({ 53 + title: "songs", 54 + identifier: "Song", 31 55 }); 32 56 33 57 const AlbumIdSymbol = Symbol.for("AlbumId"); ··· 39 63 return isValid(id as string) ? true : `Extpected AlbumID to end with valid ulid but recieved (${id})`; 40 64 }), 41 65 ); 42 - const Album = Schema.TaggedStruct("Album", { 66 + const Album = Schema.Struct({ 43 67 id: AlbumId, 44 68 title: Schema.NonEmptyString, 45 69 artists: Schema.Array(Artist), 46 - songs: Schema.Array(Song), 70 + songs: Schema.optional(Schema.Array(Song)), 71 + }).annotations({ 72 + title: "albums", 73 + identifier: "Album", 47 74 }); 48 75 49 - export { Song, Album, Artist }; 76 + export { Song, Album, Artist, AudioFile };