WIP. A little custom music server
1
fork

Configure Feed

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

trying to use same types in both apps

+91 -20
+1 -1
backend/src/api.ts
··· 115 115 { concurrency: "unbounded" }, 116 116 ); 117 117 118 - return yield* Schema.decodeUnknown(Album)({ 118 + return yield* Schema.decodeUnknown(Schema.Struct({ ...Album.fields, songs: Schema.Undefined }))({ 119 119 ...row, 120 120 artists, 121 121 });
+1 -1
backend/src/types.ts
··· 67 67 id: AlbumId, 68 68 title: Schema.NonEmptyString, 69 69 artists: Schema.Array(Artist), 70 - songs: Schema.optional(Schema.Array(Song)), 70 + songs: Schema.Array(Song), 71 71 }).annotations({ 72 72 title: "albums", 73 73 identifier: "Album",
+6
bun.lock
··· 41 41 }, 42 42 "shared": { 43 43 "name": "@boombox/shared", 44 + "dependencies": { 45 + "effect": "^3.19.0", 46 + "ulid": "^3.0.1", 47 + }, 44 48 "devDependencies": { 45 49 "@types/bun": "latest", 46 50 }, ··· 1070 1074 "zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="], 1071 1075 1072 1076 "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 1077 + 1078 + "@boombox/shared/effect": ["effect@3.19.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-eFvvryWkbXvQ4Gak1Nadv9CW6U35+UUS/fIkF4c/Th8rs2u47g+tNkViYeVGliglNnR6Ai5Otl9tLbav3yZjXg=="], 1073 1079 1074 1080 "@boombox/web/typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 1075 1081
+3 -1
shared/index.ts
··· 1 - console.log("Hello via Bun!"); 1 + export * from "./types" 2 + 3 +
+4
shared/package.json
··· 7 7 }, 8 8 "peerDependencies": { 9 9 "typescript": "^5" 10 + }, 11 + "dependencies": { 12 + "effect": "^3.19.0", 13 + "ulid": "^3.0.1" 10 14 } 11 15 }
+76
shared/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.Struct({ 14 + id: ArtistId, 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", 36 + }); 37 + 38 + const SongIdSymbol = Symbol.for("SongId"); 39 + const SongId = Schema.NonEmptyString.pipe( 40 + Schema.brand(SongIdSymbol), 41 + Schema.startsWith("song_"), 42 + Schema.filter((str) => { 43 + const [, id] = str.split("_"); 44 + return isValid(id as string) ? true : `Extpected SongID to end with valid ulid but recieved (${id})`; 45 + }), 46 + ); 47 + const Song = Schema.Struct({ 48 + id: SongId, 49 + title: Schema.NonEmptyString, 50 + artists: Schema.Array(Artist), 51 + fileId: AudioFileId, 52 + }).annotations({ 53 + title: "songs", 54 + identifier: "Song", 55 + }); 56 + 57 + const AlbumIdSymbol = Symbol.for("AlbumId"); 58 + const AlbumId = Schema.NonEmptyString.pipe( 59 + Schema.brand(AlbumIdSymbol), 60 + Schema.startsWith("album_"), 61 + Schema.filter((str) => { 62 + const [, id] = str.split("_"); 63 + return isValid(id as string) ? true : `Extpected AlbumID to end with valid ulid but recieved (${id})`; 64 + }), 65 + ); 66 + const Album = Schema.Struct({ 67 + id: AlbumId, 68 + title: Schema.NonEmptyString, 69 + artists: Schema.Array(Artist), 70 + songs: Schema.optional(Schema.Array(Song)), 71 + }).annotations({ 72 + title: "albums", 73 + identifier: "Album", 74 + }); 75 + 76 + export { Song, Album, Artist, AudioFile };
-17
web/src/pages/album/[id].tsx
··· 65 65 }; 66 66 } 67 67 68 - const ArtistSchema = Schema.Struct({ 69 - id: Schema.NonEmptyString, 70 - name: Schema.NonEmptyString, 71 - }); 72 - const SongSchema = Schema.Struct({ 73 - id: Schema.NonEmptyString, 74 - title: Schema.NonEmptyString, 75 - fileId: Schema.NonEmptyString, 76 - artists: Schema.Array(ArtistSchema), 77 - }); 78 - 79 - const AlbumSchema = Schema.Struct({ 80 - id: Schema.NonEmptyString, 81 - title: Schema.NonEmptyString, 82 - songs: Schema.Array(SongSchema), 83 - }); 84 - 85 68 const fetchAlbum = (id: string) => 86 69 pipe( 87 70 Effect.tryPromise({