Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

Merge pull request #48 from teal-fm/move-to-pg

authored by mmatt.net and committed by

GitHub 5c67309f 29289649

+5032 -2844
+10 -9
apps/amethyst/app/(tabs)/(stamp)/stamp/index.tsx
··· 8 8 FlatList, 9 9 Image, 10 10 ScrollView, 11 - TextInput, 12 11 TouchableOpacity, 13 12 View, 14 13 } from "react-native"; ··· 26 25 import SheetBackdrop, { SheetHandle } from "@/components/ui/sheetBackdrop"; 27 26 import { StampContext, StampContextValue, StampStep } from "./_layout"; 28 27 import { ExternalLink } from "@/components/ExternalLink"; 28 + 29 + import { Input } from "@/components/ui/input"; 29 30 30 31 export default function StepOne() { 31 32 const router = useRouter(); ··· 87 88 }} 88 89 /> 89 90 {/* Search Form */} 90 - <View className="flex gap-4 max-w-2xl w-screen px-4"> 91 + <View className="flex gap-2 max-w-2xl w-screen px-4"> 92 + 91 93 <Text className="font-bold text-lg">Search for a track</Text> 92 - <TextInput 93 - className="p-2 border rounded-lg border-gray-300 bg-white" 94 + <Input 94 95 placeholder="Track name..." 95 96 value={searchFields.track} 96 97 onChangeText={(text) => ··· 102 103 } 103 104 }} 104 105 /> 105 - <TextInput 106 - className="p-2 border rounded-lg border-gray-300 bg-white" 106 + <Input 107 107 placeholder="Artist name..." 108 108 value={searchFields.artist} 109 109 onChangeText={(text) => ··· 115 115 } 116 116 }} 117 117 /> 118 - <TextInput 119 - className="p-2 border rounded-lg border-gray-300 bg-white" 118 + <Input 119 + 120 120 placeholder="Album name..." 121 121 value={searchFields.release} 122 122 onChangeText={(text) => ··· 128 128 } 129 129 }} 130 130 /> 131 - <View className="flex-row gap-2"> 131 + <View className="flex-row gap-2 mt-2"> 132 + 132 133 <Button 133 134 className="flex-1" 134 135 onPress={handleSearch}
-1
apps/amethyst/app/(tabs)/(stamp)/stamp/submit.tsx
··· 19 19 import { ExternalLink } from "@/components/ExternalLink"; 20 20 import { StampContext, StampContextValue, StampStep } from "./_layout"; 21 21 import { Image } from "react-native"; 22 - import PlayView from "@/components/play/playView"; 23 22 24 23 type CardyBResponse = { 25 24 error: string;
+40 -16
apps/amethyst/app/(tabs)/_layout.tsx
··· 1 - import React from "react"; 1 + 2 + import React from 'react'; 3 + 2 4 import { 3 5 FilePen, 4 6 Home, 5 7 LogOut, 8 + Search, 6 9 Settings, 7 10 type LucideIcon, 8 - } from "lucide-react-native"; 9 - import { Link, Tabs } from "expo-router"; 10 - import { Pressable } from "react-native"; 11 + } from 'lucide-react-native'; 12 + import { Link, Tabs } from 'expo-router'; 13 + import { Pressable } from 'react-native'; 11 14 12 - import Colors from "../../constants/Colors"; 13 - import { Icon, iconWithClassName } from "../../lib/icons/iconWithClassName"; 15 + import Colors from '../../constants/Colors'; 16 + import { Icon, iconWithClassName } from '../../lib/icons/iconWithClassName'; 14 17 //import useIsMobile from "@/hooks/useIsMobile"; 15 - import { useStore } from "@/stores/mainStore"; 16 - import { useColorScheme } from "nativewind"; 18 + import { useStore } from '@/stores/mainStore'; 19 + import { useColorScheme } from 'nativewind'; 20 + import AuthOptions from '../auth/options'; 21 + import useIsMobile from '@/hooks/useIsMobile'; 17 22 18 23 function TabBarIcon(props: { name: LucideIcon; color: string }) { 19 24 const Name = props.name; ··· 24 29 export default function TabLayout() { 25 30 const { colorScheme } = useColorScheme(); 26 31 const authStatus = useStore((state) => state.status); 32 + const isMobile = useIsMobile(); 27 33 // if we are on web but not native and web width is greater than 1024px 28 - const hideTabBar = authStatus !== "loggedIn"; // || useIsMobile() 34 + const hideTabBar = authStatus !== 'loggedIn'; // || useIsMobile() 35 + 36 + const j = useStore((state) => state.status); 37 + // @me 38 + const agent = useStore((state) => state.pdsAgent); 39 + const profile = useStore((state) => state.profiles[agent?.did ?? '']); 40 + 41 + if (j !== 'loggedIn') { 42 + return <AuthOptions />; 43 + } 29 44 30 45 return ( 31 46 <Tabs 32 47 screenOptions={{ 33 - title: "Home", 34 - tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint, 48 + title: 'Home', 49 + tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, 35 50 // Disable the static render of the header on web 36 51 // to prevent a hydration error in 37 52 // React Navigation v6. ··· 39 54 headerStyle: { 40 55 height: 50, 41 56 }, 42 - tabBarShowLabel: true, 57 + tabBarShowLabel: isMobile, 58 + 43 59 tabBarStyle: { 44 60 //height: 75, 45 - display: hideTabBar ? "none" : "flex", 61 + display: hideTabBar ? 'none' : 'flex', 46 62 }, 47 63 }} 48 64 > 49 65 <Tabs.Screen 50 66 name="index" 51 67 options={{ 52 - title: "Home", 68 + title: 'Home', 53 69 tabBarIcon: ({ color }) => <TabBarIcon name={Home} color={color} />, 54 70 headerRight: () => ( 55 71 <Link href="/auth/logoutModal" asChild> ··· 67 83 }} 68 84 /> 69 85 <Tabs.Screen 86 + name="search/index" 87 + options={{ 88 + title: 'Search', 89 + tabBarIcon: ({ color }) => <TabBarIcon name={Search} color={color} />, 90 + }} 91 + /> 92 + <Tabs.Screen 70 93 name="(stamp)" 71 94 options={{ 72 - title: "Stamp", 95 + title: 'Stamp', 73 96 tabBarIcon: ({ color }) => ( 74 97 <TabBarIcon name={FilePen} color={color} /> 75 98 ), ··· 78 101 <Tabs.Screen 79 102 name="settings/index" 80 103 options={{ 81 - title: "Settings", 104 + title: 'Settings', 105 + 82 106 tabBarIcon: ({ color }) => ( 83 107 <TabBarIcon name={Settings} color={color} /> 84 108 ),
+61 -70
apps/amethyst/app/(tabs)/index.tsx
··· 1 - import * as React from "react"; 2 - import { ActivityIndicator, ScrollView, View, Image } from "react-native"; 3 - import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 4 - import { CardTitle } from "../../components/ui/card"; 5 - import { Text } from "@/components/ui/text"; 6 - import { useStore } from "@/stores/mainStore"; 7 - import AuthOptions from "../auth/options"; 1 + 2 + import * as React from 'react'; 3 + import { ActivityIndicator, ScrollView, View } from 'react-native'; 8 4 9 - import { Stack } from "expo-router"; 10 - import ActorPlaysView from "@/components/play/actorPlaysView"; 11 - import { Button } from "@/components/ui/button"; 12 - import { Icon } from "@/lib/icons/iconWithClassName"; 13 - import { Plus } from "lucide-react-native"; 5 + import { useStore } from '@/stores/mainStore'; 6 + import AuthOptions from '../auth/options'; 14 7 15 - const GITHUB_AVATAR_URI = 16 - "https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg"; 8 + import { Redirect, Stack } from 'expo-router'; 9 + import ActorView from '@/components/actor/actorView'; 10 + import { useEffect, useState } from 'react'; 11 + import { useRouter } from 'expo-router'; 17 12 18 13 export default function Screen() { 14 + const router = useRouter(); 19 15 const j = useStore((state) => state.status); 20 16 // @me 21 17 const agent = useStore((state) => state.pdsAgent); 22 - const profile = useStore((state) => state.profiles[agent?.did ?? ""]); 18 + const profile = useStore((state) => state.profiles[agent?.did ?? '']); 19 + const tealDid = useStore((state) => state.tealDid); 20 + const [hasTealProfile, setHasTealProfile] = useState<boolean | null>(null); 23 21 24 - if (j !== "loggedIn") { 22 + useEffect(() => { 23 + let isMounted = true; 24 + 25 + const fetchProfile = async () => { 26 + try { 27 + if (!agent || !tealDid) return; 28 + let res = await agent.call( 29 + 'fm.teal.alpha.actor.getProfile', 30 + { actor: agent?.did }, 31 + {}, 32 + { headers: { 'atproto-proxy': tealDid + '#teal_fm_appview' } }, 33 + ); 34 + if (isMounted) { 35 + setHasTealProfile(true); 36 + } 37 + } catch (error) { 38 + setHasTealProfile(false); 39 + console.error('Error fetching profile:', error); 40 + if ( 41 + error instanceof Error && 42 + error.message.includes('could not resolve proxy did') 43 + ) { 44 + router.replace('/offline'); 45 + } 46 + } 47 + }; 48 + 49 + fetchProfile(); 50 + 51 + return () => { 52 + isMounted = false; 53 + }; 54 + }, [agent, tealDid, router]); 55 + 56 + if (j !== 'loggedIn') { 25 57 return <AuthOptions />; 26 58 } 27 59 60 + if (hasTealProfile !== null && !hasTealProfile) { 61 + return ( 62 + <View className="flex-1 justify-center items-center gap-5 p-6 bg-background"> 63 + <Redirect href="/onboarding" /> 64 + </View> 65 + ); 66 + } 67 + 28 68 // TODO: replace with skeleton 29 - if (!profile) { 69 + if (!profile || !agent) { 30 70 return ( 31 71 <View className="flex-1 justify-center items-center gap-5 p-6 bg-background"> 32 72 <ActivityIndicator size="large" /> ··· 38 78 <ScrollView className="flex-1 justify-start items-center gap-5 bg-background w-full"> 39 79 <Stack.Screen 40 80 options={{ 41 - title: "Home", 42 - headerBackButtonDisplayMode: "minimal", 81 + title: 'Home', 82 + headerBackButtonDisplayMode: 'minimal', 43 83 headerShown: false, 44 84 }} 45 85 /> 46 - {profile.bsky?.banner && ( 47 - <Image 48 - className="w-full max-w-[100vh] h-32 md:h-44 scale-[1.32] rounded-xl -mb-6" 49 - source={{ uri: profile.bsky?.banner ?? GITHUB_AVATAR_URI }} 50 - /> 51 - )} 52 - <View className="flex flex-col items-left justify-start text-left max-w-2xl w-screen gap-1 p-4 px-8"> 53 - <View className="flex flex-row justify-between items-center"> 54 - <View className="flex justify-between"> 55 - <Avatar alt="Rick Sanchez's Avatar" className="w-24 h-24"> 56 - <AvatarImage 57 - source={{ uri: profile.bsky?.avatar ?? GITHUB_AVATAR_URI }} 58 - /> 59 - <AvatarFallback> 60 - <Text>{profile.bsky?.displayName?.substring(0, 1) ?? "R"}</Text> 61 - </AvatarFallback> 62 - </Avatar> 63 - <CardTitle className="text-left flex w-full justify-between mt-2"> 64 - {profile.bsky?.displayName ?? " Richard"} 65 - </CardTitle> 66 - </View> 67 - <View className="mt-8"> 68 - <Button 69 - variant="outline" 70 - size="sm" 71 - className="text-white rounded-xl flex flex-row gap-2 justify-center items-center" 72 - > 73 - <Icon icon={Plus} size={18} /> 74 - <Text>Follow</Text> 75 - </Button> 76 - </View> 77 - </View> 78 - <View> 79 - {profile 80 - ? profile.bsky?.description?.split("\n").map((str, i) => ( 81 - <Text 82 - className="text-start self-start place-self-start" 83 - key={i} 84 - > 85 - {str} 86 - </Text> 87 - )) || "A very mysterious person" 88 - : "Loading..."} 89 - </View> 90 - </View> 91 - <View className="max-w-2xl w-full gap-4 py-4 pl-8"> 92 - <Text className="text-left text-2xl border-b border-b-muted-foreground/30 -ml-2 pl-2 mr-6"> 93 - Your Stamps 94 - </Text> 95 - <ActorPlaysView repo={agent?.did} /> 96 - </View> 86 + <ActorView actorDid={agent.did!} pdsAgent={agent} /> 87 + 97 88 </ScrollView> 98 89 ); 99 90 }
+38
apps/amethyst/app/(tabs)/profile/[handle].tsx
··· 1 + import ActorView from '@/components/actor/actorView'; 2 + import { Text } from '@/components/ui/text'; 3 + import { resolveHandle } from '@/lib/atp/pid'; 4 + import { useStore } from '@/stores/mainStore'; 5 + import { Stack, useLocalSearchParams } from 'expo-router'; 6 + import { useEffect, useState } from 'react'; 7 + import { ActivityIndicator, ScrollView, View } from 'react-native'; 8 + 9 + export default function Handle() { 10 + let { handle } = useLocalSearchParams(); 11 + 12 + let agent = useStore((state) => state.pdsAgent); 13 + 14 + // resolve handle 15 + const [did, setDid] = useState<string | null>(null); 16 + useEffect(() => { 17 + const fetchAgent = async () => { 18 + const agent = await resolveHandle(handle); 19 + setDid(agent); 20 + }; 21 + fetchAgent(); 22 + }, [handle]); 23 + 24 + if (!did) return <ActivityIndicator size="large" color="#0000ff" />; 25 + 26 + return ( 27 + <ScrollView className="flex-1 justify-start items-center gap-5 bg-background w-full"> 28 + <Stack.Screen 29 + options={{ 30 + title: 'Home', 31 + headerBackButtonDisplayMode: 'minimal', 32 + headerShown: false, 33 + }} 34 + /> 35 + <ActorView actorDid={did} pdsAgent={agent} /> 36 + </ScrollView> 37 + ); 38 + }
+109
apps/amethyst/app/(tabs)/search/index.tsx
··· 1 + import React, { useEffect, useState } from 'react'; 2 + import { ScrollView, View } from 'react-native'; 3 + import { Link, Stack } from 'expo-router'; 4 + import { Input } from '@/components/ui/input'; 5 + import { Text } from '@/components/ui/text'; 6 + import { useStore } from '@/stores/mainStore'; 7 + 8 + import { OutputSchema as SearchActorsOutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/searchActors'; 9 + import { MiniProfileView } from '@teal/lexicons/src/types/fm/teal/alpha/actor/defs'; 10 + import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 11 + import getImageCdnLink from '@/lib/atp/getImageCdnLink'; 12 + 13 + export default function Search() { 14 + const [searchQuery, setSearchQuery] = React.useState(''); 15 + const [searchResults, setSearchResults] = useState<MiniProfileView[]>([]); 16 + 17 + const tealDid = useStore((state) => state.tealDid); 18 + const agent = useStore((state) => state.pdsAgent); 19 + 20 + useEffect(() => { 21 + let isMounted = true; 22 + 23 + const fetchResults = async () => { 24 + if (!agent || !searchQuery) { 25 + // Don't fetch if searchQuery is empty 26 + setSearchResults([]); // Clear results when searchQuery is empty 27 + return; 28 + } 29 + try { 30 + let res = await agent.call( 31 + 'fm.teal.alpha.actor.searchActors', 32 + { q: searchQuery }, 33 + {}, 34 + { headers: { 'atproto-proxy': tealDid + '#teal_fm_appview' } }, 35 + ); 36 + if (isMounted) { 37 + setSearchResults( 38 + res.data['actors'] as SearchActorsOutputSchema['actors'], 39 + ); 40 + } 41 + } catch (error) { 42 + console.error('Error fetching profile:', error); 43 + } 44 + }; 45 + 46 + fetchResults(); 47 + 48 + return () => { 49 + isMounted = false; 50 + }; 51 + }, [agent, tealDid, searchQuery]); 52 + 53 + return ( 54 + <ScrollView className="flex-1 justify-start items-center gap-5 bg-background w-full"> 55 + <Stack.Screen 56 + options={{ 57 + title: 'Search', 58 + headerBackButtonDisplayMode: 'minimal', 59 + headerShown: false, 60 + }} 61 + /> 62 + <View className="max-w-2xl flex-1 w-screen flex flex-col p-4 divide-y divide-muted-foreground/50 gap-4 rounded-xl my-2 mt-5"> 63 + <Input 64 + placeholder="Search for users..." 65 + value={searchQuery} 66 + onChangeText={setSearchQuery} 67 + /> 68 + </View> 69 + <View className="my-2 mx-5"> 70 + {searchResults.map((user) => ( 71 + <Link 72 + href={`/profile/${user.handle?.replace('at://', '')}`} 73 + key={user.did} 74 + className="flex flex-row items-center gap-4 hover:bg-muted-foreground/20 p-2 rounded-xl" 75 + > 76 + <Avatar 77 + alt={`${user.displayName}'s profile`} 78 + className="w-14 h-14 border border-border" 79 + > 80 + <AvatarImage 81 + source={{ 82 + uri: 83 + user.avatar && 84 + getImageCdnLink({ 85 + did: user.did!, 86 + hash: user.avatar, 87 + }), 88 + }} 89 + /> 90 + <AvatarFallback> 91 + <Text> 92 + {user.displayName?.substring(0, 1) ?? 93 + user.handle?.substring(0, 1) ?? 94 + 'R'} 95 + </Text> 96 + </AvatarFallback> 97 + </Avatar> 98 + <View className="flex flex-col"> 99 + <Text className="font-semibold">{user.displayName}</Text> 100 + <Text className="text-muted-foreground"> 101 + {user.handle?.replace('at://', '@')} 102 + </Text> 103 + </View> 104 + </Link> 105 + ))} 106 + </View> 107 + </ScrollView> 108 + ); 109 + }
+17 -17
apps/amethyst/app/auth/signup.tsx
··· 1 - import React from "react"; 2 - import { Platform, View } from "react-native"; 3 - import { SafeAreaView } from "react-native-safe-area-context"; 4 - import { Text } from "@/components/ui/text"; 5 - import { Button } from "@/components/ui/button"; 6 - import { Icon } from "@/lib/icons/iconWithClassName"; 7 - import { ArrowRight, AtSignIcon } from "lucide-react-native"; 1 + import React from 'react'; 2 + import { Platform, View } from 'react-native'; 3 + import { SafeAreaView } from 'react-native-safe-area-context'; 4 + import { Text } from '@/components/ui/text'; 5 + import { Button } from '@/components/ui/button'; 6 + import { Icon } from '@/lib/icons/iconWithClassName'; 7 + import { ArrowRight, AtSignIcon } from 'lucide-react-native'; 8 8 9 - import { Stack, router } from "expo-router"; 9 + import { Stack, router } from 'expo-router'; 10 10 11 11 const LoginScreen = () => { 12 12 return ( 13 13 <SafeAreaView className="flex-1 flex justify-center items-center"> 14 14 <Stack.Screen 15 15 options={{ 16 - title: "Sign in", 17 - headerBackButtonDisplayMode: "minimal", 16 + title: 'Sign in', 17 + headerBackButtonDisplayMode: 'minimal', 18 18 headerShown: false, 19 19 }} 20 20 /> 21 21 <View className="flex-1 justify-center p-8 gap-4 pb-32 w-screen max-w-md"> 22 22 <Text className="text-3xl text-center text-foreground -mb-2"> 23 - Sign up via <br /> the{" "} 23 + Sign up via <br /> the{' '} 24 24 <Icon 25 25 icon={AtSignIcon} 26 - className="color-bsky inline mb-2" 26 + className="color-bsky inline mb-2 mr-1.5" 27 27 size={32} 28 - />{" "} 28 + /> 29 29 Atmosphere 30 30 </Text> 31 31 <Text className="text-foreground text-xl text-center"> ··· 43 43 <Button 44 44 onPress={() => { 45 45 // on web, open new tab 46 - if (Platform.OS === "web") { 47 - window.open("https://bsky.app/signup", "_blank"); 46 + if (Platform.OS === 'web') { 47 + window.open('https://bsky.app/signup', '_blank'); 48 48 } else { 49 - router.navigate("https://bsky.app"); 49 + router.navigate('https://bsky.app'); 50 50 } 51 51 setTimeout(() => { 52 - router.replace("/auth/login"); 52 + router.replace('/auth/login'); 53 53 }, 1000); 54 54 }} 55 55 className="flex flex-row justify-center items-center gap-2 bg-bsky"
+13
apps/amethyst/app/offline.tsx
··· 1 + import { View } from 'react-native'; 2 + import { Text } from '@/components/ui/text'; 3 + import { Icon } from '@/lib/icons/iconWithClassName'; 4 + import { CloudOff } from 'lucide-react-native'; 5 + 6 + export default function Offline() { 7 + return ( 8 + <View className="flex-1 justify-center items-center gap-2"> 9 + <Icon icon={CloudOff} size={64} /> 10 + <Text className="text-center">Oops! Can’t connect to teal.fm.</Text> 11 + </View> 12 + ); 13 + }
+66
apps/amethyst/app/onboarding/descriptionPage.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { View } from 'react-native'; 3 + import { Text } from '@/components/ui/text'; 4 + import { Button } from '@/components/ui/button'; 5 + import { Textarea } from '@/components/ui/textarea'; 6 + import { Icon } from '@/lib/icons/iconWithClassName'; 7 + import { CheckCircle } from 'lucide-react-native'; 8 + 9 + interface DescriptionPageProps { 10 + onComplete: (description: string) => void; 11 + initialDescription?: string; 12 + onBack?: () => void; 13 + } 14 + 15 + const DescriptionPage: React.FC<DescriptionPageProps> = ({ 16 + onComplete, 17 + initialDescription, 18 + onBack, 19 + }) => { 20 + const [description, setDescription] = useState(initialDescription || ''); 21 + 22 + const handleComplete = () => { 23 + if (description) { 24 + onComplete(description); 25 + } 26 + }; 27 + 28 + return ( 29 + <View className="flex-1 justify-between items-center px-5"> 30 + <View /> 31 + <View className="gap-4 max-w-lg"> 32 + <Text className="text-2xl font-semibold text-center"> 33 + Tell us about yourself! 34 + </Text> 35 + <Text className="text-sm text-center text-muted-foreground -mt-2"> 36 + Your bio is your chance to shine. Let your creativity flow and tell 37 + the world who you are. You can always edit it later. 38 + </Text> 39 + <Textarea 40 + className="border border-gray-300 rounded px-3 py-2 mb-5 min-h-[150px]" 41 + placeholder="A short bio or description" 42 + multiline 43 + value={description} 44 + onChangeText={setDescription} 45 + /> 46 + </View> 47 + <View className="flex-row justify-between w-full"> 48 + {onBack && ( 49 + <Button variant="outline" onPress={onBack} className="flex-1 mr-2"> 50 + <Text>Back</Text> 51 + </Button> 52 + )} 53 + <Button 54 + onPress={handleComplete} 55 + disabled={!description} 56 + className="flex-1 ml-2" 57 + > 58 + <Text>Next</Text>{' '} 59 + <Icon icon={CheckCircle} size={18} className="ml-2" /> 60 + </Button> 61 + </View> 62 + </View> 63 + ); 64 + }; 65 + 66 + export default DescriptionPage;
+62
apps/amethyst/app/onboarding/displayNamePage.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { View } from 'react-native'; 3 + import { Text } from '@/components/ui/text'; 4 + import { Button } from '@/components/ui/button'; 5 + import { Input } from '@/components/ui/input'; 6 + 7 + interface DisplayNamePageProps { 8 + onComplete: (displayName: string) => void; 9 + initialDisplayName?: string; 10 + onBack?: () => void; 11 + } 12 + 13 + const DisplayNamePage: React.FC<DisplayNamePageProps> = ({ 14 + onComplete, 15 + initialDisplayName, 16 + onBack, 17 + }) => { 18 + const [displayName, setDisplayName] = useState(initialDisplayName || ''); 19 + 20 + const handleNext = () => { 21 + if (displayName) { 22 + onComplete(displayName); 23 + } 24 + }; 25 + 26 + return ( 27 + <View className="flex-1 justify-between items-center px-5"> 28 + <View /> 29 + <View className="gap-4 max-w-lg"> 30 + <Text className="text-2xl font-semibold text-center"> 31 + Welcome! What should we call you? 32 + </Text> 33 + <Text className="text-sm text-center text-muted-foreground -mt-2"> 34 + Choose something unique, memorable, and something others will easily 35 + recognise. It can be your real name or a nickname you like. 36 + </Text> 37 + <Input 38 + className="border border-gray-300 rounded px-3 py-2 mb-5" 39 + placeholder="Your Display Name" 40 + value={displayName} 41 + onChangeText={setDisplayName} 42 + /> 43 + </View> 44 + <View className="flex-row justify-between w-full"> 45 + {onBack && ( 46 + <Button variant="outline" onPress={onBack} className="flex-1 mr-2"> 47 + <Text>Back</Text> 48 + </Button> 49 + )} 50 + <Button 51 + onPress={handleNext} 52 + disabled={!displayName} 53 + className="flex-1 ml-2" 54 + > 55 + <Text>Next</Text> 56 + </Button> 57 + </View> 58 + </View> 59 + ); 60 + }; 61 + 62 + export default DisplayNamePage;
+104
apps/amethyst/app/onboarding/imageSelectionPage.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { View, Pressable, Image, ActivityIndicator } from 'react-native'; 3 + import { Text } from '@/components/ui/text'; 4 + import { Button } from '@/components/ui/button'; 5 + import * as ImagePicker from 'expo-image-picker'; 6 + import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 7 + import { Icon } from '@/lib/icons/iconWithClassName'; 8 + import { Pen } from 'lucide-react-native'; 9 + 10 + interface ImageSelectionPageProps { 11 + onComplete: (avatarUri: string, bannerUri: string) => void; 12 + initialAvatar?: string; 13 + initialBanner?: string; 14 + } 15 + 16 + const ImageSelectionPage: React.FC<ImageSelectionPageProps> = ({ 17 + onComplete, 18 + initialAvatar, 19 + initialBanner, 20 + }) => { 21 + const [avatarUri, setAvatarUri] = useState(initialAvatar || ''); 22 + const [bannerUri, setBannerUri] = useState(initialBanner || ''); 23 + const [loading, setLoading] = useState(false); 24 + 25 + const pickImage = async ( 26 + setType: typeof setAvatarUri | typeof setBannerUri, 27 + ) => { 28 + setLoading(true); 29 + let result = await ImagePicker.launchImageLibraryAsync({ 30 + mediaTypes: ['images'], 31 + allowsEditing: true, 32 + aspect: setType === setAvatarUri ? [1, 1] : [3, 1], 33 + quality: 1, 34 + }); 35 + 36 + if (!result.canceled) { 37 + setType(result.assets[0].uri); 38 + } 39 + setLoading(false); 40 + }; 41 + const handleNext = () => { 42 + if (avatarUri && bannerUri) { 43 + onComplete(avatarUri, bannerUri); 44 + } 45 + }; 46 + 47 + return ( 48 + <View className="flex-1 justify-center items-center h-screen min-h-full px-5"> 49 + <Text className="text-2xl font-bold mb-5 text-center"> 50 + What do you look like? 51 + </Text> 52 + <Pressable 53 + onPress={() => pickImage(setBannerUri)} 54 + className="w-full aspect-[3/1] mb-5" 55 + > 56 + <View className="flex-1 bg-gray-200 rounded-lg justify-center items-center relative"> 57 + {loading && !bannerUri && <ActivityIndicator />} 58 + {bannerUri ? ( 59 + <> 60 + <Image 61 + source={{ uri: bannerUri }} 62 + className="w-full h-full rounded-lg object-cover" 63 + /> 64 + <View className="absolute -bottom-2 -right-2 bg-gray-500/50 rounded-full p-1"> 65 + <Icon icon={Pen} size={18} className="fill-white" /> 66 + </View> 67 + </> 68 + ) : ( 69 + <Text className="text-gray-500">Add Banner Image</Text> 70 + )} 71 + </View> 72 + </Pressable> 73 + <Pressable onPress={() => pickImage(setAvatarUri)} className="mb-10"> 74 + <View className="relative"> 75 + {loading && !avatarUri && <ActivityIndicator />} 76 + <Avatar className="w-24 h-24" alt="User Avatar"> 77 + {avatarUri ? ( 78 + <> 79 + <AvatarImage source={{ uri: avatarUri }} /> 80 + <View className="absolute bottom-0 right-0 bg-gray-500/50 rounded-full p-1"> 81 + <Icon icon={Pen} size={18} className="fill-white" /> 82 + </View> 83 + </> 84 + ) : ( 85 + <AvatarFallback> 86 + <Text>?</Text> 87 + </AvatarFallback> 88 + )} 89 + </Avatar> 90 + </View> 91 + </Pressable> 92 + 93 + <Button 94 + onPress={handleNext} 95 + disabled={!avatarUri || !bannerUri} 96 + className="w-full" 97 + > 98 + <Text>Next</Text> 99 + </Button> 100 + </View> 101 + ); 102 + }; 103 + 104 + export default ImageSelectionPage;
+217
apps/amethyst/app/onboarding/index.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { ActivityIndicator, View } from 'react-native'; 3 + import { Text } from '@/components/ui/text'; // Your UI components 4 + import ImageSelectionPage from './imageSelectionPage'; // Separate page components 5 + import DisplayNamePage from './displayNamePage'; 6 + import DescriptionPage from './descriptionPage'; 7 + import { SafeAreaView } from 'react-native-safe-area-context'; 8 + import ProgressDots from '@/components/onboarding/progressDots'; 9 + 10 + import { Record as ProfileRecord } from '@teal/lexicons/src/types/fm/teal/alpha/actor/profile'; 11 + import { useStore } from '@/stores/mainStore'; 12 + import { useRouter } from 'expo-router'; 13 + 14 + const OnboardingSubmissionSteps: string[] = [ 15 + '', 16 + 'Double checking everything', 17 + 'Submitting Profile Picture', 18 + 'Submitting Header Image', 19 + 'Submitting Profile', 20 + 'Done!', 21 + ]; 22 + 23 + export default function OnboardingPage() { 24 + const [step, setStep] = useState(1); 25 + const [displayName, setDisplayName] = useState(''); 26 + const [description, setDescription] = useState(''); 27 + const [avatarUri, setAvatarUri] = useState(''); 28 + const [bannerUri, setBannerUri] = useState(''); 29 + 30 + const [submissionStep, setSubmissionStep] = useState(0); 31 + const [submissionError, setSubmissionError] = useState(''); 32 + 33 + const router = useRouter(); 34 + 35 + const agent = useStore((store) => store.pdsAgent); 36 + const profile = useStore((store) => store.profiles); 37 + 38 + const handleImageSelectionComplete = (avatar: string, banner: string) => { 39 + setAvatarUri(avatar); 40 + setBannerUri(banner); 41 + onComplete({ displayName, description }, avatar, banner); 42 + }; 43 + 44 + const handleDisplayNameComplete = (name: string) => { 45 + setDisplayName(name); 46 + setStep(2); 47 + }; 48 + 49 + const handleDescriptionComplete = (desc: string) => { 50 + setDescription(desc); 51 + setStep(3); 52 + }; 53 + 54 + const onComplete = async ( 55 + updatedProfile: { displayName: any; description: any }, 56 + newAvatarUri: string, 57 + newBannerUri: string, 58 + ) => { 59 + if (!agent) return; 60 + // Implement your save logic here (e.g., update your database or state) 61 + console.log('Saving profile:', updatedProfile, newAvatarUri, newBannerUri); 62 + 63 + setSubmissionStep(1); 64 + 65 + // get the current user's profile (getRecord) 66 + let currentUser: ProfileRecord | undefined; 67 + let cid: string | undefined; 68 + try { 69 + const res = await agent.call('com.atproto.repo.getRecord', { 70 + repo: agent.did, 71 + collection: 'fm.teal.alpha.actor.profile', 72 + rkey: 'self', 73 + }); 74 + currentUser = res.data.value; 75 + cid = res.data.cid; 76 + } catch (error) { 77 + console.error('Error fetching user profile:', error); 78 + } 79 + 80 + // upload blobs if necessary 81 + let newAvatarBlob = currentUser?.avatar ?? undefined; 82 + let newBannerBlob = currentUser?.banner ?? undefined; 83 + if (newAvatarUri) { 84 + console.log(newAvatarUri); 85 + // if it is http/s url then do nothing 86 + if (!newAvatarUri.startsWith('http')) { 87 + setSubmissionStep(2); 88 + console.log('Uploading avatar'); 89 + // its a b64 encoded data uri, decode it and get a blob 90 + const data = await fetch(newAvatarUri).then((r) => r.blob()); 91 + const fileType = newAvatarUri.split(';')[0].split(':')[1]; 92 + console.log(fileType); 93 + const blob = new Blob([data], { type: fileType }); 94 + newAvatarBlob = (await agent.uploadBlob(blob)).data.blob; 95 + } 96 + } 97 + if (newBannerUri) { 98 + if (!newBannerUri.startsWith('http')) { 99 + setSubmissionStep(3); 100 + console.log('Uploading banner'); 101 + const data = await fetch(newBannerUri).then((r) => r.blob()); 102 + const fileType = newBannerUri.split(';')[0].split(':')[1]; 103 + console.log(fileType); 104 + const blob = new Blob([data], { type: fileType }); 105 + newBannerBlob = (await agent.uploadBlob(blob)).data.blob; 106 + } 107 + } 108 + 109 + console.log('done uploading'); 110 + 111 + setSubmissionStep(4); 112 + 113 + let record: ProfileRecord = { 114 + displayName: updatedProfile.displayName, 115 + description: updatedProfile.description, 116 + avatar: newAvatarBlob, 117 + banner: newBannerBlob, 118 + }; 119 + 120 + let post; 121 + 122 + if (cid) { 123 + post = await agent.call( 124 + 'com.atproto.repo.putRecord', 125 + {}, 126 + { 127 + repo: agent.did, 128 + collection: 'fm.teal.alpha.actor.profile', 129 + rkey: 'self', 130 + record, 131 + swapRecord: cid, 132 + }, 133 + ); 134 + } else { 135 + post = await agent.call( 136 + 'com.atproto.repo.createRecord', 137 + {}, 138 + { 139 + repo: agent.did, 140 + collection: 'fm.teal.alpha.actor.profile', 141 + rkey: 'self', 142 + record, 143 + }, 144 + ); 145 + } 146 + 147 + console.log(post); 148 + setSubmissionStep(5); 149 + //redirect to / after 2 seconds 150 + setTimeout(() => { 151 + router.replace('/'); 152 + }, 2000); 153 + }; 154 + 155 + if (!agent || !profile[agent?.did!]) { 156 + return <div>Loading...</div>; 157 + } 158 + 159 + // if we already have stuff then go back 160 + // 161 + 162 + console.log(profile); 163 + if (profile[agent?.did!].teal !== null) { 164 + return ( 165 + <Text> 166 + Profile already exists: {JSON.stringify(profile[agent?.did!].teal)} 167 + </Text> 168 + ); 169 + } 170 + 171 + if (submissionStep) { 172 + return ( 173 + <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> 174 + <ActivityIndicator size="large" color="#0000ff" /> 175 + <Text>{OnboardingSubmissionSteps[submissionStep]}</Text> 176 + </View> 177 + ); 178 + } 179 + 180 + const renderPage = () => { 181 + switch (step) { 182 + case 1: 183 + return ( 184 + <DisplayNamePage 185 + onComplete={handleDisplayNameComplete} 186 + initialDisplayName={displayName} 187 + onBack={() => setStep(1)} 188 + /> 189 + ); 190 + case 2: 191 + return ( 192 + <DescriptionPage 193 + onComplete={handleDescriptionComplete} 194 + initialDescription={description} 195 + onBack={() => setStep(2)} 196 + /> 197 + ); 198 + case 3: 199 + return ( 200 + <ImageSelectionPage 201 + onComplete={handleImageSelectionComplete} 202 + initialAvatar={avatarUri} 203 + initialBanner={bannerUri} 204 + /> 205 + ); 206 + default: 207 + return null; 208 + } 209 + }; 210 + 211 + return ( 212 + <SafeAreaView className="flex-1 p-5 pt-5"> 213 + <View className="flex-1 flex min-h-max h-full">{renderPage()}</View> 214 + <ProgressDots totalSteps={3} currentStep={step} /> 215 + </SafeAreaView> 216 + ); 217 + }
+266
apps/amethyst/components/actor/actorView.tsx
··· 1 + import { View, Image } from 'react-native'; 2 + import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 3 + import { CardTitle } from '../../components/ui/card'; 4 + import { Text } from '@/components/ui/text'; 5 + import { useStore } from '@/stores/mainStore'; 6 + 7 + import ActorPlaysView from '@/components/play/actorPlaysView'; 8 + import { Button } from '@/components/ui/button'; 9 + import { Icon } from '@/lib/icons/iconWithClassName'; 10 + import { MoreHorizontal, Pen, Plus } from 'lucide-react-native'; 11 + import { Agent } from '@atproto/api'; 12 + import { useState, useEffect } from 'react'; 13 + import EditProfileModal from './editProfileView'; 14 + 15 + import { Record as ProfileRecord } from '@teal/lexicons/src/types/fm/teal/alpha/actor/profile'; 16 + import { OutputSchema as GetProfileOutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile'; 17 + import getImageCdnLink from '@/lib/atp/getImageCdnLink'; 18 + 19 + const GITHUB_AVATAR_URI = 20 + 'https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg'; 21 + 22 + export interface ActorViewProps { 23 + actorDid: string; 24 + pdsAgent: Agent | null; 25 + } 26 + 27 + export default function ActorView({ actorDid, pdsAgent }: ActorViewProps) { 28 + const [isEditing, setIsEditing] = useState(false); 29 + const [profile, setProfile] = useState< 30 + GetProfileOutputSchema['actor'] | null 31 + >(null); 32 + 33 + const tealDid = useStore((state) => state.tealDid); 34 + 35 + useEffect(() => { 36 + let isMounted = true; 37 + 38 + const fetchProfile = async () => { 39 + if (!pdsAgent) { 40 + return; 41 + } 42 + try { 43 + let res = await pdsAgent.call( 44 + 'fm.teal.alpha.actor.getProfile', 45 + { actor: actorDid }, 46 + {}, 47 + { headers: { 'atproto-proxy': tealDid + '#teal_fm_appview' } }, 48 + ); 49 + if (isMounted) { 50 + setProfile(res.data['actor'] as GetProfileOutputSchema['actor']); 51 + } 52 + } catch (error) { 53 + console.error('Error fetching profile:', error); 54 + } 55 + }; 56 + 57 + fetchProfile(); 58 + 59 + return () => { 60 + isMounted = false; 61 + }; 62 + }, [pdsAgent, actorDid, tealDid]); 63 + 64 + const isSelf = actorDid === (pdsAgent?.did || ''); 65 + 66 + const handleSave = async ( 67 + updatedProfile: { displayName: any; description: any }, 68 + newAvatarUri: string, 69 + newBannerUri: string, 70 + ) => { 71 + if (!pdsAgent) { 72 + return; 73 + } 74 + // Implement your save logic here (e.g., update your database or state) 75 + console.log('Saving profile:', updatedProfile, newAvatarUri, newBannerUri); 76 + 77 + // Update the local profile data 78 + setProfile((prevProfile) => ({ 79 + ...prevProfile, 80 + displayName: updatedProfile.displayName, 81 + description: updatedProfile.description, 82 + avatar: newAvatarUri, 83 + banner: newBannerUri, 84 + })); 85 + 86 + // get the current user's profile (getRecord) 87 + let currentUser: ProfileRecord | undefined; 88 + let cid: string | undefined; 89 + try { 90 + const res = await pdsAgent.call('com.atproto.repo.getRecord', { 91 + repo: pdsAgent.did, 92 + collection: 'fm.teal.alpha.actor.profile', 93 + rkey: 'self', 94 + }); 95 + currentUser = res.data.value; 96 + cid = res.data.cid; 97 + } catch (error) { 98 + console.error('Error fetching user profile:', error); 99 + } 100 + 101 + // upload blobs if necessary 102 + let newAvatarBlob = currentUser?.avatar ?? undefined; 103 + let newBannerBlob = currentUser?.banner ?? undefined; 104 + if (newAvatarUri) { 105 + // if it is http/s url then do nothing 106 + if (!newAvatarUri.startsWith('http')) { 107 + console.log('Uploading avatar'); 108 + // its a b64 encoded data uri, decode it and get a blob 109 + const data = await fetch(newAvatarUri).then((r) => r.blob()); 110 + const fileType = newAvatarUri.split(';')[0].split(':')[1]; 111 + console.log(fileType); 112 + const blob = new Blob([data], { type: fileType }); 113 + newAvatarBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 114 + } 115 + } 116 + if (newBannerUri) { 117 + if (!newBannerUri.startsWith('http')) { 118 + console.log('Uploading banner'); 119 + const data = await fetch(newBannerUri).then((r) => r.blob()); 120 + const fileType = newBannerUri.split(';')[0].split(':')[1]; 121 + console.log(fileType); 122 + const blob = new Blob([data], { type: fileType }); 123 + newBannerBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 124 + } 125 + } 126 + 127 + console.log('done uploading'); 128 + 129 + let record: ProfileRecord = { 130 + displayName: updatedProfile.displayName, 131 + description: updatedProfile.description, 132 + avatar: newAvatarBlob, 133 + banner: newBannerBlob, 134 + }; 135 + 136 + let post; 137 + 138 + if (cid) { 139 + post = await pdsAgent.call( 140 + 'com.atproto.repo.putRecord', 141 + {}, 142 + { 143 + repo: pdsAgent.did, 144 + collection: 'fm.teal.alpha.actor.profile', 145 + rkey: 'self', 146 + record, 147 + swapRecord: cid, 148 + }, 149 + ); 150 + } else { 151 + post = await pdsAgent.call( 152 + 'com.atproto.repo.createRecord', 153 + {}, 154 + { 155 + repo: pdsAgent.did, 156 + collection: 'fm.teal.alpha.actor.profile', 157 + rkey: 'self', 158 + record, 159 + }, 160 + ); 161 + } 162 + 163 + setIsEditing(false); // Close the modal after saving 164 + }; 165 + 166 + if (!profile) { 167 + return null; 168 + } 169 + 170 + return ( 171 + <> 172 + {profile.banner ? ( 173 + <Image 174 + className="w-full max-w-[100vh] h-32 md:h-44 scale-[1.32] rounded-xl -mb-6" 175 + source={{ 176 + uri: 177 + getImageCdnLink({ did: profile.did!, hash: profile.banner }) ?? 178 + GITHUB_AVATAR_URI, 179 + }} 180 + /> 181 + ) : ( 182 + <View className="w-full max-w-[100vh] h-32 md:h-44 scale-[1.32] rounded-xl -mb-6 bg-background" /> 183 + )} 184 + <View className="flex flex-col items-left justify-start text-left max-w-2xl w-screen gap-1 p-4 px-8"> 185 + <View className="flex flex-row justify-between items-center"> 186 + <View className="flex justify-between"> 187 + <Avatar alt="Rick Sanchez's Avatar" className="w-24 h-24"> 188 + <AvatarImage 189 + source={{ 190 + uri: 191 + (profile.avatar && 192 + getImageCdnLink({ 193 + did: profile.did!, 194 + hash: profile.avatar, 195 + })) || 196 + GITHUB_AVATAR_URI, 197 + }} 198 + /> 199 + <AvatarFallback> 200 + <Text>{profile.displayName?.substring(0, 1) ?? 'R'}</Text> 201 + </AvatarFallback> 202 + </Avatar> 203 + <CardTitle className="text-left flex w-full justify-between mt-2"> 204 + {profile.displayName ?? ' Richard'} 205 + </CardTitle> 206 + </View> 207 + <View className="mt-2 flex-row gap-2"> 208 + {isSelf ? ( 209 + <Button 210 + variant="outline" 211 + size="sm" 212 + className="rounded-xl flex-row gap-2 justify-center items-center" 213 + onPress={() => setIsEditing(true)} 214 + > 215 + <Icon icon={Pen} size={18} /> 216 + <Text>Edit</Text> 217 + </Button> 218 + ) : ( 219 + <Button 220 + variant="outline" 221 + size="sm" 222 + className="rounded-xl flex-row gap-2 justify-center items-center" 223 + > 224 + <Icon icon={Plus} size={18} /> 225 + <Text>Follow</Text> 226 + </Button> 227 + )} 228 + <Button 229 + variant="outline" 230 + size="sm" 231 + className="text-white aspect-square p-0 rounded-full flex flex-row gap-2 justify-center items-center" 232 + > 233 + <Icon icon={MoreHorizontal} size={18} /> 234 + </Button> 235 + </View> 236 + </View> 237 + <View> 238 + {profile 239 + ? profile.description?.split('\n').map((str, i) => ( 240 + <Text 241 + className="text-start self-start place-self-start" 242 + key={i} 243 + > 244 + {str} 245 + </Text> 246 + )) || <Text>'A very mysterious person'</Text> 247 + : 'Loading...'} 248 + </View> 249 + </View> 250 + <View className="max-w-2xl w-full gap-4 py-4 pl-8"> 251 + <Text className="text-left text-2xl border-b border-b-muted-foreground/30 -ml-2 pl-2 mr-6"> 252 + Stamps 253 + </Text> 254 + <ActorPlaysView repo={actorDid} pdsAgent={pdsAgent} /> 255 + </View> 256 + {isSelf && ( 257 + <EditProfileModal 258 + isVisible={isEditing} 259 + onClose={() => setIsEditing(false)} 260 + profile={profile} // Pass the profile data 261 + onSave={handleSave} // Pass the save handler 262 + /> 263 + )} 264 + </> 265 + ); 266 + }
+180
apps/amethyst/components/actor/editProfileView.tsx
··· 1 + import * as React from 'react'; 2 + import { useState } from 'react'; 3 + import { 4 + Modal, 5 + Pressable, 6 + View, 7 + Image, 8 + ActivityIndicator, 9 + Touchable, 10 + TouchableWithoutFeedback, 11 + } from 'react-native'; 12 + import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 13 + import { Text } from '@/components/ui/text'; 14 + import { Button } from '@/components/ui/button'; 15 + import * as ImagePicker from 'expo-image-picker'; 16 + import { Card } from '../ui/card'; 17 + import { Input } from '../ui/input'; 18 + import { Textarea } from '../ui/textarea'; 19 + import { cn } from '@/lib/utils'; 20 + import { useOnEscape } from '@/hooks/useOnEscape'; 21 + import { Icon } from '@/lib/icons/iconWithClassName'; 22 + import { Pen } from 'lucide-react-native'; 23 + import getImageCdnLink from '@/lib/atp/getImageCdnLink'; 24 + 25 + const GITHUB_AVATAR_URI = 26 + 'https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg'; 27 + 28 + export interface EditProfileModalProps { 29 + isVisible: boolean; 30 + onClose: () => void; 31 + profile: any; // Pass the profile data as a prop 32 + onSave: (profile: any, avatarUri: string, bannerUri: string) => void; // Pass the onSave callback function 33 + } 34 + 35 + export default function EditProfileModal({ 36 + isVisible, 37 + onClose, 38 + profile, // Pass the profile data as a prop 39 + onSave, // Pass the onSave callback function 40 + }: EditProfileModalProps) { 41 + const [editedProfile, setEditedProfile] = useState({ ...profile }); 42 + const [avatarUri, setAvatarUri] = useState(profile?.avatar); 43 + const [bannerUri, setBannerUri] = useState(profile?.banner); 44 + const [loading, setLoading] = useState(false); 45 + 46 + const pickImage = async ( 47 + setType: typeof setAvatarUri | typeof setBannerUri, 48 + ) => { 49 + setLoading(true); // Start loading 50 + 51 + let result = await ImagePicker.launchImageLibraryAsync({ 52 + mediaTypes: ['images'], 53 + allowsEditing: true, 54 + aspect: setType === setAvatarUri ? [1, 1] : [3, 1], 55 + quality: 1, 56 + }); 57 + 58 + if (!result.canceled) { 59 + setType(result.assets[0].uri); 60 + } 61 + 62 + setLoading(false); // Stop loading 63 + }; 64 + 65 + const handleSave = () => { 66 + onSave(editedProfile, avatarUri, bannerUri); // Call the onSave callback with updated data 67 + //onClose(); 68 + }; 69 + 70 + useOnEscape(onClose); 71 + 72 + if (!profile) { 73 + return ( 74 + <View className="flex-1 justify-center items-center gap-5 p-6 bg-background"> 75 + <ActivityIndicator size="large" /> 76 + </View> 77 + ); 78 + } 79 + 80 + return ( 81 + <Modal animationType="fade" transparent={true} visible={isVisible}> 82 + <TouchableWithoutFeedback onPress={() => onClose()}> 83 + <View className="flex-1 justify-center items-center bg-black/50 backdrop-blur"> 84 + <TouchableWithoutFeedback> 85 + <Card className="bg-card rounded-lg p-4 w-11/12 max-w-md"> 86 + <Text className="text-xl font-bold mb-4">Edit Profile</Text> 87 + <Pressable onPress={() => pickImage(setBannerUri)}> 88 + {loading && !bannerUri && <ActivityIndicator />} 89 + {bannerUri ? ( 90 + <View className="relative group"> 91 + <Image 92 + source={{ 93 + uri: bannerUri?.includes(';') 94 + ? bannerUri 95 + : getImageCdnLink({ 96 + did: editedProfile.did, 97 + hash: bannerUri, 98 + }), 99 + }} 100 + className="w-full h-24 rounded-lg" 101 + /> 102 + <View className="absolute -right-2 -bottom-2 rounded-full bg-muted/70 group-hover:bg-muted/90 text-foreground transition-colors duration-300 border border-border p-1"> 103 + <Icon icon={Pen} size={18} className="fill-muted" /> 104 + </View> 105 + </View> 106 + ) : ( 107 + <View className="w-full max-w-[100vh] h-32 md:h-44 rounded-xl -mb-6 bg-muted" /> 108 + )} 109 + </Pressable> 110 + 111 + <Pressable 112 + onPress={() => pickImage(setAvatarUri)} 113 + className={cn('mb-4 relative group', bannerUri && 'pl-4 -mt-8')} 114 + > 115 + {loading && !avatarUri && <ActivityIndicator />} 116 + <View className="relative group w-min"> 117 + <Avatar 118 + className="w-20 h-20" 119 + alt={`Avatar for ${editedProfile?.displayName ?? 'User'}`} 120 + > 121 + <AvatarImage 122 + source={{ 123 + uri: avatarUri?.includes(';') 124 + ? avatarUri 125 + : getImageCdnLink({ 126 + did: editedProfile.did, 127 + hash: avatarUri, 128 + }), 129 + }} 130 + /> 131 + <AvatarFallback> 132 + <Text> 133 + {editedProfile?.displayName?.substring(0, 1) ?? 'R'} 134 + </Text> 135 + </AvatarFallback> 136 + </Avatar> 137 + <View className="absolute right-0 bottom-0 rounded-full bg-muted/70 group-hover:bg-muted/90 text-foreground transition-colors duration-300 border border-border p-1"> 138 + <Icon icon={Pen} size={18} className="fill-muted" /> 139 + </View> 140 + </View> 141 + </Pressable> 142 + 143 + <Text className="text-sm font-semibold text-muted-foreground pl-1"> 144 + Display Name 145 + </Text> 146 + <Input 147 + className="border border-gray-300 rounded px-3 py-2 mb-4" 148 + placeholder="Display Name" 149 + value={editedProfile.displayName} 150 + onChangeText={(text) => 151 + setEditedProfile({ ...editedProfile, displayName: text }) 152 + } 153 + /> 154 + <Text className="text-sm font-semibold text-muted-foreground pl-1"> 155 + Description 156 + </Text> 157 + <Textarea 158 + className="border border-gray-300 rounded px-3 py-2 mb-4" 159 + placeholder="Description" 160 + multiline 161 + value={editedProfile.description} 162 + onChangeText={(text) => 163 + setEditedProfile({ ...editedProfile, description: text }) 164 + } 165 + /> 166 + <View className="flex-row justify-between"> 167 + <Button variant="outline" onPress={onClose}> 168 + <Text>Cancel</Text> 169 + </Button> 170 + <Button onPress={handleSave}> 171 + <Text>Save</Text> 172 + </Button> 173 + </View> 174 + </Card> 175 + </TouchableWithoutFeedback> 176 + </View> 177 + </TouchableWithoutFeedback> 178 + </Modal> 179 + ); 180 + }
+34
apps/amethyst/components/onboarding/progressDots.tsx
··· 1 + import React from 'react'; 2 + import { View } from 'react-native'; 3 + 4 + interface ProgressDotsProps { 5 + totalSteps: number; 6 + currentStep: number; 7 + } 8 + 9 + const ProgressDots: React.FC<ProgressDotsProps> = ({ 10 + totalSteps, 11 + currentStep, 12 + }) => { 13 + const dots = []; 14 + 15 + for (let i = 1; i <= totalSteps; i++) { 16 + const isActive = i <= currentStep; 17 + dots.push( 18 + <View 19 + key={i} 20 + className={` 21 + w-4 22 + h-4 23 + rounded-full 24 + m-2 25 + ${isActive ? `bg-accent` : `bg-muted`} 26 + `} 27 + />, 28 + ); 29 + } 30 + 31 + return <View className="flex-row justify-center items-center">{dots}</View>; 32 + }; 33 + 34 + export default ProgressDots;
+32 -30
apps/amethyst/components/play/actorPlaysView.tsx
··· 1 - import { useStore } from "@/stores/mainStore"; 2 - import { Record as Play } from "@teal/lexicons/src/types/fm/teal/alpha/feed/play"; 3 - import { useEffect, useState } from "react"; 4 - import { ScrollView } from "react-native"; 5 - import { Text } from "@/components/ui/text"; 6 - import PlayView from "./playView"; 1 + 2 + import { useStore } from '@/stores/mainStore'; 3 + import { OutputSchema as ActorFeedResponse } from '@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed'; 4 + import { useEffect, useState } from 'react'; 5 + import { ScrollView } from 'react-native'; 6 + import { Text } from '@/components/ui/text'; 7 + import PlayView from './playView'; 8 + import { Agent } from '@atproto/api'; 9 + 7 10 interface ActorPlaysViewProps { 8 11 repo: string | undefined; 12 + pdsAgent: Agent | null; 9 13 } 10 - interface PlayWrapper { 11 - cid: string; 12 - uri: string; 13 - value: Play; 14 - } 15 - const ActorPlaysView = ({ repo }: ActorPlaysViewProps) => { 16 - const [play, setPlay] = useState<PlayWrapper[] | null>(null); 17 - const agent = useStore((state) => state.pdsAgent); 14 + const ActorPlaysView = ({ repo, pdsAgent }: ActorPlaysViewProps) => { 15 + const [play, setPlay] = useState<ActorFeedResponse['plays'] | null>(null); 18 16 const isReady = useStore((state) => state.isAgentReady); 17 + const tealDid = useStore((state) => state.tealDid); 19 18 useEffect(() => { 20 - if (agent) { 21 - agent 22 - .call("com.atproto.repo.listRecords", { 23 - repo, 24 - collection: "fm.teal.alpha.feed.play", 25 - }) 26 - .then((profile) => { 27 - profile.data.records as PlayWrapper[]; 28 - return setPlay(profile.data.records); 19 + if (pdsAgent) { 20 + pdsAgent 21 + .call( 22 + 'fm.teal.alpha.feed.getActorFeed', 23 + { authorDID: repo }, 24 + {}, 25 + { headers: { 'atproto-proxy': tealDid + '#teal_fm_appview' } }, 26 + ) 27 + .then((res) => { 28 + res.data.plays as ActorFeedResponse; 29 + return setPlay(res.data.plays); 29 30 }) 30 31 .catch((e) => { 31 32 console.log(e); 32 33 }); 33 34 } else { 34 - console.log("No agent"); 35 + console.log('No agent'); 35 36 } 36 - }, [isReady, agent, repo]); 37 + }, [isReady, pdsAgent, repo, tealDid]); 37 38 if (!play) { 38 39 return <Text>Loading...</Text>; 39 40 } ··· 41 42 <ScrollView className="w-full *:gap-4"> 42 43 {play.map((p) => ( 43 44 <PlayView 44 - key={p.uri} 45 - releaseTitle={p.value.releaseName} 46 - trackTitle={p.value.trackName} 47 - artistName={p.value.artistNames.join(", ")} 48 - releaseMbid={p.value.releaseMbId} 45 + key={p.playedTime + p.trackName} 46 + releaseTitle={p.releaseName} 47 + trackTitle={p.trackName} 48 + artistName={p.artistNames.join(', ')} 49 + releaseMbid={p.releaseMbId} 50 + 49 51 /> 50 52 ))} 51 53 </ScrollView>
+58
apps/amethyst/components/ui/ago.tsx
··· 1 + import { Text } from './text'; 2 + 3 + const Ago = ({ time }: { time: Date }) => { 4 + return ( 5 + <Text className="text-gray-500 text-sm">{timeAgoSinceDate(time)}</Text> 6 + ); 7 + }; 8 + 9 + /** 10 + * Calculates a human-readable string representing how long ago a date occurred relative to now. 11 + * Mimics the behavior of the provided Dart function. 12 + * 13 + * @param createdDate The date to compare against the current time. 14 + * @param numericDates If true, uses numeric representations like "1 minute ago", otherwise uses text like "A minute ago". Defaults to true. 15 + * @returns A string describing the time elapsed since the createdDate. 16 + */ 17 + function timeAgoSinceDate( 18 + createdDate: Date, 19 + numericDates: boolean = true, 20 + ): string { 21 + const now = new Date(); 22 + const differenceInMs = now.getTime() - createdDate.getTime(); 23 + 24 + const seconds = Math.floor(differenceInMs / 1000); 25 + const minutes = Math.floor(seconds / 60); 26 + const hours = Math.floor(minutes / 60); 27 + const days = Math.floor(hours / 24); 28 + 29 + if (seconds < 5) { 30 + return 'Just now'; 31 + } else if (seconds <= 60) { 32 + return `${seconds} seconds ago`; 33 + } else if (minutes <= 1) { 34 + return numericDates ? '1 minute ago' : 'A minute ago'; 35 + } else if (minutes <= 60) { 36 + return `${minutes} minutes ago`; 37 + } else if (hours <= 1) { 38 + return numericDates ? '1 hour ago' : 'An hour ago'; 39 + } else if (hours <= 60) { 40 + return `${hours} hours ago`; 41 + } else if (days <= 1) { 42 + return numericDates ? '1 day ago' : 'Yesterday'; 43 + } else if (days <= 6) { 44 + return `${days} days ago`; 45 + } else if (Math.ceil(days / 7) <= 1) { 46 + return numericDates ? '1 week ago' : 'Last week'; 47 + } else if (Math.ceil(days / 7) <= 4) { 48 + return `${Math.ceil(days / 7)} weeks ago`; 49 + } else if (Math.ceil(days / 30) <= 1) { 50 + return numericDates ? '1 month ago' : 'Last month'; 51 + } else if (Math.ceil(days / 30) <= 30) { 52 + return `${Math.ceil(days / 30)} months ago`; 53 + } else if (Math.ceil(days / 365) <= 1) { 54 + return numericDates ? '1 year ago' : 'Last year'; 55 + } else { 56 + return `${Math.floor(days / 365)} years ago`; 57 + } 58 + }
+27
apps/amethyst/components/ui/textarea.tsx
··· 1 + import * as React from 'react'; 2 + import { TextInput, type TextInputProps } from 'react-native'; 3 + import { cn } from '~/lib/utils'; 4 + 5 + const Textarea = React.forwardRef<React.ElementRef<typeof TextInput>, TextInputProps>( 6 + ({ className, multiline = true, numberOfLines = 4, placeholderClassName, ...props }, ref) => { 7 + return ( 8 + <TextInput 9 + ref={ref} 10 + className={cn( 11 + 'web:flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base lg:text-sm native:text-lg native:leading-[1.25] text-foreground web:ring-offset-background placeholder:text-muted-foreground web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2', 12 + props.editable === false && 'opacity-50 web:cursor-not-allowed', 13 + className 14 + )} 15 + placeholderClassName={cn('text-muted-foreground', placeholderClassName)} 16 + multiline={multiline} 17 + numberOfLines={numberOfLines} 18 + textAlignVertical='top' 19 + {...props} 20 + /> 21 + ); 22 + } 23 + ); 24 + 25 + Textarea.displayName = 'Textarea'; 26 + 27 + export { Textarea };
+17
apps/amethyst/hooks/useOnEscape.tsx
··· 1 + import { useEffect } from "react"; 2 + 3 + export const useOnEscape = (callback: () => void) => { 4 + useEffect(() => { 5 + const handleKeyDown = (event: KeyboardEvent) => { 6 + if (event.key === "Escape") { 7 + callback(); 8 + } 9 + }; 10 + 11 + document.addEventListener("keydown", handleKeyDown); 12 + 13 + return () => { 14 + document.removeEventListener("keydown", handleKeyDown); 15 + }; 16 + }, [callback]); 17 + };
+14
apps/amethyst/lib/atp/getImageCdnLink.tsx
··· 1 + const DEFAULT_IMAGE_TEMPLATE = 'https://at.uwu.wang/{did}/{hash}'; 2 + 3 + export default function getImageCdnLink({ 4 + did, 5 + hash, 6 + }: { 7 + did: string; 8 + hash: string; 9 + }): string | undefined { 10 + if (!did || !hash) return undefined; 11 + // if hash is actually a data url return it 12 + if (hash.startsWith('data:')) return hash; 13 + return DEFAULT_IMAGE_TEMPLATE.replace('{did}', did).replace('{hash}', hash); 14 + }
+1
apps/amethyst/package.json
··· 40 40 "expo": "~52.0.27", 41 41 "expo-constants": "^17.0.3", 42 42 "expo-font": "~13.0.1", 43 + "expo-image-picker": "^16.0.6", 43 44 "expo-linking": "~7.0.3", 44 45 "expo-router": "~4.0.1", 45 46 "expo-splash-screen": "~0.29.21",
+66 -40
apps/amethyst/stores/authenticationSlice.tsx
··· 1 - import { StateCreator } from "./mainStore"; 2 - import createOAuthClient, { AquareumOAuthClient } from "../lib/atp/oauth"; 3 - import { OAuthSession } from "@atproto/oauth-client"; 4 - import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 5 - import { Agent } from "@atproto/api"; 6 - import * as Lexicons from "@teal/lexicons/src/lexicons"; 7 - import { resolveFromIdentity } from "@/lib/atp/pid"; 1 + import { StateCreator } from './mainStore'; 2 + import createOAuthClient, { AquareumOAuthClient } from '../lib/atp/oauth'; 3 + import { OAuthSession } from '@atproto/oauth-client'; 4 + import { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 5 + import { OutputSchema as GetProfileOutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile'; 6 + import { Agent } from '@atproto/api'; 7 + import * as Lexicons from '@teal/lexicons/src/lexicons'; 8 + import { resolveFromIdentity } from '@/lib/atp/pid'; 8 9 9 10 export interface AllProfileViews { 10 11 bsky: null | ProfileViewDetailed; 12 + teal: null | GetProfileOutputSchema['actor']; 11 13 // todo: teal profile view 12 14 } 13 15 14 16 export interface AuthenticationSlice { 15 17 auth: AquareumOAuthClient; 16 - status: "start" | "loggedIn" | "loggedOut"; 18 + status: 'start' | 'loggedIn' | 'loggedOut'; 17 19 oauthState: null | string; 18 20 oauthSession: null | OAuthSession; 19 21 pdsAgent: null | Agent; ··· 41 43 get, 42 44 ) => { 43 45 // check if we have CF_PAGES_URL set. if not, use localhost 44 - const baseUrl = process.env.EXPO_PUBLIC_BASE_URL || "http://localhost:8081"; 45 - console.log("Using base URL:", baseUrl); 46 - const initialAuth = createOAuthClient(baseUrl, "bsky.social"); 46 + const baseUrl = process.env.EXPO_PUBLIC_BASE_URL || 'http://localhost:8081'; 47 + console.log('Using base URL:', baseUrl); 48 + const initialAuth = createOAuthClient(baseUrl, 'bsky.social'); 47 49 48 - console.log("Auth client created!"); 50 + console.log('Auth client created!'); 49 51 50 52 return { 51 53 auth: initialAuth, 52 - status: "start", 54 + status: 'start', 53 55 oauthState: null, 54 56 oauthSession: null, 55 57 pdsAgent: null, ··· 78 80 }); 79 81 return url; 80 82 } catch (error) { 81 - console.error("Failed to get login URL:", error); 83 + console.error('Failed to get login URL:', error); 82 84 return null; 83 85 } 84 86 }, 85 87 86 88 oauthCallback: async (state: URLSearchParams) => { 87 89 try { 88 - if (!(state.has("code") && state.has("state") && state.has("iss"))) { 89 - throw new Error("Missing params, got: " + state); 90 + if (!(state.has('code') && state.has('state') && state.has('iss'))) { 91 + throw new Error('Missing params, got: ' + state); 90 92 } 91 93 // are we already logged in? 92 - if (get().status === "loggedIn") { 94 + if (get().status === 'loggedIn') { 93 95 return; 94 96 } 95 97 const { session, state: oauthState } = 96 98 await initialAuth.callback(state); 97 99 const agent = new Agent(session); 98 100 set({ 99 - oauthSession: session, 101 + // TODO: fork or update auth lib 102 + oauthSession: session as any, 100 103 oauthState, 101 - status: "loggedIn", 104 + status: 'loggedIn', 102 105 pdsAgent: addDocs(agent), 103 106 isAgentReady: true, 104 107 }); 105 108 get().populateLoggedInProfile(); 106 109 } catch (error: any) { 107 - console.error("OAuth callback failed:", error); 110 + console.error('OAuth callback failed:', error); 108 111 set({ 109 - status: "loggedOut", 112 + status: 'loggedOut', 110 113 login: { 111 114 loading: false, 112 115 error: 113 116 (error?.message as string) || 114 - "Unknown error during OAuth callback", 117 + 'Unknown error during OAuth callback', 115 118 }, 116 119 }); 117 120 } ··· 128 131 let sess = await initialAuth.restore(did); 129 132 130 133 if (!sess) { 131 - throw new Error("Failed to restore session"); 134 + throw new Error('Failed to restore session'); 132 135 } 133 136 134 137 const agent = new Agent(sess); ··· 136 139 set({ 137 140 pdsAgent: addDocs(agent), 138 141 isAgentReady: true, 139 - status: "loggedIn", 142 + status: 'loggedIn', 140 143 }); 141 144 get().populateLoggedInProfile(); 142 - console.log("Restored agent"); 145 + console.log('Restored agent'); 143 146 } catch (error) { 144 - console.error("Failed to restore agent:", error); 147 + console.error('Failed to restore agent:', error); 145 148 get().logOut(); 146 149 } 147 150 }, 148 151 logOut: () => { 149 - console.log("Logging out"); 152 + console.log('Logging out'); 150 153 let profiles = { ...get().profiles }; 151 154 // TODO: something better than 'delete' 152 - delete profiles[get().pdsAgent?.did ?? ""]; 155 + delete profiles[get().pdsAgent?.did ?? '']; 153 156 set({ 154 - status: "loggedOut", 157 + status: 'loggedOut', 155 158 oauthSession: null, 156 159 oauthState: null, 157 160 profiles, ··· 161 164 }); 162 165 }, 163 166 populateLoggedInProfile: async () => { 164 - console.log("Populating logged in profile"); 167 + console.log('Populating logged in profile'); 165 168 const agent = get().pdsAgent; 166 169 if (!agent) { 167 - throw new Error("No agent"); 170 + throw new Error('No agent'); 168 171 } 169 172 if (!agent.did) { 170 - throw new Error("No agent did! This is bad!"); 173 + throw new Error('No agent did! This is bad!'); 171 174 } 172 175 try { 173 176 let bskyProfile = await agent ··· 176 179 console.log(profile); 177 180 return profile.data || null; 178 181 }); 182 + // get teal did 183 + try { 184 + let tealDid = get().tealDid; 185 + let tealProfile = await agent 186 + .call( 187 + 'fm.teal.alpha.actor.getProfile', 188 + { actor: agent?.did }, 189 + {}, 190 + { headers: { 'atproto-proxy': tealDid + '#teal_fm_appview' } }, 191 + ) 192 + .then((profile) => { 193 + console.log(profile); 194 + return profile.data.agent || null; 195 + }); 179 196 180 - set({ 181 - profiles: { 182 - [agent.did]: { bsky: bskyProfile }, 183 - }, 184 - }); 197 + set({ 198 + profiles: { 199 + [agent.did]: { bsky: bskyProfile, teal: tealProfile }, 200 + }, 201 + }); 202 + } catch (error) { 203 + console.error('Failed to get teal profile:', error); 204 + // insert bsky profile 205 + set({ 206 + profiles: { 207 + [agent.did]: { bsky: bskyProfile, teal: null }, 208 + }, 209 + }); 210 + } 185 211 } catch (error) { 186 - console.error("Failed to get profile:", error); 212 + console.error('Failed to get profile:', error); 187 213 } 188 214 }, 189 215 }; ··· 191 217 192 218 function addDocs(agent: Agent) { 193 219 Lexicons.schemas 194 - .filter((schema) => !schema.id.startsWith("app.bsky.")) 220 + .filter((schema) => !schema.id.startsWith('app.bsky.')) 195 221 .map((schema) => { 196 222 try { 197 223 agent.lex.add(schema); 198 224 } catch (e) { 199 - console.error("Failed to add schema:", e); 225 + console.error('Failed to add schema:', e); 200 226 } 201 227 }); 202 228 return agent;
+11 -10
apps/amethyst/stores/mainStore.tsx
··· 1 - import { create, StateCreator as ZustandStateCreator } from "zustand"; 2 - import { persist, createJSONStorage } from "zustand/middleware"; 1 + import { create, StateCreator as ZustandStateCreator } from 'zustand'; 2 + import { persist, createJSONStorage } from 'zustand/middleware'; 3 3 import { 4 4 AuthenticationSlice, 5 5 createAuthenticationSlice, 6 - } from "./authenticationSlice"; 7 - import AsyncStorage from "@react-native-async-storage/async-storage"; 8 - import { createTempSlice, TempSlice } from "./tempSlice"; 9 - 6 + } from './authenticationSlice'; 7 + import AsyncStorage from '@react-native-async-storage/async-storage'; 8 + import { createTempSlice, TempSlice } from './tempSlice'; 9 + import { createPreferenceSlice, PreferenceSlice } from './preferenceSlice'; 10 10 11 11 /// Put all your non-shared slices here 12 - export type Slices = AuthenticationSlice & TempSlice; 12 + export type Slices = AuthenticationSlice & TempSlice & PreferenceSlice; 13 13 /// Put all your shared slices here 14 14 export type PlusSharedSlices = Slices; 15 15 /// Convenience type for creating a store. Uses the slices type defined above. 16 - /// Type parameter T is the type of the state object. 16 + /// Type parameter T is the type of the state object. 17 17 export type StateCreator<T> = ZustandStateCreator<Slices, [], [], T>; 18 18 19 19 export const useStore = create<PlusSharedSlices>()( ··· 21 21 (...a) => ({ 22 22 ...createAuthenticationSlice(...a), 23 23 ...createTempSlice(...a), 24 + ...createPreferenceSlice(...a), 24 25 }), 25 26 { 26 27 partialize: ({ pdsAgent, isAgentReady, ...state }) => state, 27 28 28 29 onRehydrateStorage: () => (state) => { 29 - state?.restorePdsAgent() 30 + state?.restorePdsAgent(); 30 31 }, 31 - name: "mainStore", 32 + name: 'mainStore', 32 33 storage: createJSONStorage(() => AsyncStorage), 33 34 }, 34 35 ),
+17
apps/amethyst/stores/preferenceSlice.tsx
··· 1 + import { StateCreator } from './mainStore'; 2 + 3 + export interface PreferenceSlice { 4 + colorTheme: 'dark' | 'light' | 'system'; 5 + setColorTheme: (theme: 'dark' | 'light' | 'system') => void; 6 + tealDid: string; 7 + setTealDid: (url: string) => void; 8 + } 9 + 10 + export const createPreferenceSlice: StateCreator<PreferenceSlice> = (set) => { 11 + return { 12 + colorTheme: 'system', 13 + setColorTheme: (theme) => set({ colorTheme: theme }), 14 + tealDid: 'did:web:rina.z.teal.fm', 15 + setTealDid: (url) => set({ tealDid: url }), 16 + }; 17 + };
+3 -11
apps/amethyst/tsconfig.json
··· 4 4 "strict": true, 5 5 "baseUrl": ".", 6 6 "paths": { 7 - "@/*": [ 8 - "*" 9 - ], 10 - "~/*": [ 11 - "*" 12 - ] 7 + "@/*": ["*"], 8 + "~/*": ["*"] 13 9 } 14 10 }, 15 - "include": [ 16 - "**/*.ts", 17 - "**/*.tsx", 18 - "nativewind-env.d.ts" 19 - ] 11 + "include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"] 20 12 }
+1 -1
apps/aqua/.env.example apps/aqua/src/.env.example
··· 2 2 PORT=3000 3 3 HOST=localhost 4 4 PUBLIC_URL= 5 - DB_PATH=:memory 5 + DATABASE_URL=
apps/aqua/bun.lockb

This is a binary file and will not be displayed.

-28
apps/aqua/src/auth/client.ts
··· 1 - import { NodeOAuthClient } from "@atproto/oauth-client-node"; 2 - import type { Database } from "@teal/db/connect"; 3 - import { env } from "@/lib/env"; 4 - import { SessionStore, StateStore } from "./storage"; 5 - 6 - import { db } from "@teal/db/connect"; 7 - 8 - const publicUrl = env.PUBLIC_URL; 9 - const url = publicUrl || `http://127.0.0.1:${env.PORT}`; 10 - const enc = encodeURIComponent; 11 - export const atclient = new NodeOAuthClient({ 12 - clientMetadata: { 13 - client_name: "teal", 14 - client_id: publicUrl 15 - ? `${url}/client-metadata.json` 16 - : `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc("atproto transition:generic")}`, 17 - client_uri: url, 18 - redirect_uris: [`${url}/oauth/callback`, `${url}/oauth/callback/app`], 19 - scope: "atproto transition:generic", 20 - grant_types: ["authorization_code", "refresh_token"], 21 - response_types: ["code"], 22 - application_type: "web", 23 - token_endpoint_auth_method: "none", 24 - dpop_bound_access_tokens: true, 25 - }, 26 - stateStore: new StateStore(db), 27 - sessionStore: new SessionStore(db), 28 - });
-166
apps/aqua/src/auth/router.ts
··· 1 - import { atclient } from "./client"; 2 - import { db } from "@teal/db/connect"; 3 - import { atProtoSession } from "@teal/db/schema"; 4 - import { eq } from "drizzle-orm"; 5 - import { EnvWithCtx, TealContext } from "@/ctx"; 6 - import { Hono } from "hono"; 7 - import { tealSession } from "@teal/db/schema"; 8 - import { setCookie } from "hono/cookie"; 9 - import { env } from "@/lib/env"; 10 - 11 - const publicUrl = env.PUBLIC_URL; 12 - const redirectBase = publicUrl || `http://127.0.0.1:${env.PORT}`; 13 - 14 - export function generateState(prefix?: string) { 15 - const state = crypto.randomUUID(); 16 - return `${prefix}${prefix ? ":" : ""}${state}`; 17 - } 18 - 19 - const SPA_PREFIX = "a37d"; 20 - 21 - // /oauth/login?handle=teal.fm 22 - export async function login(c: TealContext) { 23 - const { handle, spa } = c.req.query(); 24 - if (!handle) { 25 - return Response.json({ error: "Missing handle" }); 26 - } 27 - const url = await atclient.authorize(handle, { 28 - scope: "atproto transition:generic", 29 - // state.appState in callback 30 - state: generateState(spa ? SPA_PREFIX : undefined), 31 - }); 32 - return c.json({ url }); 33 - } 34 - 35 - // Redirect to the app's callback URL. 36 - async function callbackToApp(c: TealContext) { 37 - const queries = c.req.query(); 38 - const params = new URLSearchParams(queries); 39 - return c.redirect(`${env.APP_URI}/oauth/callback?${params.toString()}`); 40 - } 41 - 42 - export async function callback(c: TealContext, isSpa: boolean = false) { 43 - try { 44 - const honoParams = c.req.query(); 45 - console.log("params", honoParams); 46 - const params = new URLSearchParams(honoParams); 47 - 48 - const { session, state } = await atclient.callback(params); 49 - 50 - console.log("state", state); 51 - 52 - const did = session.did; 53 - 54 - // Process successful authentication here 55 - console.log("User authenticated as:", did); 56 - 57 - // gen opaque tealSessionKey 58 - const sess = crypto.randomUUID(); 59 - await db 60 - .insert(tealSession) 61 - .values({ 62 - key: sess, 63 - // ATP session key (DID) 64 - session: JSON.stringify(did), 65 - provider: "atproto", 66 - }) 67 - .execute(); 68 - 69 - // cookie time 70 - console.log("Setting cookie", sess); 71 - setCookie(c, "tealSession", "teal:" + sess, { 72 - httpOnly: true, 73 - secure: env.HOST.startsWith("https"), 74 - sameSite: "lax", 75 - path: "/", 76 - maxAge: 60 * 60 * 24 * 365, 77 - }); 78 - 79 - if (isSpa) { 80 - return c.json({ 81 - provider: "atproto", 82 - jwt: did, 83 - accessToken: did, 84 - }); 85 - } 86 - 87 - return c.redirect("/"); 88 - } catch (e) { 89 - console.error(e); 90 - return Response.json({ error: "Could not authorize user" }); 91 - } 92 - } 93 - 94 - // Refresh an access token from a refresh token. Should be only used in SPAs. 95 - // Pass in 'key' and 'refresh_token' query params. 96 - export async function refresh(c: TealContext) { 97 - try { 98 - const honoParams = c.req.query(); 99 - console.log("params", honoParams); 100 - const params = new URLSearchParams(honoParams); 101 - let key = params.get("key"); 102 - let refresh_token = params.get("refresh_token"); 103 - if (!key || !refresh_token) { 104 - return Response.json({ error: "Missing key or refresh_token" }); 105 - } 106 - // check if refresh token is valid 107 - let r_tk_check = (await db 108 - .select() 109 - .from(atProtoSession) 110 - .where(eq(atProtoSession.key, key)) 111 - .execute()) as any; 112 - 113 - if (r_tk_check.tokenSet.refresh_token !== refresh_token) { 114 - return Response.json({ error: "Invalid refresh token" }); 115 - } 116 - 117 - const session = await atclient.restore(key); 118 - 119 - const did = session.did; 120 - 121 - // Process successful authentication here 122 - console.log("User authenticated as:", did); 123 - 124 - // gen opaque tealSessionKey 125 - const sess = crypto.randomUUID(); 126 - await db 127 - .insert(tealSession) 128 - .values({ 129 - key: sess, 130 - // ATP session key (DID) 131 - session: JSON.stringify(did), 132 - provider: "atproto", 133 - }) 134 - .execute(); 135 - 136 - // cookie time 137 - console.log("Setting cookie", sess); 138 - setCookie(c, "tealSession", "teal:" + sess, { 139 - httpOnly: true, 140 - secure: env.HOST.startsWith("https"), 141 - sameSite: "lax", 142 - path: "/", 143 - maxAge: 60 * 60 * 24 * 365, 144 - }); 145 - 146 - return c.json({ 147 - provider: "atproto", 148 - jwt: did, 149 - accessToken: did, 150 - }); 151 - } catch (e) { 152 - console.error(e); 153 - return Response.json({ error: "Could not authorize user" }); 154 - } 155 - } 156 - 157 - const app = new Hono<EnvWithCtx>(); 158 - 159 - app.get("/login", async (c) => login(c)); 160 - app.get("/callback", async (c) => callback(c)); 161 - app.get("/callback/app", async (c) => callback(c, true)); 162 - app.get("/refresh", async (c) => refresh(c)); 163 - 164 - export const getAuthRouter = () => { 165 - return app; 166 - };
-71
apps/aqua/src/auth/storage.ts
··· 1 - import type { 2 - NodeSavedSession, 3 - NodeSavedSessionStore, 4 - NodeSavedState, 5 - NodeSavedStateStore, 6 - } from "@atproto/oauth-client-node"; 7 - import type { Database } from "@teal/db/connect"; 8 - import { atProtoSession, authState } from "@teal/db/schema"; 9 - import { eq } from "drizzle-orm"; 10 - 11 - export class StateStore implements NodeSavedStateStore { 12 - constructor(private db: Database) {} 13 - async get(key: string): Promise<NodeSavedState | undefined> { 14 - const result = await this.db 15 - .select() 16 - .from(authState) 17 - .where(eq(authState.key, key)) 18 - .limit(1) 19 - .execute(); 20 - if (!result[0]) return; 21 - return JSON.parse(result[0].state) as NodeSavedState; 22 - } 23 - async set(key: string, val: NodeSavedState) { 24 - const state = JSON.stringify(val); 25 - console.log("inserting state", key, state); 26 - await this.db 27 - .insert(authState) 28 - .values({ key, state }) 29 - .onConflictDoUpdate({ 30 - set: { state: state }, 31 - target: authState.key, 32 - }) 33 - .execute(); 34 - } 35 - async del(key: string) { 36 - await this.db.delete(authState).where(eq(authState.key, key)).execute(); 37 - //.deleteFrom("auth_state").where("key", "=", key).execute(); 38 - } 39 - } 40 - 41 - export class SessionStore implements NodeSavedSessionStore { 42 - constructor(private db: Database) {} 43 - async get(key: string): Promise<NodeSavedSession | undefined> { 44 - const result = await this.db 45 - .select() 46 - .from(atProtoSession) 47 - .where(eq(atProtoSession.key, key)) 48 - .limit(1) 49 - .all(); 50 - if (!result[0]) return; 51 - return JSON.parse(result[0].session) as NodeSavedSession; 52 - } 53 - async set(key: string, val: NodeSavedSession) { 54 - const session = JSON.stringify(val); 55 - console.log("inserting session", key, session); 56 - await this.db 57 - .insert(atProtoSession) 58 - .values({ key, session }) 59 - .onConflictDoUpdate({ 60 - set: { session: session }, 61 - target: atProtoSession.key, 62 - }) 63 - .execute(); 64 - } 65 - async del(key: string) { 66 - await this.db 67 - .delete(atProtoSession) 68 - .where(eq(atProtoSession.key, key)) 69 - .execute(); 70 - } 71 - }
+5 -10
apps/aqua/src/ctx.ts
··· 1 1 import { NodeOAuthClient } from "@atproto/oauth-client-node"; 2 - import { Client } from "@libsql/client/."; 3 - import { LibSQLDatabase } from "drizzle-orm/libsql"; 2 + import { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 4 3 import { Context, Next } from "hono"; 5 4 import { Logger } from "pino"; 6 - import { atclient } from "./auth/client"; 5 + 6 + import { db as tdb } from "@teal/db"; 7 7 8 8 export type TealContext = Context<EnvWithCtx, any, any>; 9 9 ··· 13 13 14 14 export type Ctx = { 15 15 auth: NodeOAuthClient; 16 - db: LibSQLDatabase<typeof import("@teal/db/schema")> & { 17 - $client: Client; 18 - }; 16 + db: typeof tdb; 19 17 logger: Logger<never, boolean>; 20 18 }; 21 19 22 20 export const setupContext = async ( 23 21 c: TealContext, 24 - db: LibSQLDatabase<typeof import("@teal/db/schema")> & { 25 - $client: Client; 26 - }, 22 + db: typeof tdb, 27 23 logger: Logger<never, boolean>, 28 24 next: Next, 29 25 ) => { 30 26 c.set("db", db); 31 - c.set("auth", atclient); 32 27 c.set("logger", logger); 33 28 await next(); 34 29 };
+28
apps/aqua/src/did/web.ts
··· 1 + // get info and respond with a did web 2 + 3 + /// Responds with a did:web with a TealFmAppview service at the given domain. 4 + export function createDidWeb(domain: string, pubKey: string) { 5 + return { 6 + '@context': [ 7 + 'https://www.w3.org/ns/did/v1', 8 + 'https://w3id.org/security/multikey/v1', 9 + 'https://w3id.org/security/suites/secp256k1-2019/v1', 10 + ], 11 + id: 'did:web:' + domain, 12 + verificationMethod: [ 13 + { 14 + id: 'did:web:' + domain + '#atproto', 15 + type: 'Multikey', 16 + controller: 'did:web:' + domain, 17 + publicKeyMultibase: pubKey, 18 + }, 19 + ], 20 + service: [ 21 + { 22 + id: '#teal_fm_appview', 23 + type: 'TealFmAppView', 24 + serviceEndpoint: 'https://' + domain, 25 + }, 26 + ], 27 + }; 28 + }
+58 -270
apps/aqua/src/index.ts
··· 1 - import { serve } from "@hono/node-server"; 2 - import { serveStatic } from "@hono/node-server/serve-static"; 3 - import { Hono } from "hono"; 4 - import { db } from "@teal/db/connect"; 5 - import { getAuthRouter } from "./auth/router"; 6 - import pino from "pino"; 7 - import { EnvWithCtx, setupContext, TealContext } from "./ctx"; 8 - import { env } from "./lib/env"; 9 - import { getCookie, deleteCookie } from "hono/cookie"; 10 - import { atclient } from "./auth/client"; 11 - import { getSessionAgent } from "./lib/auth"; 12 - import { RichText } from "@atproto/api"; 13 - import { sanitizeUrl } from "@braintree/sanitize-url"; 14 - import { getXrpcRouter } from "./xrpc/route"; 1 + import { env } from './lib/env'; 15 2 16 - const HEAD = `<head> 17 - <link rel="stylesheet" href="/latex.css"> 18 - </head>`; 3 + import { serve } from '@hono/node-server'; 4 + import { serveStatic } from '@hono/node-server/serve-static'; 5 + import { Hono } from 'hono'; 6 + import { db } from '@teal/db/connect'; 7 + import pino from 'pino'; 8 + import { EnvWithCtx, setupContext, TealContext } from './ctx'; 9 + import { getXrpcRouter } from './xrpc/route'; 10 + import { createDidWeb } from './did/web'; 19 11 20 - const logger = pino({ name: "server start" }); 12 + const logger = pino({ name: 'server start' }); 21 13 22 14 const app = new Hono<EnvWithCtx>(); 23 15 24 16 app.use((c, next) => setupContext(c, db, logger, next)); 25 17 26 - app.route("/oauth", getAuthRouter()); 27 - 28 - app.route("/xrpc", getXrpcRouter()); 29 - 30 - app.get("/client-metadata.json", (c) => { 31 - return c.json(atclient.clientMetadata); 32 - }); 33 - 34 - app.get("/", async (c) => { 35 - const tealSession = getCookie(c, "tealSession"); 18 + const HOME_TEXT = ` 36 19 37 - // Serve logged in content 38 - if (tealSession) { 39 - // const followers = await agent?.getFollowers(); 40 - return c.html( 41 - ` 42 - ${HEAD} 43 - <div id="root"> 44 - <div id="header" style="display: flex; flex-direction: column; gap: 0.5rem; width: 100%;"> 45 - <div> 46 - <h1>teal.fm</h1> 47 - <p>Your music, beautifully tracked. (soon.)</p> 48 - </div> 49 - <div style=" width: 100%; display: flex; flex-direction: row; justify-content: space-between; gap: 0.5rem;"> 50 - <div> 51 - <a href="/">home</a> 52 - <a href="/stamp">stamp</a> 53 - </div> 54 - <form action="/logout" method="post" class="session-form"> 55 - <button type="submit" style="background-color: #cc0000; color: white; border: none; padding: 0rem 0.5rem; border-radius: 0.5rem;">logout</button> 56 - </form> 57 - </div> 58 - </div> 59 - <div class="container"> 20 + █████ 21 + ███ ███ 22 + ██ ▐▌ ██ ██▌ 23 + █ ██ █ ▐█ █▌ 24 + █ ███████ █ ██ 25 + █ ██ █ ▐█ ▐▄▄▄ ▄▄▄ 26 + █ ██ █ ▐█ ▐█▌ ██ █▌ ██ 27 + █ ██ █ █████ █▌ ▐█▌ ▐█ 28 + █ ██ █ ██ █▌ ▐█ ▐█ ▐█ 29 + ██ ███ █████ █▌ █▌ █▌ █▌▐ 30 + ███ █████■██ ██ █ █ ▐█▀ 31 + █████ ███ █▌ 32 + ▐█ █▌ 33 + ▐██ 60 34 61 - </div> 62 - </div>`, 63 - ); 64 - } 35 + You have reached 'aqua', an AT Protocol Application View (AppView) 36 + for the 'teal.fm' application. 65 37 66 - // Serve non-logged in content 67 - return c.html( 68 - ` 69 - ${HEAD} 70 - <div id="root"> 71 - <div id="header"> 72 - <h1>teal.fm</h1> 73 - <p>Your music, beautifully tracked. (soon.)</p> 74 - <div style=" width: 100%; display: flex; flex-direction: row; justify-content: space-between; gap: 0.5rem;"> 75 - <div> 76 - <a href="/">home</a> 77 - <a href="/stamp">stamp</a> 78 - </div> 79 - <button style="background-color: #acf; color: white; border: none; padding: 0rem 0.5rem; border-radius: 0.5rem;"><a href="/login">Login</a></button> 80 - </div> 81 - </div> 82 - <div class="container"> 83 - <div class="signup-cta"> 84 - Don't have an account on the Atmosphere? 85 - <a href="https://bsky.app">Sign up for Bluesky</a> to create one now! 86 - </div> 87 - </div> 88 - </div>`, 89 - ); 90 - }); 38 + Most API routes are under /xrpc/ 91 39 92 - app.get("/login", (c) => { 93 - const tealSession = getCookie(c, "tealSession"); 40 + Code: <a href="https://github.com/teal-fm/teal">github.com/teal-fm/teal</a> 41 + Protocol: <a href="https://atproto.com">atproto.com</a> 94 42 95 - return c.html( 96 - ` 97 - ${HEAD} 98 - <div id="root"> 99 - <div id="header"> 100 - <h1>teal.fm</h1> 101 - <p>Your music, beautifully tracked. (soon.)</p> 102 - <div style=" width: 100%; display: flex; flex-direction: row; justify-content: space-between; gap: 0.5rem;"> 103 - <div> 104 - <a href="/">home</a> 105 - <a href="/stamp">stamp</a> 106 - </div> 107 - <div /> 108 - </div> 109 - </div> 110 - <div class="container"> 111 - <form action="/login" method="post" class="login-form"> 112 - <input 113 - type="text" 114 - name="handle" 115 - placeholder="Enter your handle (eg alice.bsky.social)" 116 - required 117 - /> 118 - <button type="submit">Log in</button> 119 - </form> 120 - <div class="signup-cta"> 121 - Don't have an account on the Atmosphere? 122 - <a href="https://bsky.app">Sign up for Bluesky</a> to create one now! 123 - </div> 124 - </div> 125 - </div>`, 126 - ); 127 - }); 43 + Visit <a href="https://docs.teal.fm">docs.teal.fm</a> for more information.`; 128 44 129 - app.post("/login", async (c: TealContext) => { 130 - const body = await c.req.parseBody(); 131 - let { handle } = body; 132 - // shouldn't be a file, escape now 133 - if (handle instanceof File) return c.redirect("/login"); 134 - handle = sanitizeUrl(handle); 135 - console.log("handle", handle); 136 - // Initiate the OAuth flow 137 - try { 138 - console.log("Calling authorize"); 139 - if (typeof handle === "string") { 140 - const url = await atclient.authorize(handle, { 141 - scope: "atproto transition:generic", 142 - }); 143 - console.log("Redirecting to oauth login page"); 144 - console.log(url); 145 - return Response.redirect(url); 146 - } 147 - } catch (e) { 148 - console.error(e); 149 - return Response.json({ error: "Could not authorize user" }); 45 + const HOME_STYLES = ` 46 + body { 47 + background-color: #000; 48 + color: #ddd; 150 49 } 151 - }); 152 - 153 - app.post("/logout", (c) => { 154 - deleteCookie(c, "tealSession"); 155 - // TODO: delete session record from db?? 156 - return c.redirect("/"); 157 - }); 158 - 159 - app.get("/stamp", (c) => { 160 - // check logged in 161 - const tealSession = getCookie(c, "tealSession"); 162 - if (!tealSession) { 163 - return c.redirect("/login"); 50 + a { 51 + color: #aaf; 52 + text-decoration: none; 164 53 } 165 - return c.html( 166 - ` 167 - ${HEAD} 168 - <div id="root"> 169 - <div id="header"> 170 - <h1>teal.fm</h1> 171 - <p>Your music, beautifully tracked. (soon.)</p> 172 - <div style=" width: 100%; display: flex; flex-direction: row; justify-content: space-between; gap: 0.5rem;"> 173 - <div> 174 - <a href="/">home</a> 175 - <a href="/stamp">stamp</a> 176 - </div> 177 - <form action="/logout" method="post" class="session-form"> 178 - <button type="submit" style="background-color: #cc0000; color: white; border: none; padding: 0rem 0.5rem; border-radius: 0.5rem;">logout</button> 179 - </form> 180 - </div> 181 - </div> 182 - <div class="container"> 183 - <p>🛠️ while we're building our music tracker, share what you're listening to here!<br/> 184 - <a href="https://emojipedia.org/white-flower">💮</a> we'll create a post on Bluesky for you to share with the world!<br/>​</p> 185 - <form action="/stamp" method="post" class="login-form" style="display: flex; flex-direction: column; gap: 0.5rem;"> 186 - <input 187 - type="text" 188 - name="artist" 189 - placeholder="artist name (eg blink-182)" 190 - required 191 - /> 192 - <input 193 - type="text" 194 - name="track" 195 - placeholder="track title (eg what's my age again?)" 196 - required 197 - /> 198 - <input 199 - type="text" 200 - name="link" 201 - placeholder="https://www.youtube.com/watch?v=K7l5ZeVVoCA" 202 - /> 203 - <button type="submit" style="width: 15%">Stamp!</button> 204 - </form> 205 - <div class="signup-cta"> 206 - Don't have an account on the Atmosphere? 207 - <a href="https://bsky.app">Sign up for Bluesky</a> to create one now! 208 - </div> 209 - </div> 210 - </div>`, 211 - ); 212 - }); 213 - 214 - app.post("/stamp", async (c: TealContext) => { 215 - const body = await c.req.parseBody(); 216 - let { artist, track, link } = body; 217 - // shouldn't get a File, escape now 218 - if (artist instanceof File || track instanceof File || link instanceof File) 219 - return c.redirect("/stamp"); 220 - 221 - artist = sanitizeUrl(artist); 222 - track = sanitizeUrl(track); 223 - link = sanitizeUrl(link); 54 + a:hover { 55 + text-decoration: underline; 56 + } 57 + `; 224 58 225 - const agent = await getSessionAgent(c); 59 + app.get('/', (c) => 60 + c.html( 61 + `<html><head><style>${HOME_STYLES}</style></head><body><pre>${HOME_TEXT}</pre></body></html>`, 62 + ), 63 + ); 226 64 227 - if (agent) { 228 - const rt = new RichText({ 229 - text: `💮 now playing: 230 - artist: ${artist} 231 - track: ${track} 65 + app.route('/xrpc', getXrpcRouter()); 232 66 233 - powered by @teal.fm`, 234 - }); 235 - await rt.detectFacets(agent); 236 - 237 - let embed = undefined; 238 - if (link) { 239 - embed = { 240 - $type: "app.bsky.embed.external", 241 - external: { 242 - uri: link, 243 - title: track, 244 - description: `${artist} - ${track}`, 245 - }, 246 - }; 247 - } 248 - const post = await agent.post({ 249 - text: rt.text, 250 - facets: rt.facets, 251 - embed: embed, 252 - }); 253 - 254 - return c.html( 255 - ` 256 - ${HEAD} 257 - <div id="root"> 258 - <div id="header"> 259 - <h1>teal.fm</h1> 260 - <p>Your music, beautifully tracked. (soon.)</p> 261 - <div style=" width: 100%; display: flex; flex-direction: row; justify-content: space-between; gap: 0.5rem;"> 262 - <div> 263 - <a href="/">home</a> 264 - <a href="/stamp">stamp</a> 265 - </div> 266 - <form action="/logout" method="post" class="session-form"> 267 - <button type="submit" style="background-color: #cc0000; color: white; border: none; padding: 0rem 0.5rem; border-radius: 0.5rem;">logout</button> 268 - </form> 269 - </div> 270 - </div> 271 - <div class="container"> 272 - <h2 class="stamp-success">Success! 🎉</h2> 273 - <p>Your post is being tracked by the Atmosphere.</p> 274 - <p>You can view it <a href="https://bsky.app/profile/${agent.did}/post/${post.uri.split("/").pop()}">here</a>.</p> 275 - </div> 276 - </div>`, 277 - ); 278 - } 279 - return c.html( 280 - `<h1>doesn't look like you're logged in... try <a href="/login">logging in?</a></h1>`, 281 - ); 282 - }); 67 + app.get('/.well-known/did.json', (c) => 68 + // assume public url is https! 69 + c.json(createDidWeb(env.PUBLIC_URL.split('://')[1], env.DID_WEB_PUBKEY)), 70 + ); 283 71 284 - app.use("/*", serveStatic({ root: "/public" })); 72 + app.use('/*', serveStatic({ root: '/public' })); 285 73 286 74 const run = async () => { 287 - logger.info("Running in " + navigator.userAgent); 288 - if (navigator.userAgent.includes("Node")) { 75 + logger.info('Running in ' + navigator.userAgent); 76 + if (navigator.userAgent.includes('Node')) { 289 77 serve( 290 78 { 291 79 fetch: app.fetch, ··· 295 83 (info) => { 296 84 logger.info( 297 85 `Listening on ${ 298 - info.address == "::1" 299 - ? "http://localhost" 86 + info.address == '::1' 87 + ? 'http://0.0.0.0' 300 88 : // TODO: below should probably be https:// 301 89 // but i just want to ctrl click in the terminal 302 - "http://" + info.address 90 + 'http://' + info.address 303 91 }:${info.port} (${info.family})`, 304 92 ); 305 93 },
-77
apps/aqua/src/lib/auth.ts
··· 1 - import { TealContext } from "@/ctx"; 2 - import { db } from "@teal/db/connect"; 3 - import { Session } from "@atproto/oauth-client-node"; 4 - import { tealSession } from "@teal/db/schema"; 5 - import { eq } from "drizzle-orm"; 6 - import { deleteCookie, getCookie } from "hono/cookie"; 7 - import { atclient } from "@/auth/client"; 8 - import { Agent } from "@atproto/api"; 9 - 10 - interface UserSession { 11 - did: string; 12 - /// The session JWT from ATProto 13 - session: Session; 14 - } 15 - 16 - interface UserInfo { 17 - did: string; 18 - handle: string; 19 - } 20 - 21 - export async function getUserInfo( 22 - c: TealContext 23 - ): Promise<UserInfo | undefined> { 24 - // init session agent 25 - const agent = await getSessionAgent(c); 26 - if (agent && agent.did) { 27 - // fetch from ATProto 28 - const res = await agent.app.bsky.actor.getProfile({ 29 - actor: agent.did, 30 - }); 31 - if (res.success) { 32 - return { 33 - did: agent.did, 34 - handle: res.data.handle, 35 - }; 36 - } else { 37 - throw new Error("Failed to fetch user info"); 38 - } 39 - } 40 - } 41 - 42 - export async function getContextDID(c: TealContext): Promise<string> { 43 - let authSession = getCookie(c, "tealSession")?.split("teal:")[1]; 44 - console.log(`tealSession cookie: ${authSession}`); 45 - if (!authSession) { 46 - authSession = c.req.header("Authorization"); 47 - } 48 - if (!authSession) { 49 - throw new Error("No auth session found"); 50 - } else { 51 - // get the DID from the session 52 - const session = await db.query.tealSession.findFirst({ 53 - where: eq(tealSession.key, authSession), 54 - }).execute(); 55 - 56 - if (!session) { 57 - // we should log them out here and redirect to home to double check 58 - deleteCookie(c, "tealSession"); 59 - c.redirect("/"); 60 - throw new Error("No DID found in session"); 61 - } 62 - return session.session.replace(/['"]/g, ""); 63 - } 64 - } 65 - 66 - export async function getSessionAgent(c: TealContext) { 67 - const did = await getContextDID(c); 68 - 69 - if (did != undefined) { 70 - const oauthsession = await atclient.restore(did); 71 - const agent = new Agent(oauthsession); 72 - if (!agent) { 73 - return null; 74 - } 75 - return agent; 76 - } 77 - }
+11 -10
apps/aqua/src/lib/env.ts
··· 1 - import dotenv from "dotenv"; 2 - import { cleanEnv, host, port, str, testOnly } from "envalid"; 3 - import process from "node:process"; 1 + import dotenv from 'dotenv'; 2 + import { cleanEnv, host, port, str, testOnly } from 'envalid'; 3 + import process from 'node:process'; 4 4 5 5 dotenv.config(); 6 6 // in case our .env file is in the root folder 7 - dotenv.config({ path: "./../../.env" }); 7 + dotenv.config({ path: './../../.env' }); 8 8 9 9 export const env = cleanEnv(process.env, { 10 10 NODE_ENV: str({ 11 - devDefault: testOnly("test"), 12 - choices: ["development", "production", "test"], 11 + devDefault: testOnly('test'), 12 + choices: ['development', 'production', 'test'], 13 13 }), 14 - HOST: host({ devDefault: testOnly("0.0.0.0") }), 14 + HOST: host({ devDefault: testOnly('0.0.0.0') }), 15 15 PORT: port({ devDefault: testOnly(3000) }), 16 16 PUBLIC_URL: str({}), 17 - APP_URI: str({ devDefault: "fm.teal.amethyst://" }), 18 - DB_PATH: str({ devDefault: "file:./db.sqlite" }), 19 - COOKIE_SECRET: str({ devDefault: "secret_cookie! very secret!" }), 17 + DID_WEB_PUBKEY: str({ devDefault: testOnly('did:key:z6Mk...') }), 18 + APP_URI: str({ devDefault: 'fm.teal.amethyst://' }), 19 + DATABASE_URL: str({ devDefault: 'file:./db.sqlite' }), 20 + COOKIE_SECRET: str({ devDefault: 'secret_cookie! very secret!' }), 20 21 });
+52
apps/aqua/src/xrpc/actor/getProfile.ts
··· 1 + import { TealContext } from '@/ctx'; 2 + import { db, profiles } from '@teal/db'; 3 + import { eq } from 'drizzle-orm'; 4 + import { OutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile'; 5 + 6 + export default async function getProfile(c: TealContext) { 7 + const params = c.req.query(); 8 + if (!params.actor) { 9 + throw new Error('actor is required'); 10 + } 11 + 12 + // Assuming 'user' can be either a DID or a handle. We'll try to resolve 13 + // the DID first, and if that fails, try to resolve the handle. 14 + let profile; 15 + 16 + //First try to get by did 17 + profile = await db 18 + .select() 19 + .from(profiles) 20 + .where(eq(profiles.did, params.actor)) 21 + .limit(1); 22 + 23 + //If not found, try to get by handle 24 + if (!profile) { 25 + profile = await db 26 + .select() 27 + .from(profiles) 28 + .where(eq(profiles.handle, params.actor)) 29 + .limit(1); 30 + } 31 + 32 + if (!profile) { 33 + throw new Error('Profile not found'); 34 + } 35 + 36 + profile = profile[0]; 37 + 38 + const res: OutputSchema = { 39 + actor: { 40 + did: profile.did, 41 + handle: profile.handle, 42 + displayName: profile.displayName || undefined, 43 + description: profile.description || undefined, 44 + descriptionFacets: [], 45 + avatar: profile.avatar || undefined, 46 + banner: profile.banner || undefined, 47 + createdAt: profile.createdAt?.toISOString(), 48 + }, 49 + }; 50 + 51 + return res; 52 + }
+98
apps/aqua/src/xrpc/actor/searchActors.ts
··· 1 + import { TealContext } from '@/ctx'; 2 + import { db, profiles } from '@teal/db'; 3 + import { like, or, sql, lt, gt, and } from 'drizzle-orm'; 4 + import { OutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/searchActors'; 5 + 6 + export default async function searchActors(c: TealContext) { 7 + const params = c.req.query(); 8 + const limit = (params.limit ? parseInt(params.limit) : 25) || 25; // Ensure limit is a number 9 + const requestedLimit = limit + 1; // Fetch one extra for cursor detection 10 + 11 + if (!params.q) { 12 + c.status(400); 13 + c.error = new Error('q is required'); 14 + return; 15 + } 16 + 17 + const query = params.q.toLowerCase(); 18 + 19 + try { 20 + let queryBuilder = db 21 + .select({ 22 + did: profiles.did, 23 + handle: profiles.handle, 24 + displayName: profiles.displayName, 25 + description: profiles.description, 26 + avatar: profiles.avatar, 27 + banner: profiles.banner, 28 + createdAt: profiles.createdAt, 29 + }) 30 + .from(profiles); 31 + 32 + // Base WHERE clause (always applied) 33 + const baseWhere = or( 34 + like(sql`lower(${profiles.handle})`, `%${query}%`), 35 + like(sql`lower(${profiles.displayName})`, `%${query}%`), 36 + like(sql`lower(${profiles.description})`, `%${query}%`), 37 + sql`${profiles.handle} = ${params.q}`, 38 + sql`${profiles.displayName} = ${params.q}`, 39 + ); 40 + 41 + if (params.cursor) { 42 + // Decode the cursor 43 + const [createdAtStr, didStr] = Buffer.from(params.cursor, 'base64') 44 + .toString('utf-8') 45 + .split(':'); 46 + 47 + const createdAt = new Date(createdAtStr); 48 + 49 + // Cursor condition: (createdAt > cursor.createdAt) OR (createdAt == cursor.createdAt AND did > cursor.did) 50 + queryBuilder.where( 51 + and( 52 + baseWhere, // Apply the base search terms 53 + or( 54 + gt(profiles.createdAt, createdAt), 55 + and( 56 + sql`${profiles.createdAt} = ${createdAt}`, 57 + gt(profiles.did, didStr), // Compare did as string 58 + ), 59 + ), 60 + ), 61 + ); 62 + } else { 63 + queryBuilder.where(baseWhere); // Just the base search if no cursor 64 + } 65 + 66 + queryBuilder 67 + .orderBy(profiles.createdAt, profiles.did) 68 + .limit(requestedLimit); // Order by both, limit + 1 69 + 70 + const results = await queryBuilder; 71 + 72 + // Build the next cursor (if there are more results) 73 + let nextCursor = null; 74 + if (results.length > limit) { 75 + const lastResult = results[limit - 1]; // Get the *limit*-th, not limit+1-th 76 + nextCursor = Buffer.from( 77 + `${lastResult.createdAt?.toISOString() || ''}:${lastResult.did}`, 78 + ).toString('base64'); 79 + results.pop(); // Remove the extra record we fetched 80 + } 81 + const res: OutputSchema = { 82 + actors: results.map((profile) => ({ 83 + did: profile.did, 84 + handle: profile.handle ?? undefined, 85 + displayName: profile.displayName ?? undefined, 86 + avatar: profile.avatar ?? undefined, 87 + banner: profile.banner ?? undefined, 88 + })), 89 + cursor: nextCursor || undefined, 90 + }; 91 + 92 + return res; 93 + } catch (error) { 94 + console.error('Database error:', error); 95 + c.status(500); 96 + throw new Error('Internal server error'); 97 + } 98 + }
+102 -53
apps/aqua/src/xrpc/feed/getActorFeed.ts
··· 1 - import { TealContext } from "@/ctx"; 2 - import { db, tealSession, play } from "@teal/db"; 3 - import { eq, and, lt } from "drizzle-orm"; 4 - import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed"; 1 + import { TealContext } from '@/ctx'; 2 + import { artists, db, plays, playToArtists } from '@teal/db'; 3 + import { eq, and, lt, desc, sql } from 'drizzle-orm'; 4 + import { OutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed'; 5 5 6 6 export default async function getActorFeed(c: TealContext) { 7 7 const params = c.req.query(); 8 - if (!params.authorDid) { 9 - throw new Error("authorDid is required"); 8 + if (!params.authorDID) { 9 + throw new Error('authorDID is required'); 10 10 } 11 11 12 - let limit = 20 12 + let limit = 20; 13 13 14 - if(params.limit) { 15 - limit = Number(params.limit) 16 - if(limit > 50) throw new Error("Limit is over max allowed.") 14 + if (params.limit) { 15 + limit = Number(params.limit); 16 + if (limit > 50) throw new Error('Limit is over max allowed.'); 17 17 } 18 18 19 19 // 'and' is here for typing reasons 20 - let whereClause = and(eq(play.authorDid, params.authorDid)); 20 + let whereClause = and(eq(plays.did, params.authorDID)); 21 21 22 22 // Add cursor pagination if provided 23 23 if (params.cursor) { 24 - const [cursorPlay] = await db 25 - .select({ createdAt: play.createdAt }) 26 - .from(play) 27 - .where(eq(play.uri, params.cursor)) 24 + const cursorResult = await db 25 + .select() 26 + .from(plays) 27 + .where(eq(plays.uri, params.cursor)) 28 28 .limit(1); 29 29 30 + const cursorPlay = cursorResult[0]?.playedTime; 31 + 30 32 if (!cursorPlay) { 31 - throw new Error("Cursor not found"); 33 + throw new Error('Cursor not found'); 32 34 } 33 35 34 - whereClause = and(whereClause, lt(play.createdAt, cursorPlay.createdAt)); 36 + whereClause = and(whereClause, lt(plays.playedTime, cursorPlay as any)); 35 37 } 36 38 37 - const plays = await db 38 - .select() 39 - .from(play) 39 + const playRes = await db 40 + .select({ 41 + uri: plays.uri, 42 + did: plays.did, 43 + playedTime: plays.playedTime, 44 + trackName: plays.trackName, 45 + cid: plays.cid, 46 + recordingMbid: plays.recordingMbid, 47 + duration: plays.duration, 48 + releaseName: plays.releaseName, 49 + releaseMbid: plays.releaseMbid, 50 + isrc: plays.isrc, 51 + originUrl: plays.originUrl, 52 + processedTime: plays.processedTime, 53 + submissionClientAgent: plays.submissionClientAgent, 54 + musicServiceBaseDomain: plays.musicServiceBaseDomain, 55 + artists: sql<Array<{ mbid: string; name: string }>>` 56 + COALESCE( 57 + ( 58 + SELECT jsonb_agg(jsonb_build_object('mbid', pa.artist_mbid, 'name', pa.artist_name)) 59 + FROM ${playToArtists} pa 60 + WHERE pa.play_uri = ${plays.uri} 61 + AND pa.artist_mbid IS NOT NULL 62 + AND pa.artist_name IS NOT NULL -- Ensure both are non-null 63 + ), 64 + '[]'::jsonb -- Correct empty JSONB array literal 65 + )`.as('artists'), 66 + }) 67 + .from(plays) 68 + .leftJoin(playToArtists, sql`${plays.uri} = ${playToArtists.playUri}`) 40 69 .where(whereClause) 41 - .orderBy(play.createdAt) 42 - .limit(10); 43 - 44 - if (plays.length === 0) { 45 - throw new Error("Play not found"); 46 - } 70 + .groupBy( 71 + plays.uri, 72 + plays.cid, 73 + plays.did, 74 + plays.duration, 75 + plays.isrc, 76 + plays.musicServiceBaseDomain, 77 + plays.originUrl, 78 + plays.playedTime, 79 + plays.processedTime, 80 + plays.rkey, 81 + plays.recordingMbid, 82 + plays.releaseMbid, 83 + plays.releaseName, 84 + plays.submissionClientAgent, 85 + plays.trackName, 86 + ) 87 + .orderBy(desc(plays.playedTime)) 88 + .limit(limit); 89 + const cursor = 90 + playRes.length === limit ? playRes[playRes.length - 1]?.uri : undefined; 47 91 48 92 return { 49 - plays: plays.map( 93 + cursor: cursor ?? undefined, // Ensure cursor itself can be undefined 94 + plays: playRes.map( 50 95 ({ 51 - uri, 52 - authorDid, 53 - createdAt, 54 - indexedAt, 96 + // Destructure fields from the DB result 55 97 trackName, 56 - trackMbId, 57 - recordingMbId, 98 + cid: trackMbId, // Note the alias was used here in the DB query select 99 + recordingMbid, 58 100 duration, 59 - artistNames, 60 - artistMbIds, 101 + artists, // This is guaranteed to be an array '[]' if no artists, due to COALESCE 61 102 releaseName, 62 - releaseMbId, 103 + releaseMbid, 63 104 isrc, 64 105 originUrl, 65 106 musicServiceBaseDomain, 66 107 submissionClientAgent, 67 108 playedTime, 109 + // Other destructured fields like uri, did, etc. are not directly used here by name 68 110 }) => ({ 69 - uri, 70 - authorDid, 71 - createdAt, 72 - indexedAt, 73 - trackName, 74 - trackMbId, 75 - recordingMbId, 76 - duration, 77 - artistNames, 78 - artistMbIds, 79 - releaseName, 80 - releaseMbId, 81 - isrc, 82 - originUrl, 83 - musicServiceBaseDomain, 84 - submissionClientAgent, 85 - playedTime, 111 + // Apply '?? undefined' to each potentially nullable/undefined scalar field 112 + trackName: trackName ?? undefined, 113 + trackMbId: trackMbId ?? undefined, 114 + recordingMbId: recordingMbid ?? undefined, 115 + duration: duration ?? undefined, 116 + 117 + // For arrays derived from a guaranteed array, map is safe. 118 + // The SQL query ensures `artists` is '[]'::jsonb if empty. 119 + // The SQL query also ensures artist.name/mbid are NOT NULL within the jsonb_agg 120 + artistNames: artists.map((artist) => artist.name), // Will be [] if artists is [] 121 + artistMbIds: artists.map((artist) => artist.mbid), // Will be [] if artists is [] 122 + 123 + releaseName: releaseName ?? undefined, 124 + releaseMbId: releaseMbid ?? undefined, 125 + isrc: isrc ?? undefined, 126 + originUrl: originUrl ?? undefined, 127 + musicServiceBaseDomain: musicServiceBaseDomain ?? undefined, 128 + submissionClientAgent: submissionClientAgent ?? undefined, 129 + 130 + // playedTime specific handling: convert to ISO string if exists, else undefined 131 + playedTime: playedTime ? playedTime.toISOString() : undefined, 132 + // Alternative using optional chaining (effectively the same) 133 + // playedTime: playedTime?.toISOString(), 86 134 }), 87 135 ), 136 + // Explicitly cast to OutputSchema. Make sure OutputSchema allows undefined for these fields. 88 137 } as OutputSchema; 89 138 }
+89 -34
apps/aqua/src/xrpc/feed/getPlay.ts
··· 1 1 import { TealContext } from "@/ctx"; 2 - import { db, tealSession, play } from "@teal/db"; 3 - import { eq, and } from "drizzle-orm"; 4 - import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getPlay"; 2 + import { db, plays, playToArtists, artists } from "@teal/db"; 3 + import { eq, and, lt, desc, sql } from "drizzle-orm"; 4 + import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed"; 5 5 6 - export default async function getPlay(c: TealContext) { 7 - // do we have required query params? 6 + export default async function getActorFeed(c: TealContext) { 8 7 const params = c.req.query(); 9 - if (params.authorDid === undefined) { 8 + if (!params.authorDid) { 10 9 throw new Error("authorDid is required"); 11 10 } 12 11 if (!params.rkey) { 13 12 throw new Error("rkey is required"); 14 13 } 15 14 16 - let res = await db 17 - .select() 18 - .from(play) 19 - .where( 20 - and(eq(play.authorDid, params.authorDid), and(eq(play.uri, params.rkey))), 15 + // Get plays with artists as arrays 16 + const playRes = await db 17 + .select({ 18 + play: plays, 19 + artists: sql<Array<{ mbid: string; name: string }>>` 20 + COALESCE( 21 + array_agg( 22 + CASE WHEN ${artists.mbid} IS NOT NULL THEN 23 + jsonb_build_object( 24 + 'mbid', ${artists.mbid}, 25 + 'name', ${artists.name} 26 + ) 27 + END 28 + ) FILTER (WHERE ${artists.mbid} IS NOT NULL), 29 + ARRAY[]::jsonb[] 30 + ) 31 + `.as("artists"), 32 + }) 33 + .from(plays) 34 + .leftJoin(playToArtists, sql`${plays.uri} = ${playToArtists.playUri}`) 35 + .leftJoin(artists, sql`${playToArtists.artistMbid} = ${artists.mbid}`) 36 + .where(and(eq(plays.did, params.authorDid), eq(plays.rkey, params.rkey))) 37 + .groupBy( 38 + plays.uri, 39 + plays.cid, 40 + plays.did, 41 + plays.duration, 42 + plays.isrc, 43 + plays.musicServiceBaseDomain, 44 + plays.originUrl, 45 + plays.playedTime, 46 + plays.processedTime, 47 + plays.rkey, 48 + plays.recordingMbid, 49 + plays.releaseMbid, 50 + plays.releaseName, 51 + plays.submissionClientAgent, 52 + plays.trackName, 21 53 ) 22 - .execute(); 54 + .orderBy(desc(plays.playedTime)) 55 + .limit(1); 23 56 24 - if (res.length === 0) { 57 + if (playRes.length === 0) { 25 58 throw new Error("Play not found"); 26 59 } 27 - res[0]; 28 60 29 - // return a PlayView 30 61 return { 31 - play: { 32 - uri: res[0].uri, 33 - authorDid: res[0].authorDid, 34 - createdAt: res[0].createdAt, 35 - indexedAt: res[0].indexedAt, 36 - trackName: res[0].trackName, 37 - trackMbId: res[0].trackMbId, 38 - recordingMbId: res[0].recordingMbId, 39 - duration: res[0].duration, 40 - artistNames: res[0].artistNames, 41 - artistMbIds: res[0].artistMbIds, 42 - releaseName: res[0].releaseName, 43 - releaseMbId: res[0].releaseMbId, 44 - isrc: res[0].isrc, 45 - originUrl: res[0].originUrl, 46 - musicServiceBaseDomain: res[0].musicServiceBaseDomain, 47 - submissionClientAgent: res[0].submissionClientAgent, 48 - playedTime: res[0].playedTime, 49 - }, 62 + plays: playRes.map(({ play, artists }) => { 63 + const { 64 + uri, 65 + did: authorDid, 66 + processedTime: createdAt, 67 + processedTime: indexedAt, 68 + trackName, 69 + cid: trackMbId, 70 + cid: recordingMbId, 71 + duration, 72 + rkey, 73 + releaseName, 74 + cid: releaseMbId, 75 + isrc, 76 + originUrl, 77 + musicServiceBaseDomain, 78 + submissionClientAgent, 79 + playedTime, 80 + } = play; 81 + 82 + return { 83 + uri, 84 + authorDid, 85 + createdAt: createdAt?.toISOString(), 86 + indexedAt: indexedAt?.toISOString(), 87 + trackName, 88 + trackMbId, 89 + recordingMbId, 90 + duration, 91 + // Replace these with actual artist data from the array 92 + artistNames: artists.map((artist) => artist.name), 93 + artistMbIds: artists.map((artist) => artist.mbid), 94 + // Or, if you want to keep the full artist objects: 95 + // artists: artists, 96 + releaseName, 97 + releaseMbId, 98 + isrc, 99 + originUrl, 100 + musicServiceBaseDomain, 101 + submissionClientAgent, 102 + playedTime: playedTime?.toISOString(), 103 + }; 104 + }), 50 105 } as OutputSchema; 51 106 }
+16 -6
apps/aqua/src/xrpc/route.ts
··· 1 - import { EnvWithCtx } from "@/ctx"; 2 - import { Hono } from "hono"; 3 - import getPlay from "./feed/getPlay"; 4 - import getActorFeed from "./feed/getActorFeed"; 1 + import { EnvWithCtx } from '@/ctx'; 2 + import { Hono } from 'hono'; 3 + import getPlay from './feed/getPlay'; 4 + import getActorFeed from './feed/getActorFeed'; 5 + import getProfile from './actor/getProfile'; 6 + import searchActors from './actor/searchActors'; 5 7 6 8 // mount this on /xrpc 7 9 const app = new Hono<EnvWithCtx>(); 8 10 9 - app.get("fm.teal.alpha.getPlay", async (c) => c.json(await getPlay(c))); 10 - app.get("fm.teal.alpha.feed.getActorFeed", async (c) => 11 + app.get('fm.teal.alpha.feed.getPlay', async (c) => c.json(await getPlay(c))); 12 + app.get('fm.teal.alpha.feed.getActorFeed', async (c) => 11 13 c.json(await getActorFeed(c)), 14 + ); 15 + 16 + app.get('fm.teal.alpha.actor.getProfile', async (c) => 17 + c.json(await getProfile(c)), 18 + ); 19 + 20 + app.get('fm.teal.alpha.actor.searchActors', async (c) => 21 + c.json(await searchActors(c)), 12 22 ); 13 23 14 24 export const getXrpcRouter = () => {
+46
packages/db/.drizzle/0000_perfect_war_machine.sql
··· 1 + CREATE TABLE "artists" ( 2 + "mbid" uuid PRIMARY KEY NOT NULL, 3 + "name" text NOT NULL, 4 + "play_count" integer DEFAULT 0 5 + ); 6 + --> statement-breakpoint 7 + CREATE TABLE "play_to_artists" ( 8 + "play_uri" text NOT NULL, 9 + "artist_mbid" uuid NOT NULL, 10 + "artist_name" text, 11 + CONSTRAINT "play_to_artists_play_uri_artist_mbid_pk" PRIMARY KEY("play_uri","artist_mbid") 12 + ); 13 + --> statement-breakpoint 14 + CREATE TABLE "plays" ( 15 + "uri" text PRIMARY KEY NOT NULL, 16 + "did" text NOT NULL, 17 + "rkey" text NOT NULL, 18 + "cid" text NOT NULL, 19 + "isrc" text, 20 + "duration" integer, 21 + "track_name" text NOT NULL, 22 + "played_time" timestamp with time zone, 23 + "processed_time" timestamp with time zone DEFAULT now(), 24 + "release_mbid" uuid, 25 + "release_name" text, 26 + "recording_mbid" uuid, 27 + "submission_client_agent" text, 28 + "music_service_base_domain" text 29 + ); 30 + --> statement-breakpoint 31 + CREATE TABLE "recordings" ( 32 + "mbid" uuid PRIMARY KEY NOT NULL, 33 + "name" text NOT NULL, 34 + "play_count" integer DEFAULT 0 35 + ); 36 + --> statement-breakpoint 37 + CREATE TABLE "releases" ( 38 + "mbid" uuid PRIMARY KEY NOT NULL, 39 + "name" text NOT NULL, 40 + "play_count" integer DEFAULT 0 41 + ); 42 + --> statement-breakpoint 43 + ALTER TABLE "play_to_artists" ADD CONSTRAINT "play_to_artists_play_uri_plays_uri_fk" FOREIGN KEY ("play_uri") REFERENCES "public"."plays"("uri") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 44 + ALTER TABLE "play_to_artists" ADD CONSTRAINT "play_to_artists_artist_mbid_artists_mbid_fk" FOREIGN KEY ("artist_mbid") REFERENCES "public"."artists"("mbid") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 45 + ALTER TABLE "plays" ADD CONSTRAINT "plays_release_mbid_releases_mbid_fk" FOREIGN KEY ("release_mbid") REFERENCES "public"."releases"("mbid") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 46 + ALTER TABLE "plays" ADD CONSTRAINT "plays_recording_mbid_recordings_mbid_fk" FOREIGN KEY ("recording_mbid") REFERENCES "public"."recordings"("mbid") ON DELETE no action ON UPDATE no action;
-17
packages/db/.drizzle/0000_same_maelstrom.sql
··· 1 - CREATE TABLE `auth_session` ( 2 - `key` text PRIMARY KEY NOT NULL, 3 - `session` text NOT NULL 4 - ); 5 - --> statement-breakpoint 6 - CREATE TABLE `auth_state` ( 7 - `key` text PRIMARY KEY NOT NULL, 8 - `state` text NOT NULL 9 - ); 10 - --> statement-breakpoint 11 - CREATE TABLE `status` ( 12 - `uri` text PRIMARY KEY NOT NULL, 13 - `authorDid` text NOT NULL, 14 - `status` text NOT NULL, 15 - `createdAt` text NOT NULL, 16 - `indexedAt` text NOT NULL 17 - );
-3
packages/db/.drizzle/0001_fresh_tana_nile.sql
··· 1 - ALTER TABLE `status` RENAME COLUMN "authorDid" TO "author_did";--> statement-breakpoint 2 - ALTER TABLE `status` RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint 3 - ALTER TABLE `status` RENAME COLUMN "indexedAt" TO "indexed_at";
+6
packages/db/.drizzle/0001_swift_maddog.sql
··· 1 + CREATE MATERIALIZED VIEW "public"."mv_artist_play_counts" AS (select "artists"."mbid", "artists"."name", count("plays"."uri") as "play_count" from "artists" left join "play_to_artists" on "artists"."mbid" = "play_to_artists"."artist_mbid" left join "plays" on "plays"."uri" = "play_to_artists"."play_uri" group by "artists"."mbid", "artists"."name");--> statement-breakpoint 2 + CREATE MATERIALIZED VIEW "public"."mv_global_play_count" AS (select count("uri") as "total_plays", count(distinct "did") as "unique_listeners" from "plays");--> statement-breakpoint 3 + CREATE MATERIALIZED VIEW "public"."mv_recording_play_counts" AS (select "recordings"."mbid", "recordings"."name", count("plays"."uri") as "play_count" from "recordings" left join "plays" on "plays"."recording_mbid" = "recordings"."mbid" group by "recordings"."mbid", "recordings"."name");--> statement-breakpoint 4 + CREATE MATERIALIZED VIEW "public"."mv_release_play_counts" AS (select "releases"."mbid", "releases"."name", count("plays"."uri") as "play_count" from "releases" left join "plays" on "plays"."release_mbid" = "releases"."mbid" group by "releases"."mbid", "releases"."name");--> statement-breakpoint 5 + CREATE MATERIALIZED VIEW "public"."mv_top_artists_30days" AS (select "artists"."mbid", "artists"."name", count("plays"."uri") as "play_count" from "artists" inner join "play_to_artists" on "artists"."mbid" = "play_to_artists"."artist_mbid" inner join "plays" on "plays"."uri" = "play_to_artists"."play_uri" where "plays"."played_time" >= NOW() - INTERVAL '30 days' group by "artists"."mbid", "artists"."name" order by count("plays"."uri") DESC);--> statement-breakpoint 6 + CREATE MATERIALIZED VIEW "public"."mv_top_releases_30days" AS (select "releases"."mbid", "releases"."name", count("plays"."uri") as "play_count" from "releases" inner join "plays" on "plays"."release_mbid" = "releases"."mbid" where "plays"."played_time" >= NOW() - INTERVAL '30 days' group by "releases"."mbid", "releases"."name" order by count("plays"."uri") DESC);
-1
packages/db/.drizzle/0002_moaning_roulette.sql
··· 1 - ALTER TABLE `auth_session` RENAME TO `atp_session`;
+1
packages/db/.drizzle/0002_stale_the_spike.sql
··· 1 + ALTER TABLE "plays" ADD COLUMN "origin_url" text;
-12
packages/db/.drizzle/0003_sharp_medusa.sql
··· 1 - CREATE TABLE `teal_session` ( 2 - `key` text PRIMARY KEY NOT NULL, 3 - `session` text NOT NULL, 4 - `provider` text NOT NULL 5 - ); 6 - --> statement-breakpoint 7 - CREATE TABLE `teal_user` ( 8 - `did` text PRIMARY KEY NOT NULL, 9 - `handle` text NOT NULL, 10 - `email` text NOT NULL, 11 - `created_at` text NOT NULL 12 - );
+15
packages/db/.drizzle/0003_worried_unicorn.sql
··· 1 + CREATE TABLE "profiles" ( 2 + "did" text PRIMARY KEY NOT NULL, 3 + "display_name" text NOT NULL, 4 + "description" text NOT NULL, 5 + "description_facets" jsonb NOT NULL, 6 + "avatar" text NOT NULL, 7 + "banner" text NOT NULL, 8 + "created_at" timestamp NOT NULL 9 + ); 10 + --> statement-breakpoint 11 + CREATE TABLE "featured_items" ( 12 + "did" text PRIMARY KEY NOT NULL, 13 + "mbid" text NOT NULL, 14 + "type" text NOT NULL 15 + );
-29
packages/db/.drizzle/0004_exotic_ironclad.sql
··· 1 - CREATE TABLE `follow` ( 2 - `follower` text PRIMARY KEY NOT NULL, 3 - `followed` text NOT NULL, 4 - `created_at` text NOT NULL 5 - ); 6 - --> statement-breakpoint 7 - CREATE TABLE `play` ( 8 - `uri` text PRIMARY KEY NOT NULL, 9 - `author_did` text NOT NULL, 10 - `created_at` text NOT NULL, 11 - `indexed_at` text NOT NULL, 12 - `track_name` text NOT NULL, 13 - `track_mb_id` text, 14 - `recording_mb_id` text, 15 - `duration` integer, 16 - `artist_name` text NOT NULL, 17 - `artist_mb_ids` text, 18 - `release_name` text, 19 - `release_mb_id` text, 20 - `isrc` text, 21 - `origin_url` text, 22 - `music_service_base_domain` text, 23 - `submission_client_agent` text, 24 - `played_time` text 25 - ); 26 - --> statement-breakpoint 27 - ALTER TABLE `teal_user` ADD `avatar` text NOT NULL;--> statement-breakpoint 28 - ALTER TABLE `teal_user` ADD `bio` text;--> statement-breakpoint 29 - ALTER TABLE `teal_user` DROP COLUMN `email`;
+7
packages/db/.drizzle/0004_furry_gravity.sql
··· 1 + ALTER TABLE "profiles" ALTER COLUMN "display_name" DROP NOT NULL;--> statement-breakpoint 2 + ALTER TABLE "profiles" ALTER COLUMN "description" DROP NOT NULL;--> statement-breakpoint 3 + ALTER TABLE "profiles" ALTER COLUMN "description_facets" DROP NOT NULL;--> statement-breakpoint 4 + ALTER TABLE "profiles" ALTER COLUMN "avatar" DROP NOT NULL;--> statement-breakpoint 5 + ALTER TABLE "profiles" ALTER COLUMN "banner" DROP NOT NULL;--> statement-breakpoint 6 + ALTER TABLE "profiles" ALTER COLUMN "created_at" DROP NOT NULL;--> statement-breakpoint 7 + ALTER TABLE "profiles" ADD COLUMN "handle" text;
-12
packages/db/.drizzle/0005_conscious_johnny_blaze.sql
··· 1 - PRAGMA foreign_keys=OFF;--> statement-breakpoint 2 - CREATE TABLE `__new_follow` ( 3 - `rel_id` text PRIMARY KEY NOT NULL, 4 - `follower` text NOT NULL, 5 - `followed` text NOT NULL, 6 - `created_at` text NOT NULL 7 - ); 8 - --> statement-breakpoint 9 - INSERT INTO `__new_follow`("rel_id", "follower", "followed", "created_at") SELECT '0', "follower", "followed", "created_at" FROM `follow`;--> statement-breakpoint 10 - DROP TABLE `follow`;--> statement-breakpoint 11 - ALTER TABLE `__new_follow` RENAME TO `follow`;--> statement-breakpoint 12 - PRAGMA foreign_keys=ON;
+1
packages/db/.drizzle/0005_plain_vulture.sql
··· 1 + ALTER TABLE "profiles" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;
-25
packages/db/.drizzle/0006_supreme_hairball.sql
··· 1 - PRAGMA foreign_keys=OFF;--> statement-breakpoint 2 - CREATE TABLE `__new_play` ( 3 - `uri` text PRIMARY KEY NOT NULL, 4 - `author_did` text NOT NULL, 5 - `created_at` text NOT NULL, 6 - `indexed_at` text NOT NULL, 7 - `track_name` text NOT NULL, 8 - `track_mb_id` text, 9 - `recording_mb_id` text, 10 - `duration` integer, 11 - `artist_names` text, 12 - `artist_mb_ids` text, 13 - `release_name` text, 14 - `release_mb_id` text, 15 - `isrc` text, 16 - `origin_url` text, 17 - `music_service_base_domain` text, 18 - `submission_client_agent` text, 19 - `played_time` text 20 - ); 21 - --> statement-breakpoint 22 - INSERT INTO `__new_play`("uri", "author_did", "created_at", "indexed_at", "track_name", "track_mb_id", "recording_mb_id", "duration", "artist_names", "artist_mb_ids", "release_name", "release_mb_id", "isrc", "origin_url", "music_service_base_domain", "submission_client_agent", "played_time") SELECT "uri", "author_did", "created_at", "indexed_at", "track_name", "track_mb_id", "recording_mb_id", "duration", "artist_name", "artist_mb_ids", "release_name", "release_mb_id", "isrc", "origin_url", "music_service_base_domain", "submission_client_agent", "played_time" FROM `play`;--> statement-breakpoint 23 - DROP TABLE `play`;--> statement-breakpoint 24 - ALTER TABLE `__new_play` RENAME TO `play`;--> statement-breakpoint 25 - PRAGMA foreign_keys=ON;
-1
packages/db/.drizzle/0007_demonic_toad.sql
··· 1 - ALTER TABLE `play` RENAME COLUMN "uri" TO "rkey";
+251 -56
packages/db/.drizzle/meta/0000_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "417cce08-b23b-4a9f-ad7e-e96215e0fb38", 2 + "id": "8724646f-1dc1-484d-97ad-641f3a4c2aa1", 5 3 "prevId": "00000000-0000-0000-0000-000000000000", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "auth_session": { 8 - "name": "auth_session", 7 + "public.artists": { 8 + "name": "artists", 9 + "schema": "", 9 10 "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 11 + "mbid": { 12 + "name": "mbid", 13 + "type": "uuid", 13 14 "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 15 + "notNull": true 16 16 }, 17 - "session": { 18 - "name": "session", 17 + "name": { 18 + "name": "name", 19 19 "type": "text", 20 20 "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 21 + "notNull": true 22 + }, 23 + "play_count": { 24 + "name": "play_count", 25 + "type": "integer", 26 + "primaryKey": false, 27 + "notNull": false, 28 + "default": 0 23 29 } 24 30 }, 25 31 "indexes": {}, 26 32 "foreignKeys": {}, 27 33 "compositePrimaryKeys": {}, 28 34 "uniqueConstraints": {}, 29 - "checkConstraints": {} 35 + "policies": {}, 36 + "checkConstraints": {}, 37 + "isRLSEnabled": false 30 38 }, 31 - "auth_state": { 32 - "name": "auth_state", 39 + "public.play_to_artists": { 40 + "name": "play_to_artists", 41 + "schema": "", 33 42 "columns": { 34 - "key": { 35 - "name": "key", 43 + "play_uri": { 44 + "name": "play_uri", 36 45 "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 46 + "primaryKey": false, 47 + "notNull": true 40 48 }, 41 - "state": { 42 - "name": "state", 49 + "artist_mbid": { 50 + "name": "artist_mbid", 51 + "type": "uuid", 52 + "primaryKey": false, 53 + "notNull": true 54 + }, 55 + "artist_name": { 56 + "name": "artist_name", 43 57 "type": "text", 44 58 "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 59 + "notNull": false 47 60 } 48 61 }, 49 62 "indexes": {}, 50 - "foreignKeys": {}, 51 - "compositePrimaryKeys": {}, 63 + "foreignKeys": { 64 + "play_to_artists_play_uri_plays_uri_fk": { 65 + "name": "play_to_artists_play_uri_plays_uri_fk", 66 + "tableFrom": "play_to_artists", 67 + "tableTo": "plays", 68 + "columnsFrom": [ 69 + "play_uri" 70 + ], 71 + "columnsTo": [ 72 + "uri" 73 + ], 74 + "onDelete": "no action", 75 + "onUpdate": "no action" 76 + }, 77 + "play_to_artists_artist_mbid_artists_mbid_fk": { 78 + "name": "play_to_artists_artist_mbid_artists_mbid_fk", 79 + "tableFrom": "play_to_artists", 80 + "tableTo": "artists", 81 + "columnsFrom": [ 82 + "artist_mbid" 83 + ], 84 + "columnsTo": [ 85 + "mbid" 86 + ], 87 + "onDelete": "no action", 88 + "onUpdate": "no action" 89 + } 90 + }, 91 + "compositePrimaryKeys": { 92 + "play_to_artists_play_uri_artist_mbid_pk": { 93 + "name": "play_to_artists_play_uri_artist_mbid_pk", 94 + "columns": [ 95 + "play_uri", 96 + "artist_mbid" 97 + ] 98 + } 99 + }, 52 100 "uniqueConstraints": {}, 53 - "checkConstraints": {} 101 + "policies": {}, 102 + "checkConstraints": {}, 103 + "isRLSEnabled": false 54 104 }, 55 - "status": { 56 - "name": "status", 105 + "public.plays": { 106 + "name": "plays", 107 + "schema": "", 57 108 "columns": { 58 109 "uri": { 59 110 "name": "uri", 60 111 "type": "text", 61 112 "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 113 + "notNull": true 114 + }, 115 + "did": { 116 + "name": "did", 117 + "type": "text", 118 + "primaryKey": false, 119 + "notNull": true 120 + }, 121 + "rkey": { 122 + "name": "rkey", 123 + "type": "text", 124 + "primaryKey": false, 125 + "notNull": true 126 + }, 127 + "cid": { 128 + "name": "cid", 129 + "type": "text", 130 + "primaryKey": false, 131 + "notNull": true 132 + }, 133 + "isrc": { 134 + "name": "isrc", 135 + "type": "text", 136 + "primaryKey": false, 137 + "notNull": false 138 + }, 139 + "duration": { 140 + "name": "duration", 141 + "type": "integer", 142 + "primaryKey": false, 143 + "notNull": false 144 + }, 145 + "track_name": { 146 + "name": "track_name", 147 + "type": "text", 148 + "primaryKey": false, 149 + "notNull": true 150 + }, 151 + "played_time": { 152 + "name": "played_time", 153 + "type": "timestamp with time zone", 154 + "primaryKey": false, 155 + "notNull": false 156 + }, 157 + "processed_time": { 158 + "name": "processed_time", 159 + "type": "timestamp with time zone", 160 + "primaryKey": false, 161 + "notNull": false, 162 + "default": "now()" 163 + }, 164 + "release_mbid": { 165 + "name": "release_mbid", 166 + "type": "uuid", 167 + "primaryKey": false, 168 + "notNull": false 169 + }, 170 + "release_name": { 171 + "name": "release_name", 172 + "type": "text", 173 + "primaryKey": false, 174 + "notNull": false 64 175 }, 65 - "authorDid": { 66 - "name": "authorDid", 176 + "recording_mbid": { 177 + "name": "recording_mbid", 178 + "type": "uuid", 179 + "primaryKey": false, 180 + "notNull": false 181 + }, 182 + "submission_client_agent": { 183 + "name": "submission_client_agent", 67 184 "type": "text", 68 185 "primaryKey": false, 69 - "notNull": true, 70 - "autoincrement": false 186 + "notNull": false 71 187 }, 72 - "status": { 73 - "name": "status", 188 + "music_service_base_domain": { 189 + "name": "music_service_base_domain", 74 190 "type": "text", 75 191 "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 192 + "notNull": false 193 + } 194 + }, 195 + "indexes": {}, 196 + "foreignKeys": { 197 + "plays_release_mbid_releases_mbid_fk": { 198 + "name": "plays_release_mbid_releases_mbid_fk", 199 + "tableFrom": "plays", 200 + "tableTo": "releases", 201 + "columnsFrom": [ 202 + "release_mbid" 203 + ], 204 + "columnsTo": [ 205 + "mbid" 206 + ], 207 + "onDelete": "no action", 208 + "onUpdate": "no action" 209 + }, 210 + "plays_recording_mbid_recordings_mbid_fk": { 211 + "name": "plays_recording_mbid_recordings_mbid_fk", 212 + "tableFrom": "plays", 213 + "tableTo": "recordings", 214 + "columnsFrom": [ 215 + "recording_mbid" 216 + ], 217 + "columnsTo": [ 218 + "mbid" 219 + ], 220 + "onDelete": "no action", 221 + "onUpdate": "no action" 222 + } 223 + }, 224 + "compositePrimaryKeys": {}, 225 + "uniqueConstraints": {}, 226 + "policies": {}, 227 + "checkConstraints": {}, 228 + "isRLSEnabled": false 229 + }, 230 + "public.recordings": { 231 + "name": "recordings", 232 + "schema": "", 233 + "columns": { 234 + "mbid": { 235 + "name": "mbid", 236 + "type": "uuid", 237 + "primaryKey": true, 238 + "notNull": true 78 239 }, 79 - "createdAt": { 80 - "name": "createdAt", 240 + "name": { 241 + "name": "name", 81 242 "type": "text", 82 243 "primaryKey": false, 83 - "notNull": true, 84 - "autoincrement": false 244 + "notNull": true 85 245 }, 86 - "indexedAt": { 87 - "name": "indexedAt", 246 + "play_count": { 247 + "name": "play_count", 248 + "type": "integer", 249 + "primaryKey": false, 250 + "notNull": false, 251 + "default": 0 252 + } 253 + }, 254 + "indexes": {}, 255 + "foreignKeys": {}, 256 + "compositePrimaryKeys": {}, 257 + "uniqueConstraints": {}, 258 + "policies": {}, 259 + "checkConstraints": {}, 260 + "isRLSEnabled": false 261 + }, 262 + "public.releases": { 263 + "name": "releases", 264 + "schema": "", 265 + "columns": { 266 + "mbid": { 267 + "name": "mbid", 268 + "type": "uuid", 269 + "primaryKey": true, 270 + "notNull": true 271 + }, 272 + "name": { 273 + "name": "name", 88 274 "type": "text", 89 275 "primaryKey": false, 90 - "notNull": true, 91 - "autoincrement": false 276 + "notNull": true 277 + }, 278 + "play_count": { 279 + "name": "play_count", 280 + "type": "integer", 281 + "primaryKey": false, 282 + "notNull": false, 283 + "default": 0 92 284 } 93 285 }, 94 286 "indexes": {}, 95 287 "foreignKeys": {}, 96 288 "compositePrimaryKeys": {}, 97 289 "uniqueConstraints": {}, 98 - "checkConstraints": {} 290 + "policies": {}, 291 + "checkConstraints": {}, 292 + "isRLSEnabled": false 99 293 } 100 294 }, 295 + "enums": {}, 296 + "schemas": {}, 297 + "sequences": {}, 298 + "roles": {}, 299 + "policies": {}, 101 300 "views": {}, 102 - "enums": {}, 103 301 "_meta": { 302 + "columns": {}, 104 303 "schemas": {}, 105 - "tables": {}, 106 - "columns": {} 107 - }, 108 - "internal": { 109 - "indexes": {} 304 + "tables": {} 110 305 } 111 306 }
+366 -61
packages/db/.drizzle/meta/0001_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "0668ebf0-fa9b-41bc-90fa-f25992bf4b76", 5 - "prevId": "417cce08-b23b-4a9f-ad7e-e96215e0fb38", 2 + "id": "1a16d013-9247-4174-beed-2db2c4b372a9", 3 + "prevId": "8724646f-1dc1-484d-97ad-641f3a4c2aa1", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "auth_session": { 8 - "name": "auth_session", 7 + "public.artists": { 8 + "name": "artists", 9 + "schema": "", 9 10 "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 11 + "mbid": { 12 + "name": "mbid", 13 + "type": "uuid", 13 14 "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 15 + "notNull": true 16 16 }, 17 - "session": { 18 - "name": "session", 17 + "name": { 18 + "name": "name", 19 19 "type": "text", 20 20 "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 21 + "notNull": true 22 + }, 23 + "play_count": { 24 + "name": "play_count", 25 + "type": "integer", 26 + "primaryKey": false, 27 + "notNull": false, 28 + "default": 0 23 29 } 24 30 }, 25 31 "indexes": {}, 26 32 "foreignKeys": {}, 27 33 "compositePrimaryKeys": {}, 28 34 "uniqueConstraints": {}, 29 - "checkConstraints": {} 35 + "policies": {}, 36 + "checkConstraints": {}, 37 + "isRLSEnabled": false 30 38 }, 31 - "auth_state": { 32 - "name": "auth_state", 39 + "public.play_to_artists": { 40 + "name": "play_to_artists", 41 + "schema": "", 33 42 "columns": { 34 - "key": { 35 - "name": "key", 43 + "play_uri": { 44 + "name": "play_uri", 36 45 "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 46 + "primaryKey": false, 47 + "notNull": true 48 + }, 49 + "artist_mbid": { 50 + "name": "artist_mbid", 51 + "type": "uuid", 52 + "primaryKey": false, 53 + "notNull": true 40 54 }, 41 - "state": { 42 - "name": "state", 55 + "artist_name": { 56 + "name": "artist_name", 43 57 "type": "text", 44 58 "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 59 + "notNull": false 47 60 } 48 61 }, 49 62 "indexes": {}, 50 - "foreignKeys": {}, 51 - "compositePrimaryKeys": {}, 63 + "foreignKeys": { 64 + "play_to_artists_play_uri_plays_uri_fk": { 65 + "name": "play_to_artists_play_uri_plays_uri_fk", 66 + "tableFrom": "play_to_artists", 67 + "tableTo": "plays", 68 + "columnsFrom": [ 69 + "play_uri" 70 + ], 71 + "columnsTo": [ 72 + "uri" 73 + ], 74 + "onDelete": "no action", 75 + "onUpdate": "no action" 76 + }, 77 + "play_to_artists_artist_mbid_artists_mbid_fk": { 78 + "name": "play_to_artists_artist_mbid_artists_mbid_fk", 79 + "tableFrom": "play_to_artists", 80 + "tableTo": "artists", 81 + "columnsFrom": [ 82 + "artist_mbid" 83 + ], 84 + "columnsTo": [ 85 + "mbid" 86 + ], 87 + "onDelete": "no action", 88 + "onUpdate": "no action" 89 + } 90 + }, 91 + "compositePrimaryKeys": { 92 + "play_to_artists_play_uri_artist_mbid_pk": { 93 + "name": "play_to_artists_play_uri_artist_mbid_pk", 94 + "columns": [ 95 + "play_uri", 96 + "artist_mbid" 97 + ] 98 + } 99 + }, 52 100 "uniqueConstraints": {}, 53 - "checkConstraints": {} 101 + "policies": {}, 102 + "checkConstraints": {}, 103 + "isRLSEnabled": false 54 104 }, 55 - "status": { 56 - "name": "status", 105 + "public.plays": { 106 + "name": "plays", 107 + "schema": "", 57 108 "columns": { 58 109 "uri": { 59 110 "name": "uri", 60 111 "type": "text", 61 112 "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 113 + "notNull": true 114 + }, 115 + "did": { 116 + "name": "did", 117 + "type": "text", 118 + "primaryKey": false, 119 + "notNull": true 120 + }, 121 + "rkey": { 122 + "name": "rkey", 123 + "type": "text", 124 + "primaryKey": false, 125 + "notNull": true 126 + }, 127 + "cid": { 128 + "name": "cid", 129 + "type": "text", 130 + "primaryKey": false, 131 + "notNull": true 132 + }, 133 + "isrc": { 134 + "name": "isrc", 135 + "type": "text", 136 + "primaryKey": false, 137 + "notNull": false 138 + }, 139 + "duration": { 140 + "name": "duration", 141 + "type": "integer", 142 + "primaryKey": false, 143 + "notNull": false 144 + }, 145 + "track_name": { 146 + "name": "track_name", 147 + "type": "text", 148 + "primaryKey": false, 149 + "notNull": true 150 + }, 151 + "played_time": { 152 + "name": "played_time", 153 + "type": "timestamp with time zone", 154 + "primaryKey": false, 155 + "notNull": false 156 + }, 157 + "processed_time": { 158 + "name": "processed_time", 159 + "type": "timestamp with time zone", 160 + "primaryKey": false, 161 + "notNull": false, 162 + "default": "now()" 163 + }, 164 + "release_mbid": { 165 + "name": "release_mbid", 166 + "type": "uuid", 167 + "primaryKey": false, 168 + "notNull": false 169 + }, 170 + "release_name": { 171 + "name": "release_name", 172 + "type": "text", 173 + "primaryKey": false, 174 + "notNull": false 175 + }, 176 + "recording_mbid": { 177 + "name": "recording_mbid", 178 + "type": "uuid", 179 + "primaryKey": false, 180 + "notNull": false 64 181 }, 65 - "author_did": { 66 - "name": "author_did", 182 + "submission_client_agent": { 183 + "name": "submission_client_agent", 67 184 "type": "text", 68 185 "primaryKey": false, 69 - "notNull": true, 70 - "autoincrement": false 186 + "notNull": false 71 187 }, 72 - "status": { 73 - "name": "status", 188 + "music_service_base_domain": { 189 + "name": "music_service_base_domain", 74 190 "type": "text", 75 191 "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 192 + "notNull": false 193 + } 194 + }, 195 + "indexes": {}, 196 + "foreignKeys": { 197 + "plays_release_mbid_releases_mbid_fk": { 198 + "name": "plays_release_mbid_releases_mbid_fk", 199 + "tableFrom": "plays", 200 + "tableTo": "releases", 201 + "columnsFrom": [ 202 + "release_mbid" 203 + ], 204 + "columnsTo": [ 205 + "mbid" 206 + ], 207 + "onDelete": "no action", 208 + "onUpdate": "no action" 209 + }, 210 + "plays_recording_mbid_recordings_mbid_fk": { 211 + "name": "plays_recording_mbid_recordings_mbid_fk", 212 + "tableFrom": "plays", 213 + "tableTo": "recordings", 214 + "columnsFrom": [ 215 + "recording_mbid" 216 + ], 217 + "columnsTo": [ 218 + "mbid" 219 + ], 220 + "onDelete": "no action", 221 + "onUpdate": "no action" 222 + } 223 + }, 224 + "compositePrimaryKeys": {}, 225 + "uniqueConstraints": {}, 226 + "policies": {}, 227 + "checkConstraints": {}, 228 + "isRLSEnabled": false 229 + }, 230 + "public.recordings": { 231 + "name": "recordings", 232 + "schema": "", 233 + "columns": { 234 + "mbid": { 235 + "name": "mbid", 236 + "type": "uuid", 237 + "primaryKey": true, 238 + "notNull": true 78 239 }, 79 - "created_at": { 80 - "name": "created_at", 240 + "name": { 241 + "name": "name", 81 242 "type": "text", 82 243 "primaryKey": false, 83 - "notNull": true, 84 - "autoincrement": false 244 + "notNull": true 245 + }, 246 + "play_count": { 247 + "name": "play_count", 248 + "type": "integer", 249 + "primaryKey": false, 250 + "notNull": false, 251 + "default": 0 252 + } 253 + }, 254 + "indexes": {}, 255 + "foreignKeys": {}, 256 + "compositePrimaryKeys": {}, 257 + "uniqueConstraints": {}, 258 + "policies": {}, 259 + "checkConstraints": {}, 260 + "isRLSEnabled": false 261 + }, 262 + "public.releases": { 263 + "name": "releases", 264 + "schema": "", 265 + "columns": { 266 + "mbid": { 267 + "name": "mbid", 268 + "type": "uuid", 269 + "primaryKey": true, 270 + "notNull": true 85 271 }, 86 - "indexed_at": { 87 - "name": "indexed_at", 272 + "name": { 273 + "name": "name", 88 274 "type": "text", 89 275 "primaryKey": false, 90 - "notNull": true, 91 - "autoincrement": false 276 + "notNull": true 277 + }, 278 + "play_count": { 279 + "name": "play_count", 280 + "type": "integer", 281 + "primaryKey": false, 282 + "notNull": false, 283 + "default": 0 92 284 } 93 285 }, 94 286 "indexes": {}, 95 287 "foreignKeys": {}, 96 288 "compositePrimaryKeys": {}, 97 289 "uniqueConstraints": {}, 98 - "checkConstraints": {} 290 + "policies": {}, 291 + "checkConstraints": {}, 292 + "isRLSEnabled": false 99 293 } 100 294 }, 101 - "views": {}, 102 295 "enums": {}, 103 - "_meta": { 104 - "schemas": {}, 105 - "tables": {}, 106 - "columns": { 107 - "\"status\".\"authorDid\"": "\"status\".\"author_did\"", 108 - "\"status\".\"createdAt\"": "\"status\".\"created_at\"", 109 - "\"status\".\"indexedAt\"": "\"status\".\"indexed_at\"" 296 + "schemas": {}, 297 + "sequences": {}, 298 + "roles": {}, 299 + "policies": {}, 300 + "views": { 301 + "public.mv_artist_play_counts": { 302 + "columns": { 303 + "mbid": { 304 + "name": "mbid", 305 + "type": "uuid", 306 + "primaryKey": true, 307 + "notNull": true 308 + }, 309 + "name": { 310 + "name": "name", 311 + "type": "text", 312 + "primaryKey": false, 313 + "notNull": true 314 + } 315 + }, 316 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" left join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" left join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" group by \"artists\".\"mbid\", \"artists\".\"name\"", 317 + "name": "mv_artist_play_counts", 318 + "schema": "public", 319 + "isExisting": false, 320 + "materialized": true 321 + }, 322 + "public.mv_global_play_count": { 323 + "columns": {}, 324 + "definition": "select count(\"uri\") as \"total_plays\", count(distinct \"did\") as \"unique_listeners\" from \"plays\"", 325 + "name": "mv_global_play_count", 326 + "schema": "public", 327 + "isExisting": false, 328 + "materialized": true 329 + }, 330 + "public.mv_recording_play_counts": { 331 + "columns": { 332 + "mbid": { 333 + "name": "mbid", 334 + "type": "uuid", 335 + "primaryKey": true, 336 + "notNull": true 337 + }, 338 + "name": { 339 + "name": "name", 340 + "type": "text", 341 + "primaryKey": false, 342 + "notNull": true 343 + } 344 + }, 345 + "definition": "select \"recordings\".\"mbid\", \"recordings\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"recordings\" left join \"plays\" on \"plays\".\"recording_mbid\" = \"recordings\".\"mbid\" group by \"recordings\".\"mbid\", \"recordings\".\"name\"", 346 + "name": "mv_recording_play_counts", 347 + "schema": "public", 348 + "isExisting": false, 349 + "materialized": true 350 + }, 351 + "public.mv_release_play_counts": { 352 + "columns": { 353 + "mbid": { 354 + "name": "mbid", 355 + "type": "uuid", 356 + "primaryKey": true, 357 + "notNull": true 358 + }, 359 + "name": { 360 + "name": "name", 361 + "type": "text", 362 + "primaryKey": false, 363 + "notNull": true 364 + } 365 + }, 366 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" left join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" group by \"releases\".\"mbid\", \"releases\".\"name\"", 367 + "name": "mv_release_play_counts", 368 + "schema": "public", 369 + "isExisting": false, 370 + "materialized": true 371 + }, 372 + "public.mv_top_artists_30days": { 373 + "columns": { 374 + "mbid": { 375 + "name": "mbid", 376 + "type": "uuid", 377 + "primaryKey": true, 378 + "notNull": true 379 + }, 380 + "name": { 381 + "name": "name", 382 + "type": "text", 383 + "primaryKey": false, 384 + "notNull": true 385 + } 386 + }, 387 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC", 388 + "name": "mv_top_artists_30days", 389 + "schema": "public", 390 + "isExisting": false, 391 + "materialized": true 392 + }, 393 + "public.mv_top_releases_30days": { 394 + "columns": { 395 + "mbid": { 396 + "name": "mbid", 397 + "type": "uuid", 398 + "primaryKey": true, 399 + "notNull": true 400 + }, 401 + "name": { 402 + "name": "name", 403 + "type": "text", 404 + "primaryKey": false, 405 + "notNull": true 406 + } 407 + }, 408 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC", 409 + "name": "mv_top_releases_30days", 410 + "schema": "public", 411 + "isExisting": false, 412 + "materialized": true 110 413 } 111 414 }, 112 - "internal": { 113 - "indexes": {} 415 + "_meta": { 416 + "columns": {}, 417 + "schemas": {}, 418 + "tables": {} 114 419 } 115 420 }
+374 -61
packages/db/.drizzle/meta/0002_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "1123b2f8-11c2-4d28-925a-f7a30bb3bec6", 5 - "prevId": "0668ebf0-fa9b-41bc-90fa-f25992bf4b76", 2 + "id": "3f2b9825-e988-4335-8fb8-f12d54efc11f", 3 + "prevId": "1a16d013-9247-4174-beed-2db2c4b372a9", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "atp_session": { 8 - "name": "atp_session", 7 + "public.artists": { 8 + "name": "artists", 9 + "schema": "", 9 10 "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 11 + "mbid": { 12 + "name": "mbid", 13 + "type": "uuid", 13 14 "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 15 + "notNull": true 16 16 }, 17 - "session": { 18 - "name": "session", 17 + "name": { 18 + "name": "name", 19 19 "type": "text", 20 20 "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 21 + "notNull": true 22 + }, 23 + "play_count": { 24 + "name": "play_count", 25 + "type": "integer", 26 + "primaryKey": false, 27 + "notNull": false, 28 + "default": 0 23 29 } 24 30 }, 25 31 "indexes": {}, 26 32 "foreignKeys": {}, 27 33 "compositePrimaryKeys": {}, 28 34 "uniqueConstraints": {}, 29 - "checkConstraints": {} 35 + "policies": {}, 36 + "checkConstraints": {}, 37 + "isRLSEnabled": false 30 38 }, 31 - "auth_state": { 32 - "name": "auth_state", 39 + "public.play_to_artists": { 40 + "name": "play_to_artists", 41 + "schema": "", 33 42 "columns": { 34 - "key": { 35 - "name": "key", 43 + "artist_mbid": { 44 + "name": "artist_mbid", 45 + "type": "uuid", 46 + "primaryKey": false, 47 + "notNull": true 48 + }, 49 + "artist_name": { 50 + "name": "artist_name", 36 51 "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 52 + "primaryKey": false, 53 + "notNull": false 40 54 }, 41 - "state": { 42 - "name": "state", 55 + "play_uri": { 56 + "name": "play_uri", 43 57 "type": "text", 44 58 "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 59 + "notNull": true 47 60 } 48 61 }, 49 62 "indexes": {}, 50 - "foreignKeys": {}, 51 - "compositePrimaryKeys": {}, 63 + "foreignKeys": { 64 + "play_to_artists_artist_mbid_artists_mbid_fk": { 65 + "name": "play_to_artists_artist_mbid_artists_mbid_fk", 66 + "tableFrom": "play_to_artists", 67 + "tableTo": "artists", 68 + "columnsFrom": [ 69 + "artist_mbid" 70 + ], 71 + "columnsTo": [ 72 + "mbid" 73 + ], 74 + "onDelete": "no action", 75 + "onUpdate": "no action" 76 + }, 77 + "play_to_artists_play_uri_plays_uri_fk": { 78 + "name": "play_to_artists_play_uri_plays_uri_fk", 79 + "tableFrom": "play_to_artists", 80 + "tableTo": "plays", 81 + "columnsFrom": [ 82 + "play_uri" 83 + ], 84 + "columnsTo": [ 85 + "uri" 86 + ], 87 + "onDelete": "no action", 88 + "onUpdate": "no action" 89 + } 90 + }, 91 + "compositePrimaryKeys": { 92 + "play_to_artists_play_uri_artist_mbid_pk": { 93 + "name": "play_to_artists_play_uri_artist_mbid_pk", 94 + "columns": [ 95 + "play_uri", 96 + "artist_mbid" 97 + ] 98 + } 99 + }, 52 100 "uniqueConstraints": {}, 53 - "checkConstraints": {} 101 + "policies": {}, 102 + "checkConstraints": {}, 103 + "isRLSEnabled": false 54 104 }, 55 - "status": { 56 - "name": "status", 105 + "public.plays": { 106 + "name": "plays", 107 + "schema": "", 57 108 "columns": { 109 + "cid": { 110 + "name": "cid", 111 + "type": "text", 112 + "primaryKey": false, 113 + "notNull": true 114 + }, 115 + "did": { 116 + "name": "did", 117 + "type": "text", 118 + "primaryKey": false, 119 + "notNull": true 120 + }, 121 + "duration": { 122 + "name": "duration", 123 + "type": "integer", 124 + "primaryKey": false, 125 + "notNull": false 126 + }, 127 + "isrc": { 128 + "name": "isrc", 129 + "type": "text", 130 + "primaryKey": false, 131 + "notNull": false 132 + }, 133 + "music_service_base_domain": { 134 + "name": "music_service_base_domain", 135 + "type": "text", 136 + "primaryKey": false, 137 + "notNull": false 138 + }, 139 + "origin_url": { 140 + "name": "origin_url", 141 + "type": "text", 142 + "primaryKey": false, 143 + "notNull": false 144 + }, 145 + "played_time": { 146 + "name": "played_time", 147 + "type": "timestamp with time zone", 148 + "primaryKey": false, 149 + "notNull": false 150 + }, 151 + "processed_time": { 152 + "name": "processed_time", 153 + "type": "timestamp with time zone", 154 + "primaryKey": false, 155 + "notNull": false, 156 + "default": "now()" 157 + }, 158 + "rkey": { 159 + "name": "rkey", 160 + "type": "text", 161 + "primaryKey": false, 162 + "notNull": true 163 + }, 164 + "recording_mbid": { 165 + "name": "recording_mbid", 166 + "type": "uuid", 167 + "primaryKey": false, 168 + "notNull": false 169 + }, 170 + "release_mbid": { 171 + "name": "release_mbid", 172 + "type": "uuid", 173 + "primaryKey": false, 174 + "notNull": false 175 + }, 176 + "release_name": { 177 + "name": "release_name", 178 + "type": "text", 179 + "primaryKey": false, 180 + "notNull": false 181 + }, 182 + "submission_client_agent": { 183 + "name": "submission_client_agent", 184 + "type": "text", 185 + "primaryKey": false, 186 + "notNull": false 187 + }, 188 + "track_name": { 189 + "name": "track_name", 190 + "type": "text", 191 + "primaryKey": false, 192 + "notNull": true 193 + }, 58 194 "uri": { 59 195 "name": "uri", 60 196 "type": "text", 61 197 "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 198 + "notNull": true 199 + } 200 + }, 201 + "indexes": {}, 202 + "foreignKeys": { 203 + "plays_recording_mbid_recordings_mbid_fk": { 204 + "name": "plays_recording_mbid_recordings_mbid_fk", 205 + "tableFrom": "plays", 206 + "tableTo": "recordings", 207 + "columnsFrom": [ 208 + "recording_mbid" 209 + ], 210 + "columnsTo": [ 211 + "mbid" 212 + ], 213 + "onDelete": "no action", 214 + "onUpdate": "no action" 215 + }, 216 + "plays_release_mbid_releases_mbid_fk": { 217 + "name": "plays_release_mbid_releases_mbid_fk", 218 + "tableFrom": "plays", 219 + "tableTo": "releases", 220 + "columnsFrom": [ 221 + "release_mbid" 222 + ], 223 + "columnsTo": [ 224 + "mbid" 225 + ], 226 + "onDelete": "no action", 227 + "onUpdate": "no action" 228 + } 229 + }, 230 + "compositePrimaryKeys": {}, 231 + "uniqueConstraints": {}, 232 + "policies": {}, 233 + "checkConstraints": {}, 234 + "isRLSEnabled": false 235 + }, 236 + "public.recordings": { 237 + "name": "recordings", 238 + "schema": "", 239 + "columns": { 240 + "mbid": { 241 + "name": "mbid", 242 + "type": "uuid", 243 + "primaryKey": true, 244 + "notNull": true 64 245 }, 65 - "author_did": { 66 - "name": "author_did", 246 + "name": { 247 + "name": "name", 67 248 "type": "text", 68 249 "primaryKey": false, 69 - "notNull": true, 70 - "autoincrement": false 250 + "notNull": true 71 251 }, 72 - "status": { 73 - "name": "status", 74 - "type": "text", 252 + "play_count": { 253 + "name": "play_count", 254 + "type": "integer", 75 255 "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 256 + "notNull": false, 257 + "default": 0 258 + } 259 + }, 260 + "indexes": {}, 261 + "foreignKeys": {}, 262 + "compositePrimaryKeys": {}, 263 + "uniqueConstraints": {}, 264 + "policies": {}, 265 + "checkConstraints": {}, 266 + "isRLSEnabled": false 267 + }, 268 + "public.releases": { 269 + "name": "releases", 270 + "schema": "", 271 + "columns": { 272 + "mbid": { 273 + "name": "mbid", 274 + "type": "uuid", 275 + "primaryKey": true, 276 + "notNull": true 78 277 }, 79 - "created_at": { 80 - "name": "created_at", 278 + "name": { 279 + "name": "name", 81 280 "type": "text", 82 281 "primaryKey": false, 83 - "notNull": true, 84 - "autoincrement": false 282 + "notNull": true 85 283 }, 86 - "indexed_at": { 87 - "name": "indexed_at", 88 - "type": "text", 284 + "play_count": { 285 + "name": "play_count", 286 + "type": "integer", 89 287 "primaryKey": false, 90 - "notNull": true, 91 - "autoincrement": false 288 + "notNull": false, 289 + "default": 0 92 290 } 93 291 }, 94 292 "indexes": {}, 95 293 "foreignKeys": {}, 96 294 "compositePrimaryKeys": {}, 97 295 "uniqueConstraints": {}, 98 - "checkConstraints": {} 296 + "policies": {}, 297 + "checkConstraints": {}, 298 + "isRLSEnabled": false 99 299 } 100 300 }, 101 - "views": {}, 102 301 "enums": {}, 103 - "_meta": { 104 - "schemas": {}, 105 - "tables": { 106 - "\"auth_session\"": "\"atp_session\"" 302 + "schemas": {}, 303 + "sequences": {}, 304 + "roles": {}, 305 + "policies": {}, 306 + "views": { 307 + "public.mv_artist_play_counts": { 308 + "columns": { 309 + "mbid": { 310 + "name": "mbid", 311 + "type": "uuid", 312 + "primaryKey": true, 313 + "notNull": true 314 + }, 315 + "name": { 316 + "name": "name", 317 + "type": "text", 318 + "primaryKey": false, 319 + "notNull": true 320 + } 321 + }, 322 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" left join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" left join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" group by \"artists\".\"mbid\", \"artists\".\"name\"", 323 + "name": "mv_artist_play_counts", 324 + "schema": "public", 325 + "isExisting": false, 326 + "materialized": true 327 + }, 328 + "public.mv_global_play_count": { 329 + "columns": {}, 330 + "definition": "select count(\"uri\") as \"total_plays\", count(distinct \"did\") as \"unique_listeners\" from \"plays\"", 331 + "name": "mv_global_play_count", 332 + "schema": "public", 333 + "isExisting": false, 334 + "materialized": true 107 335 }, 108 - "columns": {} 336 + "public.mv_recording_play_counts": { 337 + "columns": { 338 + "mbid": { 339 + "name": "mbid", 340 + "type": "uuid", 341 + "primaryKey": true, 342 + "notNull": true 343 + }, 344 + "name": { 345 + "name": "name", 346 + "type": "text", 347 + "primaryKey": false, 348 + "notNull": true 349 + } 350 + }, 351 + "definition": "select \"recordings\".\"mbid\", \"recordings\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"recordings\" left join \"plays\" on \"plays\".\"recording_mbid\" = \"recordings\".\"mbid\" group by \"recordings\".\"mbid\", \"recordings\".\"name\"", 352 + "name": "mv_recording_play_counts", 353 + "schema": "public", 354 + "isExisting": false, 355 + "materialized": true 356 + }, 357 + "public.mv_release_play_counts": { 358 + "columns": { 359 + "mbid": { 360 + "name": "mbid", 361 + "type": "uuid", 362 + "primaryKey": true, 363 + "notNull": true 364 + }, 365 + "name": { 366 + "name": "name", 367 + "type": "text", 368 + "primaryKey": false, 369 + "notNull": true 370 + } 371 + }, 372 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" left join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" group by \"releases\".\"mbid\", \"releases\".\"name\"", 373 + "name": "mv_release_play_counts", 374 + "schema": "public", 375 + "isExisting": false, 376 + "materialized": true 377 + }, 378 + "public.mv_top_artists_30days": { 379 + "columns": { 380 + "mbid": { 381 + "name": "mbid", 382 + "type": "uuid", 383 + "primaryKey": true, 384 + "notNull": true 385 + }, 386 + "name": { 387 + "name": "name", 388 + "type": "text", 389 + "primaryKey": false, 390 + "notNull": true 391 + } 392 + }, 393 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC", 394 + "name": "mv_top_artists_30days", 395 + "schema": "public", 396 + "isExisting": false, 397 + "materialized": true 398 + }, 399 + "public.mv_top_releases_30days": { 400 + "columns": { 401 + "mbid": { 402 + "name": "mbid", 403 + "type": "uuid", 404 + "primaryKey": true, 405 + "notNull": true 406 + }, 407 + "name": { 408 + "name": "name", 409 + "type": "text", 410 + "primaryKey": false, 411 + "notNull": true 412 + } 413 + }, 414 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC", 415 + "name": "mv_top_releases_30days", 416 + "schema": "public", 417 + "isExisting": false, 418 + "materialized": true 419 + } 109 420 }, 110 - "internal": { 111 - "indexes": {} 421 + "_meta": { 422 + "columns": {}, 423 + "schemas": {}, 424 + "tables": {} 112 425 } 113 426 }
+425 -93
packages/db/.drizzle/meta/0003_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "7710000b-44fd-4d23-a768-0117f22926c3", 5 - "prevId": "1123b2f8-11c2-4d28-925a-f7a30bb3bec6", 2 + "id": "a244bd07-cde6-4d15-8384-4a94d7cf4cb9", 3 + "prevId": "3f2b9825-e988-4335-8fb8-f12d54efc11f", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "atp_session": { 8 - "name": "atp_session", 7 + "public.artists": { 8 + "name": "artists", 9 + "schema": "", 9 10 "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 11 + "mbid": { 12 + "name": "mbid", 13 + "type": "uuid", 13 14 "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 15 + "notNull": true 16 16 }, 17 - "session": { 18 - "name": "session", 17 + "name": { 18 + "name": "name", 19 19 "type": "text", 20 20 "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 21 + "notNull": true 22 + }, 23 + "play_count": { 24 + "name": "play_count", 25 + "type": "integer", 26 + "primaryKey": false, 27 + "notNull": false, 28 + "default": 0 23 29 } 24 30 }, 25 31 "indexes": {}, 26 32 "foreignKeys": {}, 27 33 "compositePrimaryKeys": {}, 28 34 "uniqueConstraints": {}, 29 - "checkConstraints": {} 35 + "policies": {}, 36 + "checkConstraints": {}, 37 + "isRLSEnabled": false 30 38 }, 31 - "auth_state": { 32 - "name": "auth_state", 39 + "public.play_to_artists": { 40 + "name": "play_to_artists", 41 + "schema": "", 33 42 "columns": { 34 - "key": { 35 - "name": "key", 43 + "artist_mbid": { 44 + "name": "artist_mbid", 45 + "type": "uuid", 46 + "primaryKey": false, 47 + "notNull": true 48 + }, 49 + "artist_name": { 50 + "name": "artist_name", 36 51 "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 52 + "primaryKey": false, 53 + "notNull": false 40 54 }, 41 - "state": { 42 - "name": "state", 55 + "play_uri": { 56 + "name": "play_uri", 43 57 "type": "text", 44 58 "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 59 + "notNull": true 47 60 } 48 61 }, 49 62 "indexes": {}, 50 - "foreignKeys": {}, 51 - "compositePrimaryKeys": {}, 63 + "foreignKeys": { 64 + "play_to_artists_artist_mbid_artists_mbid_fk": { 65 + "name": "play_to_artists_artist_mbid_artists_mbid_fk", 66 + "tableFrom": "play_to_artists", 67 + "tableTo": "artists", 68 + "columnsFrom": [ 69 + "artist_mbid" 70 + ], 71 + "columnsTo": [ 72 + "mbid" 73 + ], 74 + "onDelete": "no action", 75 + "onUpdate": "no action" 76 + }, 77 + "play_to_artists_play_uri_plays_uri_fk": { 78 + "name": "play_to_artists_play_uri_plays_uri_fk", 79 + "tableFrom": "play_to_artists", 80 + "tableTo": "plays", 81 + "columnsFrom": [ 82 + "play_uri" 83 + ], 84 + "columnsTo": [ 85 + "uri" 86 + ], 87 + "onDelete": "no action", 88 + "onUpdate": "no action" 89 + } 90 + }, 91 + "compositePrimaryKeys": { 92 + "play_to_artists_play_uri_artist_mbid_pk": { 93 + "name": "play_to_artists_play_uri_artist_mbid_pk", 94 + "columns": [ 95 + "play_uri", 96 + "artist_mbid" 97 + ] 98 + } 99 + }, 52 100 "uniqueConstraints": {}, 53 - "checkConstraints": {} 101 + "policies": {}, 102 + "checkConstraints": {}, 103 + "isRLSEnabled": false 54 104 }, 55 - "status": { 56 - "name": "status", 105 + "public.plays": { 106 + "name": "plays", 107 + "schema": "", 57 108 "columns": { 109 + "cid": { 110 + "name": "cid", 111 + "type": "text", 112 + "primaryKey": false, 113 + "notNull": true 114 + }, 115 + "did": { 116 + "name": "did", 117 + "type": "text", 118 + "primaryKey": false, 119 + "notNull": true 120 + }, 121 + "duration": { 122 + "name": "duration", 123 + "type": "integer", 124 + "primaryKey": false, 125 + "notNull": false 126 + }, 127 + "isrc": { 128 + "name": "isrc", 129 + "type": "text", 130 + "primaryKey": false, 131 + "notNull": false 132 + }, 133 + "music_service_base_domain": { 134 + "name": "music_service_base_domain", 135 + "type": "text", 136 + "primaryKey": false, 137 + "notNull": false 138 + }, 139 + "origin_url": { 140 + "name": "origin_url", 141 + "type": "text", 142 + "primaryKey": false, 143 + "notNull": false 144 + }, 145 + "played_time": { 146 + "name": "played_time", 147 + "type": "timestamp with time zone", 148 + "primaryKey": false, 149 + "notNull": false 150 + }, 151 + "processed_time": { 152 + "name": "processed_time", 153 + "type": "timestamp with time zone", 154 + "primaryKey": false, 155 + "notNull": false, 156 + "default": "now()" 157 + }, 158 + "rkey": { 159 + "name": "rkey", 160 + "type": "text", 161 + "primaryKey": false, 162 + "notNull": true 163 + }, 164 + "recording_mbid": { 165 + "name": "recording_mbid", 166 + "type": "uuid", 167 + "primaryKey": false, 168 + "notNull": false 169 + }, 170 + "release_mbid": { 171 + "name": "release_mbid", 172 + "type": "uuid", 173 + "primaryKey": false, 174 + "notNull": false 175 + }, 176 + "release_name": { 177 + "name": "release_name", 178 + "type": "text", 179 + "primaryKey": false, 180 + "notNull": false 181 + }, 182 + "submission_client_agent": { 183 + "name": "submission_client_agent", 184 + "type": "text", 185 + "primaryKey": false, 186 + "notNull": false 187 + }, 188 + "track_name": { 189 + "name": "track_name", 190 + "type": "text", 191 + "primaryKey": false, 192 + "notNull": true 193 + }, 58 194 "uri": { 59 195 "name": "uri", 60 196 "type": "text", 61 197 "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 198 + "notNull": true 199 + } 200 + }, 201 + "indexes": {}, 202 + "foreignKeys": { 203 + "plays_recording_mbid_recordings_mbid_fk": { 204 + "name": "plays_recording_mbid_recordings_mbid_fk", 205 + "tableFrom": "plays", 206 + "tableTo": "recordings", 207 + "columnsFrom": [ 208 + "recording_mbid" 209 + ], 210 + "columnsTo": [ 211 + "mbid" 212 + ], 213 + "onDelete": "no action", 214 + "onUpdate": "no action" 64 215 }, 65 - "author_did": { 66 - "name": "author_did", 216 + "plays_release_mbid_releases_mbid_fk": { 217 + "name": "plays_release_mbid_releases_mbid_fk", 218 + "tableFrom": "plays", 219 + "tableTo": "releases", 220 + "columnsFrom": [ 221 + "release_mbid" 222 + ], 223 + "columnsTo": [ 224 + "mbid" 225 + ], 226 + "onDelete": "no action", 227 + "onUpdate": "no action" 228 + } 229 + }, 230 + "compositePrimaryKeys": {}, 231 + "uniqueConstraints": {}, 232 + "policies": {}, 233 + "checkConstraints": {}, 234 + "isRLSEnabled": false 235 + }, 236 + "public.profiles": { 237 + "name": "profiles", 238 + "schema": "", 239 + "columns": { 240 + "did": { 241 + "name": "did", 242 + "type": "text", 243 + "primaryKey": true, 244 + "notNull": true 245 + }, 246 + "display_name": { 247 + "name": "display_name", 67 248 "type": "text", 68 249 "primaryKey": false, 69 - "notNull": true, 70 - "autoincrement": false 250 + "notNull": true 71 251 }, 72 - "status": { 73 - "name": "status", 252 + "description": { 253 + "name": "description", 74 254 "type": "text", 75 255 "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 256 + "notNull": true 257 + }, 258 + "description_facets": { 259 + "name": "description_facets", 260 + "type": "jsonb", 261 + "primaryKey": false, 262 + "notNull": true 78 263 }, 79 - "created_at": { 80 - "name": "created_at", 264 + "avatar": { 265 + "name": "avatar", 81 266 "type": "text", 82 267 "primaryKey": false, 83 - "notNull": true, 84 - "autoincrement": false 268 + "notNull": true 85 269 }, 86 - "indexed_at": { 87 - "name": "indexed_at", 270 + "banner": { 271 + "name": "banner", 88 272 "type": "text", 89 273 "primaryKey": false, 90 - "notNull": true, 91 - "autoincrement": false 274 + "notNull": true 275 + }, 276 + "created_at": { 277 + "name": "created_at", 278 + "type": "timestamp", 279 + "primaryKey": false, 280 + "notNull": true 92 281 } 93 282 }, 94 283 "indexes": {}, 95 284 "foreignKeys": {}, 96 285 "compositePrimaryKeys": {}, 97 286 "uniqueConstraints": {}, 98 - "checkConstraints": {} 287 + "policies": {}, 288 + "checkConstraints": {}, 289 + "isRLSEnabled": false 99 290 }, 100 - "teal_session": { 101 - "name": "teal_session", 291 + "public.recordings": { 292 + "name": "recordings", 293 + "schema": "", 102 294 "columns": { 103 - "key": { 104 - "name": "key", 105 - "type": "text", 295 + "mbid": { 296 + "name": "mbid", 297 + "type": "uuid", 106 298 "primaryKey": true, 107 - "notNull": true, 108 - "autoincrement": false 299 + "notNull": true 109 300 }, 110 - "session": { 111 - "name": "session", 301 + "name": { 302 + "name": "name", 112 303 "type": "text", 113 304 "primaryKey": false, 114 - "notNull": true, 115 - "autoincrement": false 305 + "notNull": true 116 306 }, 117 - "provider": { 118 - "name": "provider", 307 + "play_count": { 308 + "name": "play_count", 309 + "type": "integer", 310 + "primaryKey": false, 311 + "notNull": false, 312 + "default": 0 313 + } 314 + }, 315 + "indexes": {}, 316 + "foreignKeys": {}, 317 + "compositePrimaryKeys": {}, 318 + "uniqueConstraints": {}, 319 + "policies": {}, 320 + "checkConstraints": {}, 321 + "isRLSEnabled": false 322 + }, 323 + "public.releases": { 324 + "name": "releases", 325 + "schema": "", 326 + "columns": { 327 + "mbid": { 328 + "name": "mbid", 329 + "type": "uuid", 330 + "primaryKey": true, 331 + "notNull": true 332 + }, 333 + "name": { 334 + "name": "name", 119 335 "type": "text", 120 336 "primaryKey": false, 121 - "notNull": true, 122 - "autoincrement": false 337 + "notNull": true 338 + }, 339 + "play_count": { 340 + "name": "play_count", 341 + "type": "integer", 342 + "primaryKey": false, 343 + "notNull": false, 344 + "default": 0 123 345 } 124 346 }, 125 347 "indexes": {}, 126 348 "foreignKeys": {}, 127 349 "compositePrimaryKeys": {}, 128 350 "uniqueConstraints": {}, 129 - "checkConstraints": {} 351 + "policies": {}, 352 + "checkConstraints": {}, 353 + "isRLSEnabled": false 130 354 }, 131 - "teal_user": { 132 - "name": "teal_user", 355 + "public.featured_items": { 356 + "name": "featured_items", 357 + "schema": "", 133 358 "columns": { 134 359 "did": { 135 360 "name": "did", 136 361 "type": "text", 137 362 "primaryKey": true, 138 - "notNull": true, 139 - "autoincrement": false 140 - }, 141 - "handle": { 142 - "name": "handle", 143 - "type": "text", 144 - "primaryKey": false, 145 - "notNull": true, 146 - "autoincrement": false 363 + "notNull": true 147 364 }, 148 - "email": { 149 - "name": "email", 365 + "mbid": { 366 + "name": "mbid", 150 367 "type": "text", 151 368 "primaryKey": false, 152 - "notNull": true, 153 - "autoincrement": false 369 + "notNull": true 154 370 }, 155 - "created_at": { 156 - "name": "created_at", 371 + "type": { 372 + "name": "type", 157 373 "type": "text", 158 374 "primaryKey": false, 159 - "notNull": true, 160 - "autoincrement": false 375 + "notNull": true 161 376 } 162 377 }, 163 378 "indexes": {}, 164 379 "foreignKeys": {}, 165 380 "compositePrimaryKeys": {}, 166 381 "uniqueConstraints": {}, 167 - "checkConstraints": {} 382 + "policies": {}, 383 + "checkConstraints": {}, 384 + "isRLSEnabled": false 168 385 } 169 386 }, 170 - "views": {}, 171 387 "enums": {}, 388 + "schemas": {}, 389 + "sequences": {}, 390 + "roles": {}, 391 + "policies": {}, 392 + "views": { 393 + "public.mv_artist_play_counts": { 394 + "columns": { 395 + "mbid": { 396 + "name": "mbid", 397 + "type": "uuid", 398 + "primaryKey": true, 399 + "notNull": true 400 + }, 401 + "name": { 402 + "name": "name", 403 + "type": "text", 404 + "primaryKey": false, 405 + "notNull": true 406 + } 407 + }, 408 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" left join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" left join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" group by \"artists\".\"mbid\", \"artists\".\"name\"", 409 + "name": "mv_artist_play_counts", 410 + "schema": "public", 411 + "isExisting": false, 412 + "materialized": true 413 + }, 414 + "public.mv_global_play_count": { 415 + "columns": {}, 416 + "definition": "select count(\"uri\") as \"total_plays\", count(distinct \"did\") as \"unique_listeners\" from \"plays\"", 417 + "name": "mv_global_play_count", 418 + "schema": "public", 419 + "isExisting": false, 420 + "materialized": true 421 + }, 422 + "public.mv_recording_play_counts": { 423 + "columns": { 424 + "mbid": { 425 + "name": "mbid", 426 + "type": "uuid", 427 + "primaryKey": true, 428 + "notNull": true 429 + }, 430 + "name": { 431 + "name": "name", 432 + "type": "text", 433 + "primaryKey": false, 434 + "notNull": true 435 + } 436 + }, 437 + "definition": "select \"recordings\".\"mbid\", \"recordings\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"recordings\" left join \"plays\" on \"plays\".\"recording_mbid\" = \"recordings\".\"mbid\" group by \"recordings\".\"mbid\", \"recordings\".\"name\"", 438 + "name": "mv_recording_play_counts", 439 + "schema": "public", 440 + "isExisting": false, 441 + "materialized": true 442 + }, 443 + "public.mv_release_play_counts": { 444 + "columns": { 445 + "mbid": { 446 + "name": "mbid", 447 + "type": "uuid", 448 + "primaryKey": true, 449 + "notNull": true 450 + }, 451 + "name": { 452 + "name": "name", 453 + "type": "text", 454 + "primaryKey": false, 455 + "notNull": true 456 + } 457 + }, 458 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" left join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" group by \"releases\".\"mbid\", \"releases\".\"name\"", 459 + "name": "mv_release_play_counts", 460 + "schema": "public", 461 + "isExisting": false, 462 + "materialized": true 463 + }, 464 + "public.mv_top_artists_30days": { 465 + "columns": { 466 + "mbid": { 467 + "name": "mbid", 468 + "type": "uuid", 469 + "primaryKey": true, 470 + "notNull": true 471 + }, 472 + "name": { 473 + "name": "name", 474 + "type": "text", 475 + "primaryKey": false, 476 + "notNull": true 477 + } 478 + }, 479 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC", 480 + "name": "mv_top_artists_30days", 481 + "schema": "public", 482 + "isExisting": false, 483 + "materialized": true 484 + }, 485 + "public.mv_top_releases_30days": { 486 + "columns": { 487 + "mbid": { 488 + "name": "mbid", 489 + "type": "uuid", 490 + "primaryKey": true, 491 + "notNull": true 492 + }, 493 + "name": { 494 + "name": "name", 495 + "type": "text", 496 + "primaryKey": false, 497 + "notNull": true 498 + } 499 + }, 500 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC", 501 + "name": "mv_top_releases_30days", 502 + "schema": "public", 503 + "isExisting": false, 504 + "materialized": true 505 + } 506 + }, 172 507 "_meta": { 508 + "columns": {}, 173 509 "schemas": {}, 174 - "tables": {}, 175 - "columns": {} 176 - }, 177 - "internal": { 178 - "indexes": {} 510 + "tables": {} 179 511 } 180 512 }
+378 -207
packages/db/.drizzle/meta/0004_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "639ec806-61a1-448d-a922-1935bf8f6cf3", 5 - "prevId": "7710000b-44fd-4d23-a768-0117f22926c3", 2 + "id": "01c93ffb-209e-48f7-9d79-1a4b624c2b0b", 3 + "prevId": "a244bd07-cde6-4d15-8384-4a94d7cf4cb9", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "atp_session": { 8 - "name": "atp_session", 7 + "public.artists": { 8 + "name": "artists", 9 + "schema": "", 9 10 "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 11 + "mbid": { 12 + "name": "mbid", 13 + "type": "uuid", 13 14 "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 15 + "notNull": true 16 16 }, 17 - "session": { 18 - "name": "session", 17 + "name": { 18 + "name": "name", 19 19 "type": "text", 20 20 "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 21 + "notNull": true 22 + }, 23 + "play_count": { 24 + "name": "play_count", 25 + "type": "integer", 26 + "primaryKey": false, 27 + "notNull": false, 28 + "default": 0 23 29 } 24 30 }, 25 31 "indexes": {}, 26 32 "foreignKeys": {}, 27 33 "compositePrimaryKeys": {}, 28 34 "uniqueConstraints": {}, 29 - "checkConstraints": {} 35 + "policies": {}, 36 + "checkConstraints": {}, 37 + "isRLSEnabled": false 30 38 }, 31 - "auth_state": { 32 - "name": "auth_state", 39 + "public.play_to_artists": { 40 + "name": "play_to_artists", 41 + "schema": "", 33 42 "columns": { 34 - "key": { 35 - "name": "key", 43 + "artist_mbid": { 44 + "name": "artist_mbid", 45 + "type": "uuid", 46 + "primaryKey": false, 47 + "notNull": true 48 + }, 49 + "artist_name": { 50 + "name": "artist_name", 36 51 "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 52 + "primaryKey": false, 53 + "notNull": false 40 54 }, 41 - "state": { 42 - "name": "state", 55 + "play_uri": { 56 + "name": "play_uri", 43 57 "type": "text", 44 58 "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 59 + "notNull": true 47 60 } 48 61 }, 49 62 "indexes": {}, 50 - "foreignKeys": {}, 51 - "compositePrimaryKeys": {}, 52 - "uniqueConstraints": {}, 53 - "checkConstraints": {} 54 - }, 55 - "follow": { 56 - "name": "follow", 57 - "columns": { 58 - "follower": { 59 - "name": "follower", 60 - "type": "text", 61 - "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 63 + "foreignKeys": { 64 + "play_to_artists_artist_mbid_artists_mbid_fk": { 65 + "name": "play_to_artists_artist_mbid_artists_mbid_fk", 66 + "tableFrom": "play_to_artists", 67 + "tableTo": "artists", 68 + "columnsFrom": [ 69 + "artist_mbid" 70 + ], 71 + "columnsTo": [ 72 + "mbid" 73 + ], 74 + "onDelete": "no action", 75 + "onUpdate": "no action" 64 76 }, 65 - "followed": { 66 - "name": "followed", 67 - "type": "text", 68 - "primaryKey": true, 69 - "notNull": true, 70 - "autoincrement": false 71 - }, 72 - "created_at": { 73 - "name": "created_at", 74 - "type": "text", 75 - "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 77 + "play_to_artists_play_uri_plays_uri_fk": { 78 + "name": "play_to_artists_play_uri_plays_uri_fk", 79 + "tableFrom": "play_to_artists", 80 + "tableTo": "plays", 81 + "columnsFrom": [ 82 + "play_uri" 83 + ], 84 + "columnsTo": [ 85 + "uri" 86 + ], 87 + "onDelete": "no action", 88 + "onUpdate": "no action" 89 + } 90 + }, 91 + "compositePrimaryKeys": { 92 + "play_to_artists_play_uri_artist_mbid_pk": { 93 + "name": "play_to_artists_play_uri_artist_mbid_pk", 94 + "columns": [ 95 + "play_uri", 96 + "artist_mbid" 97 + ] 78 98 } 79 99 }, 80 - "indexes": {}, 81 - "foreignKeys": {}, 82 - "compositePrimaryKeys": {}, 83 100 "uniqueConstraints": {}, 84 - "checkConstraints": {} 101 + "policies": {}, 102 + "checkConstraints": {}, 103 + "isRLSEnabled": false 85 104 }, 86 - "play": { 87 - "name": "play", 105 + "public.plays": { 106 + "name": "plays", 107 + "schema": "", 88 108 "columns": { 89 - "uri": { 90 - "name": "uri", 109 + "cid": { 110 + "name": "cid", 91 111 "type": "text", 92 - "primaryKey": true, 93 - "notNull": true, 94 - "autoincrement": false 112 + "primaryKey": false, 113 + "notNull": true 95 114 }, 96 - "author_did": { 97 - "name": "author_did", 115 + "did": { 116 + "name": "did", 98 117 "type": "text", 99 118 "primaryKey": false, 100 - "notNull": true, 101 - "autoincrement": false 119 + "notNull": true 102 120 }, 103 - "created_at": { 104 - "name": "created_at", 105 - "type": "text", 121 + "duration": { 122 + "name": "duration", 123 + "type": "integer", 106 124 "primaryKey": false, 107 - "notNull": true, 108 - "autoincrement": false 125 + "notNull": false 109 126 }, 110 - "indexed_at": { 111 - "name": "indexed_at", 127 + "isrc": { 128 + "name": "isrc", 112 129 "type": "text", 113 130 "primaryKey": false, 114 - "notNull": true, 115 - "autoincrement": false 131 + "notNull": false 116 132 }, 117 - "track_name": { 118 - "name": "track_name", 133 + "music_service_base_domain": { 134 + "name": "music_service_base_domain", 119 135 "type": "text", 120 136 "primaryKey": false, 121 - "notNull": true, 122 - "autoincrement": false 137 + "notNull": false 123 138 }, 124 - "track_mb_id": { 125 - "name": "track_mb_id", 139 + "origin_url": { 140 + "name": "origin_url", 126 141 "type": "text", 127 142 "primaryKey": false, 128 - "notNull": false, 129 - "autoincrement": false 143 + "notNull": false 130 144 }, 131 - "recording_mb_id": { 132 - "name": "recording_mb_id", 133 - "type": "text", 145 + "played_time": { 146 + "name": "played_time", 147 + "type": "timestamp with time zone", 134 148 "primaryKey": false, 135 - "notNull": false, 136 - "autoincrement": false 149 + "notNull": false 137 150 }, 138 - "duration": { 139 - "name": "duration", 140 - "type": "integer", 151 + "processed_time": { 152 + "name": "processed_time", 153 + "type": "timestamp with time zone", 141 154 "primaryKey": false, 142 155 "notNull": false, 143 - "autoincrement": false 156 + "default": "now()" 144 157 }, 145 - "artist_name": { 146 - "name": "artist_name", 158 + "rkey": { 159 + "name": "rkey", 147 160 "type": "text", 148 161 "primaryKey": false, 149 - "notNull": true, 150 - "autoincrement": false 162 + "notNull": true 151 163 }, 152 - "artist_mb_ids": { 153 - "name": "artist_mb_ids", 154 - "type": "text", 164 + "recording_mbid": { 165 + "name": "recording_mbid", 166 + "type": "uuid", 155 167 "primaryKey": false, 156 - "notNull": false, 157 - "autoincrement": false 168 + "notNull": false 169 + }, 170 + "release_mbid": { 171 + "name": "release_mbid", 172 + "type": "uuid", 173 + "primaryKey": false, 174 + "notNull": false 158 175 }, 159 176 "release_name": { 160 177 "name": "release_name", 161 178 "type": "text", 162 179 "primaryKey": false, 163 - "notNull": false, 164 - "autoincrement": false 180 + "notNull": false 181 + }, 182 + "submission_client_agent": { 183 + "name": "submission_client_agent", 184 + "type": "text", 185 + "primaryKey": false, 186 + "notNull": false 165 187 }, 166 - "release_mb_id": { 167 - "name": "release_mb_id", 188 + "track_name": { 189 + "name": "track_name", 168 190 "type": "text", 169 191 "primaryKey": false, 170 - "notNull": false, 171 - "autoincrement": false 192 + "notNull": true 193 + }, 194 + "uri": { 195 + "name": "uri", 196 + "type": "text", 197 + "primaryKey": true, 198 + "notNull": true 199 + } 200 + }, 201 + "indexes": {}, 202 + "foreignKeys": { 203 + "plays_recording_mbid_recordings_mbid_fk": { 204 + "name": "plays_recording_mbid_recordings_mbid_fk", 205 + "tableFrom": "plays", 206 + "tableTo": "recordings", 207 + "columnsFrom": [ 208 + "recording_mbid" 209 + ], 210 + "columnsTo": [ 211 + "mbid" 212 + ], 213 + "onDelete": "no action", 214 + "onUpdate": "no action" 215 + }, 216 + "plays_release_mbid_releases_mbid_fk": { 217 + "name": "plays_release_mbid_releases_mbid_fk", 218 + "tableFrom": "plays", 219 + "tableTo": "releases", 220 + "columnsFrom": [ 221 + "release_mbid" 222 + ], 223 + "columnsTo": [ 224 + "mbid" 225 + ], 226 + "onDelete": "no action", 227 + "onUpdate": "no action" 228 + } 229 + }, 230 + "compositePrimaryKeys": {}, 231 + "uniqueConstraints": {}, 232 + "policies": {}, 233 + "checkConstraints": {}, 234 + "isRLSEnabled": false 235 + }, 236 + "public.profiles": { 237 + "name": "profiles", 238 + "schema": "", 239 + "columns": { 240 + "did": { 241 + "name": "did", 242 + "type": "text", 243 + "primaryKey": true, 244 + "notNull": true 172 245 }, 173 - "isrc": { 174 - "name": "isrc", 246 + "handle": { 247 + "name": "handle", 175 248 "type": "text", 176 249 "primaryKey": false, 177 - "notNull": false, 178 - "autoincrement": false 250 + "notNull": false 179 251 }, 180 - "origin_url": { 181 - "name": "origin_url", 252 + "display_name": { 253 + "name": "display_name", 182 254 "type": "text", 183 255 "primaryKey": false, 184 - "notNull": false, 185 - "autoincrement": false 256 + "notNull": false 186 257 }, 187 - "music_service_base_domain": { 188 - "name": "music_service_base_domain", 258 + "description": { 259 + "name": "description", 189 260 "type": "text", 190 261 "primaryKey": false, 191 - "notNull": false, 192 - "autoincrement": false 262 + "notNull": false 193 263 }, 194 - "submission_client_agent": { 195 - "name": "submission_client_agent", 264 + "description_facets": { 265 + "name": "description_facets", 266 + "type": "jsonb", 267 + "primaryKey": false, 268 + "notNull": false 269 + }, 270 + "avatar": { 271 + "name": "avatar", 196 272 "type": "text", 197 273 "primaryKey": false, 198 - "notNull": false, 199 - "autoincrement": false 274 + "notNull": false 200 275 }, 201 - "played_time": { 202 - "name": "played_time", 276 + "banner": { 277 + "name": "banner", 203 278 "type": "text", 204 279 "primaryKey": false, 205 - "notNull": false, 206 - "autoincrement": false 280 + "notNull": false 281 + }, 282 + "created_at": { 283 + "name": "created_at", 284 + "type": "timestamp", 285 + "primaryKey": false, 286 + "notNull": false 207 287 } 208 288 }, 209 289 "indexes": {}, 210 290 "foreignKeys": {}, 211 291 "compositePrimaryKeys": {}, 212 292 "uniqueConstraints": {}, 213 - "checkConstraints": {} 293 + "policies": {}, 294 + "checkConstraints": {}, 295 + "isRLSEnabled": false 214 296 }, 215 - "status": { 216 - "name": "status", 297 + "public.recordings": { 298 + "name": "recordings", 299 + "schema": "", 217 300 "columns": { 218 - "uri": { 219 - "name": "uri", 220 - "type": "text", 301 + "mbid": { 302 + "name": "mbid", 303 + "type": "uuid", 221 304 "primaryKey": true, 222 - "notNull": true, 223 - "autoincrement": false 305 + "notNull": true 224 306 }, 225 - "author_did": { 226 - "name": "author_did", 307 + "name": { 308 + "name": "name", 227 309 "type": "text", 228 310 "primaryKey": false, 229 - "notNull": true, 230 - "autoincrement": false 311 + "notNull": true 231 312 }, 232 - "status": { 233 - "name": "status", 234 - "type": "text", 313 + "play_count": { 314 + "name": "play_count", 315 + "type": "integer", 235 316 "primaryKey": false, 236 - "notNull": true, 237 - "autoincrement": false 317 + "notNull": false, 318 + "default": 0 319 + } 320 + }, 321 + "indexes": {}, 322 + "foreignKeys": {}, 323 + "compositePrimaryKeys": {}, 324 + "uniqueConstraints": {}, 325 + "policies": {}, 326 + "checkConstraints": {}, 327 + "isRLSEnabled": false 328 + }, 329 + "public.releases": { 330 + "name": "releases", 331 + "schema": "", 332 + "columns": { 333 + "mbid": { 334 + "name": "mbid", 335 + "type": "uuid", 336 + "primaryKey": true, 337 + "notNull": true 238 338 }, 239 - "created_at": { 240 - "name": "created_at", 339 + "name": { 340 + "name": "name", 241 341 "type": "text", 242 342 "primaryKey": false, 243 - "notNull": true, 244 - "autoincrement": false 343 + "notNull": true 245 344 }, 246 - "indexed_at": { 247 - "name": "indexed_at", 248 - "type": "text", 345 + "play_count": { 346 + "name": "play_count", 347 + "type": "integer", 249 348 "primaryKey": false, 250 - "notNull": true, 251 - "autoincrement": false 349 + "notNull": false, 350 + "default": 0 252 351 } 253 352 }, 254 353 "indexes": {}, 255 354 "foreignKeys": {}, 256 355 "compositePrimaryKeys": {}, 257 356 "uniqueConstraints": {}, 258 - "checkConstraints": {} 357 + "policies": {}, 358 + "checkConstraints": {}, 359 + "isRLSEnabled": false 259 360 }, 260 - "teal_session": { 261 - "name": "teal_session", 361 + "public.featured_items": { 362 + "name": "featured_items", 363 + "schema": "", 262 364 "columns": { 263 - "key": { 264 - "name": "key", 365 + "did": { 366 + "name": "did", 265 367 "type": "text", 266 368 "primaryKey": true, 267 - "notNull": true, 268 - "autoincrement": false 369 + "notNull": true 269 370 }, 270 - "session": { 271 - "name": "session", 371 + "mbid": { 372 + "name": "mbid", 272 373 "type": "text", 273 374 "primaryKey": false, 274 - "notNull": true, 275 - "autoincrement": false 375 + "notNull": true 276 376 }, 277 - "provider": { 278 - "name": "provider", 377 + "type": { 378 + "name": "type", 279 379 "type": "text", 280 380 "primaryKey": false, 281 - "notNull": true, 282 - "autoincrement": false 381 + "notNull": true 283 382 } 284 383 }, 285 384 "indexes": {}, 286 385 "foreignKeys": {}, 287 386 "compositePrimaryKeys": {}, 288 387 "uniqueConstraints": {}, 289 - "checkConstraints": {} 388 + "policies": {}, 389 + "checkConstraints": {}, 390 + "isRLSEnabled": false 391 + } 392 + }, 393 + "enums": {}, 394 + "schemas": {}, 395 + "sequences": {}, 396 + "roles": {}, 397 + "policies": {}, 398 + "views": { 399 + "public.mv_artist_play_counts": { 400 + "columns": { 401 + "mbid": { 402 + "name": "mbid", 403 + "type": "uuid", 404 + "primaryKey": true, 405 + "notNull": true 406 + }, 407 + "name": { 408 + "name": "name", 409 + "type": "text", 410 + "primaryKey": false, 411 + "notNull": true 412 + } 413 + }, 414 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" left join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" left join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" group by \"artists\".\"mbid\", \"artists\".\"name\"", 415 + "name": "mv_artist_play_counts", 416 + "schema": "public", 417 + "isExisting": false, 418 + "materialized": true 419 + }, 420 + "public.mv_global_play_count": { 421 + "columns": {}, 422 + "definition": "select count(\"uri\") as \"total_plays\", count(distinct \"did\") as \"unique_listeners\" from \"plays\"", 423 + "name": "mv_global_play_count", 424 + "schema": "public", 425 + "isExisting": false, 426 + "materialized": true 290 427 }, 291 - "teal_user": { 292 - "name": "teal_user", 428 + "public.mv_recording_play_counts": { 293 429 "columns": { 294 - "did": { 295 - "name": "did", 296 - "type": "text", 430 + "mbid": { 431 + "name": "mbid", 432 + "type": "uuid", 297 433 "primaryKey": true, 298 - "notNull": true, 299 - "autoincrement": false 434 + "notNull": true 300 435 }, 301 - "handle": { 302 - "name": "handle", 436 + "name": { 437 + "name": "name", 303 438 "type": "text", 304 439 "primaryKey": false, 305 - "notNull": true, 306 - "autoincrement": false 440 + "notNull": true 441 + } 442 + }, 443 + "definition": "select \"recordings\".\"mbid\", \"recordings\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"recordings\" left join \"plays\" on \"plays\".\"recording_mbid\" = \"recordings\".\"mbid\" group by \"recordings\".\"mbid\", \"recordings\".\"name\"", 444 + "name": "mv_recording_play_counts", 445 + "schema": "public", 446 + "isExisting": false, 447 + "materialized": true 448 + }, 449 + "public.mv_release_play_counts": { 450 + "columns": { 451 + "mbid": { 452 + "name": "mbid", 453 + "type": "uuid", 454 + "primaryKey": true, 455 + "notNull": true 307 456 }, 308 - "avatar": { 309 - "name": "avatar", 457 + "name": { 458 + "name": "name", 310 459 "type": "text", 311 460 "primaryKey": false, 312 - "notNull": true, 313 - "autoincrement": false 461 + "notNull": true 462 + } 463 + }, 464 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" left join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" group by \"releases\".\"mbid\", \"releases\".\"name\"", 465 + "name": "mv_release_play_counts", 466 + "schema": "public", 467 + "isExisting": false, 468 + "materialized": true 469 + }, 470 + "public.mv_top_artists_30days": { 471 + "columns": { 472 + "mbid": { 473 + "name": "mbid", 474 + "type": "uuid", 475 + "primaryKey": true, 476 + "notNull": true 314 477 }, 315 - "bio": { 316 - "name": "bio", 478 + "name": { 479 + "name": "name", 317 480 "type": "text", 318 481 "primaryKey": false, 319 - "notNull": false, 320 - "autoincrement": false 482 + "notNull": true 483 + } 484 + }, 485 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC", 486 + "name": "mv_top_artists_30days", 487 + "schema": "public", 488 + "isExisting": false, 489 + "materialized": true 490 + }, 491 + "public.mv_top_releases_30days": { 492 + "columns": { 493 + "mbid": { 494 + "name": "mbid", 495 + "type": "uuid", 496 + "primaryKey": true, 497 + "notNull": true 321 498 }, 322 - "created_at": { 323 - "name": "created_at", 499 + "name": { 500 + "name": "name", 324 501 "type": "text", 325 502 "primaryKey": false, 326 - "notNull": true, 327 - "autoincrement": false 503 + "notNull": true 328 504 } 329 505 }, 330 - "indexes": {}, 331 - "foreignKeys": {}, 332 - "compositePrimaryKeys": {}, 333 - "uniqueConstraints": {}, 334 - "checkConstraints": {} 506 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC", 507 + "name": "mv_top_releases_30days", 508 + "schema": "public", 509 + "isExisting": false, 510 + "materialized": true 335 511 } 336 512 }, 337 - "views": {}, 338 - "enums": {}, 339 513 "_meta": { 514 + "columns": {}, 340 515 "schemas": {}, 341 - "tables": {}, 342 - "columns": {} 343 - }, 344 - "internal": { 345 - "indexes": {} 516 + "tables": {} 346 517 } 347 518 }
+376 -212
packages/db/.drizzle/meta/0005_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "42a94e7a-c4c2-4bd5-92d9-ba8a829c0704", 5 - "prevId": "639ec806-61a1-448d-a922-1935bf8f6cf3", 2 + "id": "c4b1bdd0-5fea-44e4-b753-4f25193b9c87", 3 + "prevId": "01c93ffb-209e-48f7-9d79-1a4b624c2b0b", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "atp_session": { 8 - "name": "atp_session", 7 + "public.artists": { 8 + "name": "artists", 9 + "schema": "", 9 10 "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 11 + "mbid": { 12 + "name": "mbid", 13 + "type": "uuid", 13 14 "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 15 + "notNull": true 16 16 }, 17 - "session": { 18 - "name": "session", 17 + "name": { 18 + "name": "name", 19 19 "type": "text", 20 20 "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 23 - } 24 - }, 25 - "indexes": {}, 26 - "foreignKeys": {}, 27 - "compositePrimaryKeys": {}, 28 - "uniqueConstraints": {}, 29 - "checkConstraints": {} 30 - }, 31 - "auth_state": { 32 - "name": "auth_state", 33 - "columns": { 34 - "key": { 35 - "name": "key", 36 - "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 21 + "notNull": true 40 22 }, 41 - "state": { 42 - "name": "state", 43 - "type": "text", 23 + "play_count": { 24 + "name": "play_count", 25 + "type": "integer", 44 26 "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 27 + "notNull": false, 28 + "default": 0 47 29 } 48 30 }, 49 31 "indexes": {}, 50 32 "foreignKeys": {}, 51 33 "compositePrimaryKeys": {}, 52 34 "uniqueConstraints": {}, 53 - "checkConstraints": {} 35 + "policies": {}, 36 + "checkConstraints": {}, 37 + "isRLSEnabled": false 54 38 }, 55 - "follow": { 56 - "name": "follow", 39 + "public.play_to_artists": { 40 + "name": "play_to_artists", 41 + "schema": "", 57 42 "columns": { 58 - "rel_id": { 59 - "name": "rel_id", 60 - "type": "text", 61 - "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 64 - }, 65 - "follower": { 66 - "name": "follower", 67 - "type": "text", 43 + "artist_mbid": { 44 + "name": "artist_mbid", 45 + "type": "uuid", 68 46 "primaryKey": false, 69 - "notNull": true, 70 - "autoincrement": false 47 + "notNull": true 71 48 }, 72 - "followed": { 73 - "name": "followed", 49 + "artist_name": { 50 + "name": "artist_name", 74 51 "type": "text", 75 52 "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 53 + "notNull": false 78 54 }, 79 - "created_at": { 80 - "name": "created_at", 55 + "play_uri": { 56 + "name": "play_uri", 81 57 "type": "text", 82 58 "primaryKey": false, 83 - "notNull": true, 84 - "autoincrement": false 59 + "notNull": true 85 60 } 86 61 }, 87 62 "indexes": {}, 88 - "foreignKeys": {}, 89 - "compositePrimaryKeys": {}, 63 + "foreignKeys": { 64 + "play_to_artists_artist_mbid_artists_mbid_fk": { 65 + "name": "play_to_artists_artist_mbid_artists_mbid_fk", 66 + "tableFrom": "play_to_artists", 67 + "tableTo": "artists", 68 + "columnsFrom": [ 69 + "artist_mbid" 70 + ], 71 + "columnsTo": [ 72 + "mbid" 73 + ], 74 + "onDelete": "no action", 75 + "onUpdate": "no action" 76 + }, 77 + "play_to_artists_play_uri_plays_uri_fk": { 78 + "name": "play_to_artists_play_uri_plays_uri_fk", 79 + "tableFrom": "play_to_artists", 80 + "tableTo": "plays", 81 + "columnsFrom": [ 82 + "play_uri" 83 + ], 84 + "columnsTo": [ 85 + "uri" 86 + ], 87 + "onDelete": "no action", 88 + "onUpdate": "no action" 89 + } 90 + }, 91 + "compositePrimaryKeys": { 92 + "play_to_artists_play_uri_artist_mbid_pk": { 93 + "name": "play_to_artists_play_uri_artist_mbid_pk", 94 + "columns": [ 95 + "play_uri", 96 + "artist_mbid" 97 + ] 98 + } 99 + }, 90 100 "uniqueConstraints": {}, 91 - "checkConstraints": {} 101 + "policies": {}, 102 + "checkConstraints": {}, 103 + "isRLSEnabled": false 92 104 }, 93 - "play": { 94 - "name": "play", 105 + "public.plays": { 106 + "name": "plays", 107 + "schema": "", 95 108 "columns": { 96 - "uri": { 97 - "name": "uri", 109 + "cid": { 110 + "name": "cid", 98 111 "type": "text", 99 - "primaryKey": true, 100 - "notNull": true, 101 - "autoincrement": false 112 + "primaryKey": false, 113 + "notNull": true 102 114 }, 103 - "author_did": { 104 - "name": "author_did", 115 + "did": { 116 + "name": "did", 105 117 "type": "text", 106 118 "primaryKey": false, 107 - "notNull": true, 108 - "autoincrement": false 119 + "notNull": true 109 120 }, 110 - "created_at": { 111 - "name": "created_at", 112 - "type": "text", 121 + "duration": { 122 + "name": "duration", 123 + "type": "integer", 113 124 "primaryKey": false, 114 - "notNull": true, 115 - "autoincrement": false 125 + "notNull": false 116 126 }, 117 - "indexed_at": { 118 - "name": "indexed_at", 127 + "isrc": { 128 + "name": "isrc", 119 129 "type": "text", 120 130 "primaryKey": false, 121 - "notNull": true, 122 - "autoincrement": false 131 + "notNull": false 123 132 }, 124 - "track_name": { 125 - "name": "track_name", 133 + "music_service_base_domain": { 134 + "name": "music_service_base_domain", 126 135 "type": "text", 127 136 "primaryKey": false, 128 - "notNull": true, 129 - "autoincrement": false 137 + "notNull": false 130 138 }, 131 - "track_mb_id": { 132 - "name": "track_mb_id", 139 + "origin_url": { 140 + "name": "origin_url", 133 141 "type": "text", 134 142 "primaryKey": false, 135 - "notNull": false, 136 - "autoincrement": false 143 + "notNull": false 137 144 }, 138 - "recording_mb_id": { 139 - "name": "recording_mb_id", 140 - "type": "text", 145 + "played_time": { 146 + "name": "played_time", 147 + "type": "timestamp with time zone", 141 148 "primaryKey": false, 142 - "notNull": false, 143 - "autoincrement": false 149 + "notNull": false 144 150 }, 145 - "duration": { 146 - "name": "duration", 147 - "type": "integer", 151 + "processed_time": { 152 + "name": "processed_time", 153 + "type": "timestamp with time zone", 148 154 "primaryKey": false, 149 155 "notNull": false, 150 - "autoincrement": false 156 + "default": "now()" 151 157 }, 152 - "artist_name": { 153 - "name": "artist_name", 158 + "rkey": { 159 + "name": "rkey", 154 160 "type": "text", 155 161 "primaryKey": false, 156 - "notNull": true, 157 - "autoincrement": false 162 + "notNull": true 158 163 }, 159 - "artist_mb_ids": { 160 - "name": "artist_mb_ids", 161 - "type": "text", 164 + "recording_mbid": { 165 + "name": "recording_mbid", 166 + "type": "uuid", 167 + "primaryKey": false, 168 + "notNull": false 169 + }, 170 + "release_mbid": { 171 + "name": "release_mbid", 172 + "type": "uuid", 162 173 "primaryKey": false, 163 - "notNull": false, 164 - "autoincrement": false 174 + "notNull": false 165 175 }, 166 176 "release_name": { 167 177 "name": "release_name", 168 178 "type": "text", 169 179 "primaryKey": false, 170 - "notNull": false, 171 - "autoincrement": false 180 + "notNull": false 172 181 }, 173 - "release_mb_id": { 174 - "name": "release_mb_id", 182 + "submission_client_agent": { 183 + "name": "submission_client_agent", 175 184 "type": "text", 176 185 "primaryKey": false, 177 - "notNull": false, 178 - "autoincrement": false 186 + "notNull": false 179 187 }, 180 - "isrc": { 181 - "name": "isrc", 188 + "track_name": { 189 + "name": "track_name", 182 190 "type": "text", 183 191 "primaryKey": false, 184 - "notNull": false, 185 - "autoincrement": false 192 + "notNull": true 186 193 }, 187 - "origin_url": { 188 - "name": "origin_url", 194 + "uri": { 195 + "name": "uri", 196 + "type": "text", 197 + "primaryKey": true, 198 + "notNull": true 199 + } 200 + }, 201 + "indexes": {}, 202 + "foreignKeys": { 203 + "plays_recording_mbid_recordings_mbid_fk": { 204 + "name": "plays_recording_mbid_recordings_mbid_fk", 205 + "tableFrom": "plays", 206 + "tableTo": "recordings", 207 + "columnsFrom": [ 208 + "recording_mbid" 209 + ], 210 + "columnsTo": [ 211 + "mbid" 212 + ], 213 + "onDelete": "no action", 214 + "onUpdate": "no action" 215 + }, 216 + "plays_release_mbid_releases_mbid_fk": { 217 + "name": "plays_release_mbid_releases_mbid_fk", 218 + "tableFrom": "plays", 219 + "tableTo": "releases", 220 + "columnsFrom": [ 221 + "release_mbid" 222 + ], 223 + "columnsTo": [ 224 + "mbid" 225 + ], 226 + "onDelete": "no action", 227 + "onUpdate": "no action" 228 + } 229 + }, 230 + "compositePrimaryKeys": {}, 231 + "uniqueConstraints": {}, 232 + "policies": {}, 233 + "checkConstraints": {}, 234 + "isRLSEnabled": false 235 + }, 236 + "public.profiles": { 237 + "name": "profiles", 238 + "schema": "", 239 + "columns": { 240 + "did": { 241 + "name": "did", 242 + "type": "text", 243 + "primaryKey": true, 244 + "notNull": true 245 + }, 246 + "handle": { 247 + "name": "handle", 189 248 "type": "text", 190 249 "primaryKey": false, 191 - "notNull": false, 192 - "autoincrement": false 250 + "notNull": false 193 251 }, 194 - "music_service_base_domain": { 195 - "name": "music_service_base_domain", 252 + "display_name": { 253 + "name": "display_name", 196 254 "type": "text", 197 255 "primaryKey": false, 198 - "notNull": false, 199 - "autoincrement": false 256 + "notNull": false 200 257 }, 201 - "submission_client_agent": { 202 - "name": "submission_client_agent", 258 + "description": { 259 + "name": "description", 203 260 "type": "text", 204 261 "primaryKey": false, 205 - "notNull": false, 206 - "autoincrement": false 262 + "notNull": false 207 263 }, 208 - "played_time": { 209 - "name": "played_time", 264 + "description_facets": { 265 + "name": "description_facets", 266 + "type": "jsonb", 267 + "primaryKey": false, 268 + "notNull": false 269 + }, 270 + "avatar": { 271 + "name": "avatar", 210 272 "type": "text", 211 273 "primaryKey": false, 212 - "notNull": false, 213 - "autoincrement": false 274 + "notNull": false 275 + }, 276 + "banner": { 277 + "name": "banner", 278 + "type": "text", 279 + "primaryKey": false, 280 + "notNull": false 281 + }, 282 + "created_at": { 283 + "name": "created_at", 284 + "type": "timestamp with time zone", 285 + "primaryKey": false, 286 + "notNull": false 214 287 } 215 288 }, 216 289 "indexes": {}, 217 290 "foreignKeys": {}, 218 291 "compositePrimaryKeys": {}, 219 292 "uniqueConstraints": {}, 220 - "checkConstraints": {} 293 + "policies": {}, 294 + "checkConstraints": {}, 295 + "isRLSEnabled": false 221 296 }, 222 - "status": { 223 - "name": "status", 297 + "public.recordings": { 298 + "name": "recordings", 299 + "schema": "", 224 300 "columns": { 225 - "uri": { 226 - "name": "uri", 227 - "type": "text", 301 + "mbid": { 302 + "name": "mbid", 303 + "type": "uuid", 228 304 "primaryKey": true, 229 - "notNull": true, 230 - "autoincrement": false 305 + "notNull": true 231 306 }, 232 - "author_did": { 233 - "name": "author_did", 307 + "name": { 308 + "name": "name", 234 309 "type": "text", 235 310 "primaryKey": false, 236 - "notNull": true, 237 - "autoincrement": false 311 + "notNull": true 238 312 }, 239 - "status": { 240 - "name": "status", 241 - "type": "text", 313 + "play_count": { 314 + "name": "play_count", 315 + "type": "integer", 242 316 "primaryKey": false, 243 - "notNull": true, 244 - "autoincrement": false 317 + "notNull": false, 318 + "default": 0 319 + } 320 + }, 321 + "indexes": {}, 322 + "foreignKeys": {}, 323 + "compositePrimaryKeys": {}, 324 + "uniqueConstraints": {}, 325 + "policies": {}, 326 + "checkConstraints": {}, 327 + "isRLSEnabled": false 328 + }, 329 + "public.releases": { 330 + "name": "releases", 331 + "schema": "", 332 + "columns": { 333 + "mbid": { 334 + "name": "mbid", 335 + "type": "uuid", 336 + "primaryKey": true, 337 + "notNull": true 245 338 }, 246 - "created_at": { 247 - "name": "created_at", 339 + "name": { 340 + "name": "name", 248 341 "type": "text", 249 342 "primaryKey": false, 250 - "notNull": true, 251 - "autoincrement": false 343 + "notNull": true 252 344 }, 253 - "indexed_at": { 254 - "name": "indexed_at", 255 - "type": "text", 345 + "play_count": { 346 + "name": "play_count", 347 + "type": "integer", 256 348 "primaryKey": false, 257 - "notNull": true, 258 - "autoincrement": false 349 + "notNull": false, 350 + "default": 0 259 351 } 260 352 }, 261 353 "indexes": {}, 262 354 "foreignKeys": {}, 263 355 "compositePrimaryKeys": {}, 264 356 "uniqueConstraints": {}, 265 - "checkConstraints": {} 357 + "policies": {}, 358 + "checkConstraints": {}, 359 + "isRLSEnabled": false 266 360 }, 267 - "teal_session": { 268 - "name": "teal_session", 361 + "public.featured_items": { 362 + "name": "featured_items", 363 + "schema": "", 269 364 "columns": { 270 - "key": { 271 - "name": "key", 365 + "did": { 366 + "name": "did", 272 367 "type": "text", 273 368 "primaryKey": true, 274 - "notNull": true, 275 - "autoincrement": false 369 + "notNull": true 276 370 }, 277 - "session": { 278 - "name": "session", 371 + "mbid": { 372 + "name": "mbid", 279 373 "type": "text", 280 374 "primaryKey": false, 281 - "notNull": true, 282 - "autoincrement": false 375 + "notNull": true 283 376 }, 284 - "provider": { 285 - "name": "provider", 377 + "type": { 378 + "name": "type", 286 379 "type": "text", 287 380 "primaryKey": false, 288 - "notNull": true, 289 - "autoincrement": false 381 + "notNull": true 290 382 } 291 383 }, 292 384 "indexes": {}, 293 385 "foreignKeys": {}, 294 386 "compositePrimaryKeys": {}, 295 387 "uniqueConstraints": {}, 296 - "checkConstraints": {} 297 - }, 298 - "teal_user": { 299 - "name": "teal_user", 388 + "policies": {}, 389 + "checkConstraints": {}, 390 + "isRLSEnabled": false 391 + } 392 + }, 393 + "enums": {}, 394 + "schemas": {}, 395 + "sequences": {}, 396 + "roles": {}, 397 + "policies": {}, 398 + "views": { 399 + "public.mv_artist_play_counts": { 300 400 "columns": { 301 - "did": { 302 - "name": "did", 401 + "mbid": { 402 + "name": "mbid", 403 + "type": "uuid", 404 + "primaryKey": true, 405 + "notNull": true 406 + }, 407 + "name": { 408 + "name": "name", 303 409 "type": "text", 410 + "primaryKey": false, 411 + "notNull": true 412 + } 413 + }, 414 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" left join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" left join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" group by \"artists\".\"mbid\", \"artists\".\"name\"", 415 + "name": "mv_artist_play_counts", 416 + "schema": "public", 417 + "isExisting": false, 418 + "materialized": true 419 + }, 420 + "public.mv_global_play_count": { 421 + "columns": {}, 422 + "definition": "select count(\"uri\") as \"total_plays\", count(distinct \"did\") as \"unique_listeners\" from \"plays\"", 423 + "name": "mv_global_play_count", 424 + "schema": "public", 425 + "isExisting": false, 426 + "materialized": true 427 + }, 428 + "public.mv_recording_play_counts": { 429 + "columns": { 430 + "mbid": { 431 + "name": "mbid", 432 + "type": "uuid", 304 433 "primaryKey": true, 305 - "notNull": true, 306 - "autoincrement": false 434 + "notNull": true 307 435 }, 308 - "handle": { 309 - "name": "handle", 436 + "name": { 437 + "name": "name", 310 438 "type": "text", 311 439 "primaryKey": false, 312 - "notNull": true, 313 - "autoincrement": false 440 + "notNull": true 441 + } 442 + }, 443 + "definition": "select \"recordings\".\"mbid\", \"recordings\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"recordings\" left join \"plays\" on \"plays\".\"recording_mbid\" = \"recordings\".\"mbid\" group by \"recordings\".\"mbid\", \"recordings\".\"name\"", 444 + "name": "mv_recording_play_counts", 445 + "schema": "public", 446 + "isExisting": false, 447 + "materialized": true 448 + }, 449 + "public.mv_release_play_counts": { 450 + "columns": { 451 + "mbid": { 452 + "name": "mbid", 453 + "type": "uuid", 454 + "primaryKey": true, 455 + "notNull": true 314 456 }, 315 - "avatar": { 316 - "name": "avatar", 457 + "name": { 458 + "name": "name", 317 459 "type": "text", 318 460 "primaryKey": false, 319 - "notNull": true, 320 - "autoincrement": false 461 + "notNull": true 462 + } 463 + }, 464 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" left join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" group by \"releases\".\"mbid\", \"releases\".\"name\"", 465 + "name": "mv_release_play_counts", 466 + "schema": "public", 467 + "isExisting": false, 468 + "materialized": true 469 + }, 470 + "public.mv_top_artists_30days": { 471 + "columns": { 472 + "mbid": { 473 + "name": "mbid", 474 + "type": "uuid", 475 + "primaryKey": true, 476 + "notNull": true 321 477 }, 322 - "bio": { 323 - "name": "bio", 478 + "name": { 479 + "name": "name", 324 480 "type": "text", 325 481 "primaryKey": false, 326 - "notNull": false, 327 - "autoincrement": false 482 + "notNull": true 483 + } 484 + }, 485 + "definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC", 486 + "name": "mv_top_artists_30days", 487 + "schema": "public", 488 + "isExisting": false, 489 + "materialized": true 490 + }, 491 + "public.mv_top_releases_30days": { 492 + "columns": { 493 + "mbid": { 494 + "name": "mbid", 495 + "type": "uuid", 496 + "primaryKey": true, 497 + "notNull": true 328 498 }, 329 - "created_at": { 330 - "name": "created_at", 499 + "name": { 500 + "name": "name", 331 501 "type": "text", 332 502 "primaryKey": false, 333 - "notNull": true, 334 - "autoincrement": false 503 + "notNull": true 335 504 } 336 505 }, 337 - "indexes": {}, 338 - "foreignKeys": {}, 339 - "compositePrimaryKeys": {}, 340 - "uniqueConstraints": {}, 341 - "checkConstraints": {} 506 + "definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC", 507 + "name": "mv_top_releases_30days", 508 + "schema": "public", 509 + "isExisting": false, 510 + "materialized": true 342 511 } 343 512 }, 344 - "views": {}, 345 - "enums": {}, 346 513 "_meta": { 514 + "columns": {}, 347 515 "schemas": {}, 348 - "tables": {}, 349 - "columns": {} 350 - }, 351 - "internal": { 352 - "indexes": {} 516 + "tables": {} 353 517 } 354 518 }
-356
packages/db/.drizzle/meta/0006_snapshot.json
··· 1 - { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "ff3fcc45-0760-4f6d-8f7c-659a1ee80ed5", 5 - "prevId": "42a94e7a-c4c2-4bd5-92d9-ba8a829c0704", 6 - "tables": { 7 - "atp_session": { 8 - "name": "atp_session", 9 - "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 13 - "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 16 - }, 17 - "session": { 18 - "name": "session", 19 - "type": "text", 20 - "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 23 - } 24 - }, 25 - "indexes": {}, 26 - "foreignKeys": {}, 27 - "compositePrimaryKeys": {}, 28 - "uniqueConstraints": {}, 29 - "checkConstraints": {} 30 - }, 31 - "auth_state": { 32 - "name": "auth_state", 33 - "columns": { 34 - "key": { 35 - "name": "key", 36 - "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 40 - }, 41 - "state": { 42 - "name": "state", 43 - "type": "text", 44 - "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 47 - } 48 - }, 49 - "indexes": {}, 50 - "foreignKeys": {}, 51 - "compositePrimaryKeys": {}, 52 - "uniqueConstraints": {}, 53 - "checkConstraints": {} 54 - }, 55 - "follow": { 56 - "name": "follow", 57 - "columns": { 58 - "rel_id": { 59 - "name": "rel_id", 60 - "type": "text", 61 - "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 64 - }, 65 - "follower": { 66 - "name": "follower", 67 - "type": "text", 68 - "primaryKey": false, 69 - "notNull": true, 70 - "autoincrement": false 71 - }, 72 - "followed": { 73 - "name": "followed", 74 - "type": "text", 75 - "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 78 - }, 79 - "created_at": { 80 - "name": "created_at", 81 - "type": "text", 82 - "primaryKey": false, 83 - "notNull": true, 84 - "autoincrement": false 85 - } 86 - }, 87 - "indexes": {}, 88 - "foreignKeys": {}, 89 - "compositePrimaryKeys": {}, 90 - "uniqueConstraints": {}, 91 - "checkConstraints": {} 92 - }, 93 - "play": { 94 - "name": "play", 95 - "columns": { 96 - "uri": { 97 - "name": "uri", 98 - "type": "text", 99 - "primaryKey": true, 100 - "notNull": true, 101 - "autoincrement": false 102 - }, 103 - "author_did": { 104 - "name": "author_did", 105 - "type": "text", 106 - "primaryKey": false, 107 - "notNull": true, 108 - "autoincrement": false 109 - }, 110 - "created_at": { 111 - "name": "created_at", 112 - "type": "text", 113 - "primaryKey": false, 114 - "notNull": true, 115 - "autoincrement": false 116 - }, 117 - "indexed_at": { 118 - "name": "indexed_at", 119 - "type": "text", 120 - "primaryKey": false, 121 - "notNull": true, 122 - "autoincrement": false 123 - }, 124 - "track_name": { 125 - "name": "track_name", 126 - "type": "text", 127 - "primaryKey": false, 128 - "notNull": true, 129 - "autoincrement": false 130 - }, 131 - "track_mb_id": { 132 - "name": "track_mb_id", 133 - "type": "text", 134 - "primaryKey": false, 135 - "notNull": false, 136 - "autoincrement": false 137 - }, 138 - "recording_mb_id": { 139 - "name": "recording_mb_id", 140 - "type": "text", 141 - "primaryKey": false, 142 - "notNull": false, 143 - "autoincrement": false 144 - }, 145 - "duration": { 146 - "name": "duration", 147 - "type": "integer", 148 - "primaryKey": false, 149 - "notNull": false, 150 - "autoincrement": false 151 - }, 152 - "artist_names": { 153 - "name": "artist_names", 154 - "type": "text", 155 - "primaryKey": false, 156 - "notNull": false, 157 - "autoincrement": false 158 - }, 159 - "artist_mb_ids": { 160 - "name": "artist_mb_ids", 161 - "type": "text", 162 - "primaryKey": false, 163 - "notNull": false, 164 - "autoincrement": false 165 - }, 166 - "release_name": { 167 - "name": "release_name", 168 - "type": "text", 169 - "primaryKey": false, 170 - "notNull": false, 171 - "autoincrement": false 172 - }, 173 - "release_mb_id": { 174 - "name": "release_mb_id", 175 - "type": "text", 176 - "primaryKey": false, 177 - "notNull": false, 178 - "autoincrement": false 179 - }, 180 - "isrc": { 181 - "name": "isrc", 182 - "type": "text", 183 - "primaryKey": false, 184 - "notNull": false, 185 - "autoincrement": false 186 - }, 187 - "origin_url": { 188 - "name": "origin_url", 189 - "type": "text", 190 - "primaryKey": false, 191 - "notNull": false, 192 - "autoincrement": false 193 - }, 194 - "music_service_base_domain": { 195 - "name": "music_service_base_domain", 196 - "type": "text", 197 - "primaryKey": false, 198 - "notNull": false, 199 - "autoincrement": false 200 - }, 201 - "submission_client_agent": { 202 - "name": "submission_client_agent", 203 - "type": "text", 204 - "primaryKey": false, 205 - "notNull": false, 206 - "autoincrement": false 207 - }, 208 - "played_time": { 209 - "name": "played_time", 210 - "type": "text", 211 - "primaryKey": false, 212 - "notNull": false, 213 - "autoincrement": false 214 - } 215 - }, 216 - "indexes": {}, 217 - "foreignKeys": {}, 218 - "compositePrimaryKeys": {}, 219 - "uniqueConstraints": {}, 220 - "checkConstraints": {} 221 - }, 222 - "status": { 223 - "name": "status", 224 - "columns": { 225 - "uri": { 226 - "name": "uri", 227 - "type": "text", 228 - "primaryKey": true, 229 - "notNull": true, 230 - "autoincrement": false 231 - }, 232 - "author_did": { 233 - "name": "author_did", 234 - "type": "text", 235 - "primaryKey": false, 236 - "notNull": true, 237 - "autoincrement": false 238 - }, 239 - "status": { 240 - "name": "status", 241 - "type": "text", 242 - "primaryKey": false, 243 - "notNull": true, 244 - "autoincrement": false 245 - }, 246 - "created_at": { 247 - "name": "created_at", 248 - "type": "text", 249 - "primaryKey": false, 250 - "notNull": true, 251 - "autoincrement": false 252 - }, 253 - "indexed_at": { 254 - "name": "indexed_at", 255 - "type": "text", 256 - "primaryKey": false, 257 - "notNull": true, 258 - "autoincrement": false 259 - } 260 - }, 261 - "indexes": {}, 262 - "foreignKeys": {}, 263 - "compositePrimaryKeys": {}, 264 - "uniqueConstraints": {}, 265 - "checkConstraints": {} 266 - }, 267 - "teal_session": { 268 - "name": "teal_session", 269 - "columns": { 270 - "key": { 271 - "name": "key", 272 - "type": "text", 273 - "primaryKey": true, 274 - "notNull": true, 275 - "autoincrement": false 276 - }, 277 - "session": { 278 - "name": "session", 279 - "type": "text", 280 - "primaryKey": false, 281 - "notNull": true, 282 - "autoincrement": false 283 - }, 284 - "provider": { 285 - "name": "provider", 286 - "type": "text", 287 - "primaryKey": false, 288 - "notNull": true, 289 - "autoincrement": false 290 - } 291 - }, 292 - "indexes": {}, 293 - "foreignKeys": {}, 294 - "compositePrimaryKeys": {}, 295 - "uniqueConstraints": {}, 296 - "checkConstraints": {} 297 - }, 298 - "teal_user": { 299 - "name": "teal_user", 300 - "columns": { 301 - "did": { 302 - "name": "did", 303 - "type": "text", 304 - "primaryKey": true, 305 - "notNull": true, 306 - "autoincrement": false 307 - }, 308 - "handle": { 309 - "name": "handle", 310 - "type": "text", 311 - "primaryKey": false, 312 - "notNull": true, 313 - "autoincrement": false 314 - }, 315 - "avatar": { 316 - "name": "avatar", 317 - "type": "text", 318 - "primaryKey": false, 319 - "notNull": true, 320 - "autoincrement": false 321 - }, 322 - "bio": { 323 - "name": "bio", 324 - "type": "text", 325 - "primaryKey": false, 326 - "notNull": false, 327 - "autoincrement": false 328 - }, 329 - "created_at": { 330 - "name": "created_at", 331 - "type": "text", 332 - "primaryKey": false, 333 - "notNull": true, 334 - "autoincrement": false 335 - } 336 - }, 337 - "indexes": {}, 338 - "foreignKeys": {}, 339 - "compositePrimaryKeys": {}, 340 - "uniqueConstraints": {}, 341 - "checkConstraints": {} 342 - } 343 - }, 344 - "views": {}, 345 - "enums": {}, 346 - "_meta": { 347 - "schemas": {}, 348 - "tables": {}, 349 - "columns": { 350 - "\"play\".\"artist_name\"": "\"play\".\"artist_names\"" 351 - } 352 - }, 353 - "internal": { 354 - "indexes": {} 355 - } 356 - }
-356
packages/db/.drizzle/meta/0007_snapshot.json
··· 1 - { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "ee14b661-c5fd-4ec3-a87a-0b19f2a4c17f", 5 - "prevId": "ff3fcc45-0760-4f6d-8f7c-659a1ee80ed5", 6 - "tables": { 7 - "atp_session": { 8 - "name": "atp_session", 9 - "columns": { 10 - "key": { 11 - "name": "key", 12 - "type": "text", 13 - "primaryKey": true, 14 - "notNull": true, 15 - "autoincrement": false 16 - }, 17 - "session": { 18 - "name": "session", 19 - "type": "text", 20 - "primaryKey": false, 21 - "notNull": true, 22 - "autoincrement": false 23 - } 24 - }, 25 - "indexes": {}, 26 - "foreignKeys": {}, 27 - "compositePrimaryKeys": {}, 28 - "uniqueConstraints": {}, 29 - "checkConstraints": {} 30 - }, 31 - "auth_state": { 32 - "name": "auth_state", 33 - "columns": { 34 - "key": { 35 - "name": "key", 36 - "type": "text", 37 - "primaryKey": true, 38 - "notNull": true, 39 - "autoincrement": false 40 - }, 41 - "state": { 42 - "name": "state", 43 - "type": "text", 44 - "primaryKey": false, 45 - "notNull": true, 46 - "autoincrement": false 47 - } 48 - }, 49 - "indexes": {}, 50 - "foreignKeys": {}, 51 - "compositePrimaryKeys": {}, 52 - "uniqueConstraints": {}, 53 - "checkConstraints": {} 54 - }, 55 - "follow": { 56 - "name": "follow", 57 - "columns": { 58 - "rel_id": { 59 - "name": "rel_id", 60 - "type": "text", 61 - "primaryKey": true, 62 - "notNull": true, 63 - "autoincrement": false 64 - }, 65 - "follower": { 66 - "name": "follower", 67 - "type": "text", 68 - "primaryKey": false, 69 - "notNull": true, 70 - "autoincrement": false 71 - }, 72 - "followed": { 73 - "name": "followed", 74 - "type": "text", 75 - "primaryKey": false, 76 - "notNull": true, 77 - "autoincrement": false 78 - }, 79 - "created_at": { 80 - "name": "created_at", 81 - "type": "text", 82 - "primaryKey": false, 83 - "notNull": true, 84 - "autoincrement": false 85 - } 86 - }, 87 - "indexes": {}, 88 - "foreignKeys": {}, 89 - "compositePrimaryKeys": {}, 90 - "uniqueConstraints": {}, 91 - "checkConstraints": {} 92 - }, 93 - "play": { 94 - "name": "play", 95 - "columns": { 96 - "rkey": { 97 - "name": "rkey", 98 - "type": "text", 99 - "primaryKey": true, 100 - "notNull": true, 101 - "autoincrement": false 102 - }, 103 - "author_did": { 104 - "name": "author_did", 105 - "type": "text", 106 - "primaryKey": false, 107 - "notNull": true, 108 - "autoincrement": false 109 - }, 110 - "created_at": { 111 - "name": "created_at", 112 - "type": "text", 113 - "primaryKey": false, 114 - "notNull": true, 115 - "autoincrement": false 116 - }, 117 - "indexed_at": { 118 - "name": "indexed_at", 119 - "type": "text", 120 - "primaryKey": false, 121 - "notNull": true, 122 - "autoincrement": false 123 - }, 124 - "track_name": { 125 - "name": "track_name", 126 - "type": "text", 127 - "primaryKey": false, 128 - "notNull": true, 129 - "autoincrement": false 130 - }, 131 - "track_mb_id": { 132 - "name": "track_mb_id", 133 - "type": "text", 134 - "primaryKey": false, 135 - "notNull": false, 136 - "autoincrement": false 137 - }, 138 - "recording_mb_id": { 139 - "name": "recording_mb_id", 140 - "type": "text", 141 - "primaryKey": false, 142 - "notNull": false, 143 - "autoincrement": false 144 - }, 145 - "duration": { 146 - "name": "duration", 147 - "type": "integer", 148 - "primaryKey": false, 149 - "notNull": false, 150 - "autoincrement": false 151 - }, 152 - "artist_names": { 153 - "name": "artist_names", 154 - "type": "text", 155 - "primaryKey": false, 156 - "notNull": false, 157 - "autoincrement": false 158 - }, 159 - "artist_mb_ids": { 160 - "name": "artist_mb_ids", 161 - "type": "text", 162 - "primaryKey": false, 163 - "notNull": false, 164 - "autoincrement": false 165 - }, 166 - "release_name": { 167 - "name": "release_name", 168 - "type": "text", 169 - "primaryKey": false, 170 - "notNull": false, 171 - "autoincrement": false 172 - }, 173 - "release_mb_id": { 174 - "name": "release_mb_id", 175 - "type": "text", 176 - "primaryKey": false, 177 - "notNull": false, 178 - "autoincrement": false 179 - }, 180 - "isrc": { 181 - "name": "isrc", 182 - "type": "text", 183 - "primaryKey": false, 184 - "notNull": false, 185 - "autoincrement": false 186 - }, 187 - "origin_url": { 188 - "name": "origin_url", 189 - "type": "text", 190 - "primaryKey": false, 191 - "notNull": false, 192 - "autoincrement": false 193 - }, 194 - "music_service_base_domain": { 195 - "name": "music_service_base_domain", 196 - "type": "text", 197 - "primaryKey": false, 198 - "notNull": false, 199 - "autoincrement": false 200 - }, 201 - "submission_client_agent": { 202 - "name": "submission_client_agent", 203 - "type": "text", 204 - "primaryKey": false, 205 - "notNull": false, 206 - "autoincrement": false 207 - }, 208 - "played_time": { 209 - "name": "played_time", 210 - "type": "text", 211 - "primaryKey": false, 212 - "notNull": false, 213 - "autoincrement": false 214 - } 215 - }, 216 - "indexes": {}, 217 - "foreignKeys": {}, 218 - "compositePrimaryKeys": {}, 219 - "uniqueConstraints": {}, 220 - "checkConstraints": {} 221 - }, 222 - "status": { 223 - "name": "status", 224 - "columns": { 225 - "uri": { 226 - "name": "uri", 227 - "type": "text", 228 - "primaryKey": true, 229 - "notNull": true, 230 - "autoincrement": false 231 - }, 232 - "author_did": { 233 - "name": "author_did", 234 - "type": "text", 235 - "primaryKey": false, 236 - "notNull": true, 237 - "autoincrement": false 238 - }, 239 - "status": { 240 - "name": "status", 241 - "type": "text", 242 - "primaryKey": false, 243 - "notNull": true, 244 - "autoincrement": false 245 - }, 246 - "created_at": { 247 - "name": "created_at", 248 - "type": "text", 249 - "primaryKey": false, 250 - "notNull": true, 251 - "autoincrement": false 252 - }, 253 - "indexed_at": { 254 - "name": "indexed_at", 255 - "type": "text", 256 - "primaryKey": false, 257 - "notNull": true, 258 - "autoincrement": false 259 - } 260 - }, 261 - "indexes": {}, 262 - "foreignKeys": {}, 263 - "compositePrimaryKeys": {}, 264 - "uniqueConstraints": {}, 265 - "checkConstraints": {} 266 - }, 267 - "teal_session": { 268 - "name": "teal_session", 269 - "columns": { 270 - "key": { 271 - "name": "key", 272 - "type": "text", 273 - "primaryKey": true, 274 - "notNull": true, 275 - "autoincrement": false 276 - }, 277 - "session": { 278 - "name": "session", 279 - "type": "text", 280 - "primaryKey": false, 281 - "notNull": true, 282 - "autoincrement": false 283 - }, 284 - "provider": { 285 - "name": "provider", 286 - "type": "text", 287 - "primaryKey": false, 288 - "notNull": true, 289 - "autoincrement": false 290 - } 291 - }, 292 - "indexes": {}, 293 - "foreignKeys": {}, 294 - "compositePrimaryKeys": {}, 295 - "uniqueConstraints": {}, 296 - "checkConstraints": {} 297 - }, 298 - "teal_user": { 299 - "name": "teal_user", 300 - "columns": { 301 - "did": { 302 - "name": "did", 303 - "type": "text", 304 - "primaryKey": true, 305 - "notNull": true, 306 - "autoincrement": false 307 - }, 308 - "handle": { 309 - "name": "handle", 310 - "type": "text", 311 - "primaryKey": false, 312 - "notNull": true, 313 - "autoincrement": false 314 - }, 315 - "avatar": { 316 - "name": "avatar", 317 - "type": "text", 318 - "primaryKey": false, 319 - "notNull": true, 320 - "autoincrement": false 321 - }, 322 - "bio": { 323 - "name": "bio", 324 - "type": "text", 325 - "primaryKey": false, 326 - "notNull": false, 327 - "autoincrement": false 328 - }, 329 - "created_at": { 330 - "name": "created_at", 331 - "type": "text", 332 - "primaryKey": false, 333 - "notNull": true, 334 - "autoincrement": false 335 - } 336 - }, 337 - "indexes": {}, 338 - "foreignKeys": {}, 339 - "compositePrimaryKeys": {}, 340 - "uniqueConstraints": {}, 341 - "checkConstraints": {} 342 - } 343 - }, 344 - "views": {}, 345 - "enums": {}, 346 - "_meta": { 347 - "schemas": {}, 348 - "tables": {}, 349 - "columns": { 350 - "\"play\".\"uri\"": "\"play\".\"rkey\"" 351 - } 352 - }, 353 - "internal": { 354 - "indexes": {} 355 - } 356 - }
+19 -33
packages/db/.drizzle/meta/_journal.json
··· 1 1 { 2 2 "version": "7", 3 - "dialect": "sqlite", 3 + "dialect": "postgresql", 4 4 "entries": [ 5 5 { 6 6 "idx": 0, 7 - "version": "6", 8 - "when": 1729467718506, 9 - "tag": "0000_same_maelstrom", 7 + "version": "7", 8 + "when": 1741812188376, 9 + "tag": "0000_perfect_war_machine", 10 10 "breakpoints": true 11 11 }, 12 12 { 13 13 "idx": 1, 14 - "version": "6", 15 - "when": 1730659321301, 16 - "tag": "0001_fresh_tana_nile", 14 + "version": "7", 15 + "when": 1741812893244, 16 + "tag": "0001_swift_maddog", 17 17 "breakpoints": true 18 18 }, 19 19 { 20 20 "idx": 2, 21 - "version": "6", 22 - "when": 1730659385717, 23 - "tag": "0002_moaning_roulette", 21 + "version": "7", 22 + "when": 1741816927440, 23 + "tag": "0002_stale_the_spike", 24 24 "breakpoints": true 25 25 }, 26 26 { 27 27 "idx": 3, 28 - "version": "6", 29 - "when": 1731093709171, 30 - "tag": "0003_sharp_medusa", 28 + "version": "7", 29 + "when": 1741927519447, 30 + "tag": "0003_worried_unicorn", 31 31 "breakpoints": true 32 32 }, 33 33 { 34 34 "idx": 4, 35 - "version": "6", 36 - "when": 1735101894454, 37 - "tag": "0004_exotic_ironclad", 35 + "version": "7", 36 + "when": 1741929813766, 37 + "tag": "0004_furry_gravity", 38 38 "breakpoints": true 39 39 }, 40 40 { 41 41 "idx": 5, 42 - "version": "6", 43 - "when": 1735497040757, 44 - "tag": "0005_conscious_johnny_blaze", 45 - "breakpoints": true 46 - }, 47 - { 48 - "idx": 6, 49 - "version": "6", 50 - "when": 1736372650928, 51 - "tag": "0006_supreme_hairball", 52 - "breakpoints": true 53 - }, 54 - { 55 - "idx": 7, 56 - "version": "6", 57 - "when": 1736663520095, 58 - "tag": "0007_demonic_toad", 42 + "version": "7", 43 + "when": 1742260281562, 44 + "tag": "0005_plain_vulture", 59 45 "breakpoints": true 60 46 } 61 47 ]
+14 -11
packages/db/connect.ts
··· 1 - import { drizzle } from "drizzle-orm/libsql"; 2 - import { createClient } from "@libsql/client"; 1 + import { drizzle } from "drizzle-orm/postgres-js"; 2 + import postgres from "postgres"; 3 3 import * as schema from "./schema"; 4 4 import process from "node:process"; 5 5 import path from "node:path"; 6 6 7 + /// Trim a password from a db connection url 8 + function withoutPassword(url: string) { 9 + const urlObj = new URL(url); 10 + urlObj.password = "*****"; 11 + return urlObj.toString(); 12 + } 13 + 7 14 console.log( 8 - "Loading SQLite file at", 9 - path.join(process.cwd(), "./../../db.sqlite"), 15 + "Connecting to database at " + 16 + withoutPassword(process.env.DATABASE_URL ?? ""), 10 17 ); 11 18 12 - const client = createClient({ 13 - url: 14 - process.env.DATABASE_URL ?? 15 - "file:" + path.join(process.cwd(), "./../../db.sqlite"), 16 - }); 19 + const client = postgres(process.env.DATABASE_URL ?? ""); 17 20 18 - export const db = drizzle(client, { 21 + export const db = drizzle({ 22 + client, 19 23 schema: schema, 20 - casing: "snake_case", 21 24 }); 22 25 23 26 // If you need to export the type:
+2 -2
packages/db/drizzle.config.ts
··· 1 1 import { defineConfig } from "drizzle-kit"; 2 2 3 3 export default defineConfig({ 4 - dialect: "sqlite", 4 + dialect: "postgresql", 5 5 schema: "./schema.ts", 6 6 out: "./.drizzle", 7 7 casing: "snake_case", 8 8 dbCredentials: { 9 - url: process.env.DATABASE_URL ?? "./../../db.sqlite", 9 + url: process.env.DATABASE_URL || "", 10 10 }, 11 11 });
+2 -1
packages/db/package.json
··· 14 14 "@libsql/client": "^0.14.0", 15 15 "@teal/tsconfig": "workspace:*", 16 16 "drizzle-kit": "^0.30.1", 17 - "drizzle-orm": "^0.38.3" 17 + "drizzle-orm": "^0.38.3", 18 + "postgres": "^3.4.5" 18 19 }, 19 20 "devDependencies": { 20 21 "@types/node": "^20.17.6"
+156 -85
packages/db/schema.ts
··· 1 + import { sql } from "drizzle-orm"; 1 2 import { 2 - numeric, 3 - sqliteTable, 3 + pgTable, 4 4 text, 5 - customType, 5 + pgEnum, 6 + timestamp, 7 + uuid, 6 8 integer, 7 - } from "drizzle-orm/sqlite-core"; 9 + jsonb, 10 + primaryKey, 11 + foreignKey, 12 + pgMaterializedView, 13 + } from "drizzle-orm/pg-core"; 14 + import { createDeflate } from "node:zlib"; 15 + 16 + export const artists = pgTable("artists", { 17 + mbid: uuid("mbid").primaryKey(), 18 + name: text("name").notNull(), 19 + playCount: integer("play_count").default(0), 20 + }); 21 + 22 + export const plays = pgTable("plays", { 23 + cid: text("cid").notNull(), 24 + did: text("did").notNull(), 25 + duration: integer("duration"), 26 + isrc: text("isrc"), 27 + musicServiceBaseDomain: text("music_service_base_domain"), 28 + originUrl: text("origin_url"), 29 + playedTime: timestamp("played_time", { withTimezone: true }), 30 + processedTime: timestamp("processed_time", { 31 + withTimezone: true, 32 + }).defaultNow(), 33 + rkey: text("rkey").notNull(), 34 + recordingMbid: uuid("recording_mbid").references(() => recordings.mbid), 35 + releaseMbid: uuid("release_mbid").references(() => releases.mbid), 36 + releaseName: text("release_name"), 37 + submissionClientAgent: text("submission_client_agent"), 38 + trackName: text("track_name").notNull(), 39 + uri: text("uri").primaryKey(), 40 + }); 8 41 9 - // string array custom type 10 - const json = <TData>() => 11 - customType<{ data: TData; driverData: string }>({ 12 - dataType() { 13 - return "text"; 14 - }, 15 - toDriver(value: TData): string { 16 - return JSON.stringify(value); 17 - }, 18 - // handle single value (no json array) as well 19 - fromDriver(value: string): TData { 20 - if (value[0] === "[") { 21 - return JSON.parse(value); 22 - } 23 - return [value] as TData; 24 - }, 25 - })(); 42 + export const playToArtists = pgTable( 43 + "play_to_artists", 44 + { 45 + artistMbid: uuid("artist_mbid") 46 + .references(() => artists.mbid) 47 + .notNull(), 48 + artistName: text("artist_name"), 49 + playUri: text("play_uri") 50 + .references(() => plays.uri) 51 + .notNull(), 52 + }, 53 + (table) => [primaryKey({ columns: [table.playUri, table.artistMbid] })], 54 + ); 26 55 27 - // Tables 56 + export const recordings = pgTable("recordings", { 57 + mbid: uuid("mbid").primaryKey(), 58 + name: text("name").notNull(), 59 + playCount: integer("play_count").default(0), 60 + }); 28 61 29 - export const status = sqliteTable("status", { 30 - uri: text().primaryKey(), 31 - authorDid: text().notNull(), 32 - status: text().notNull(), 33 - createdAt: text().notNull(), 34 - indexedAt: text().notNull(), 62 + export const releases = pgTable("releases", { 63 + mbid: uuid("mbid").primaryKey(), 64 + name: text("name").notNull(), 65 + playCount: integer("play_count").default(0), 35 66 }); 36 67 37 - // ATP Auth Tables (oAuth) 38 - export const atProtoSession = sqliteTable("atp_session", { 39 - key: text().primaryKey(), 40 - session: text().notNull(), 68 + export const mvArtistPlayCounts = pgMaterializedView( 69 + "mv_artist_play_counts", 70 + ).as((qb) => { 71 + return qb 72 + .select({ 73 + artistMbid: artists.mbid, 74 + artistName: artists.name, 75 + playCount: sql<number>`count(${plays.uri})`.as("play_count"), 76 + }) 77 + .from(artists) 78 + .leftJoin(playToArtists, sql`${artists.mbid} = ${playToArtists.artistMbid}`) 79 + .leftJoin(plays, sql`${plays.uri} = ${playToArtists.playUri}`) 80 + .groupBy(artists.mbid, artists.name); 41 81 }); 42 82 43 - export const authState = sqliteTable("auth_state", { 44 - key: text().primaryKey(), 45 - state: text().notNull(), 83 + export const mvGlobalPlayCount = pgMaterializedView("mv_global_play_count").as( 84 + (qb) => { 85 + return qb 86 + .select({ 87 + totalPlays: sql<number>`count(${plays.uri})`.as("total_plays"), 88 + uniqueListeners: sql<number>`count(distinct ${plays.did})`.as( 89 + "unique_listeners", 90 + ), 91 + }) 92 + .from(plays); 93 + }, 94 + ); 95 + 96 + export const mvRecordingPlayCounts = pgMaterializedView( 97 + "mv_recording_play_counts", 98 + ).as((qb) => { 99 + return qb 100 + .select({ 101 + recordingMbid: recordings.mbid, 102 + recordingName: recordings.name, 103 + playCount: sql<number>`count(${plays.uri})`.as("play_count"), 104 + }) 105 + .from(recordings) 106 + .leftJoin(plays, sql`${plays.recordingMbid} = ${recordings.mbid}`) 107 + .groupBy(recordings.mbid, recordings.name); 46 108 }); 47 109 48 - export const tealSession = sqliteTable("teal_session", { 49 - key: text().primaryKey(), 50 - session: text().notNull(), 51 - provider: text().notNull(), 110 + export const mvReleasePlayCounts = pgMaterializedView( 111 + "mv_release_play_counts", 112 + ).as((qb) => { 113 + return qb 114 + .select({ 115 + releaseMbid: releases.mbid, 116 + releaseName: releases.name, 117 + playCount: sql<number>`count(${plays.uri})`.as("play_count"), 118 + }) 119 + .from(releases) 120 + .leftJoin(plays, sql`${plays.releaseMbid} = ${releases.mbid}`) 121 + .groupBy(releases.mbid, releases.name); 52 122 }); 53 123 54 - // Regular Auth Tables 55 - export const tealUser = sqliteTable("teal_user", { 56 - did: text().primaryKey(), 57 - handle: text().notNull(), 58 - avatar: text().notNull(), 59 - bio: text(), 60 - createdAt: text().notNull(), 124 + export const mvTopArtists30Days = pgMaterializedView( 125 + "mv_top_artists_30days", 126 + ).as((qb) => { 127 + return qb 128 + .select({ 129 + artistMbid: artists.mbid, 130 + artistName: artists.name, 131 + playCount: sql<number>`count(${plays.uri})`.as("play_count"), 132 + }) 133 + .from(artists) 134 + .innerJoin( 135 + playToArtists, 136 + sql`${artists.mbid} = ${playToArtists.artistMbid}`, 137 + ) 138 + .innerJoin(plays, sql`${plays.uri} = ${playToArtists.playUri}`) 139 + .where(sql`${plays.playedTime} >= NOW() - INTERVAL '30 days'`) 140 + .groupBy(artists.mbid, artists.name) 141 + .orderBy(sql`count(${plays.uri}) DESC`); 61 142 }); 62 143 63 - // follow relationship 64 - export const follow = sqliteTable("follow", { 65 - relId: text().primaryKey(), 66 - follower: text().notNull(), 67 - followed: text().notNull(), 68 - createdAt: text().notNull(), 144 + export const mvTopReleases30Days = pgMaterializedView( 145 + "mv_top_releases_30days", 146 + ).as((qb) => { 147 + return qb 148 + .select({ 149 + releaseMbid: releases.mbid, 150 + releaseName: releases.name, 151 + playCount: sql<number>`count(${plays.uri})`.as("play_count"), 152 + }) 153 + .from(releases) 154 + .innerJoin(plays, sql`${plays.releaseMbid} = ${releases.mbid}`) 155 + .where(sql`${plays.playedTime} >= NOW() - INTERVAL '30 days'`) 156 + .groupBy(releases.mbid, releases.name) 157 + .orderBy(sql`count(${plays.uri}) DESC`); 69 158 }); 70 159 71 - // play 72 - export const play = sqliteTable("play", { 73 - rkey: text().primaryKey(), 74 - authorDid: text().notNull(), 75 - createdAt: text().notNull(), 76 - indexedAt: text().notNull(), 160 + export const profiles = pgTable("profiles", { 161 + did: text("did").primaryKey(), 162 + handle: text("handle"), 163 + displayName: text("display_name"), 164 + description: text("description"), 165 + descriptionFacets: jsonb("description_facets"), 166 + // the IPLD of the image. bafy... 167 + avatar: text("avatar"), 168 + banner: text("banner"), 169 + createdAt: timestamp("created_at", { withTimezone: true }), 170 + }); 77 171 78 - /** The name of the track */ 79 - trackName: text().notNull(), 80 - /** The Musicbrainz ID of the track */ 81 - trackMbId: text(), 82 - /** The Musicbrainz recording ID of the track */ 83 - recordingMbId: text(), 84 - /** The length of the track in seconds */ 85 - duration: integer(), 86 - /** The names of the artists in order of original appearance */ 87 - artistNames: json<string[]>(), 88 - /** Array of Musicbrainz artist IDs */ 89 - // type of string[] 90 - artistMbIds: json<string[]>(), 91 - /** The name of the release/album */ 92 - releaseName: text(), 93 - /** The Musicbrainz release ID */ 94 - releaseMbId: text(), 95 - /** The ISRC code associated with the recording */ 96 - isrc: text(), 97 - /** The URL associated with this track */ 98 - originUrl: text(), 99 - /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. */ 100 - musicServiceBaseDomain: text(), 101 - /** A user-agent style string specifying the user agent. e.g. fm.teal.frontend/0.0.1b */ 102 - submissionClientAgent: text(), 103 - /** The unix timestamp of when the track was played */ 104 - playedTime: text(), 172 + export const userFeaturedItems = pgTable("featured_items", { 173 + did: text("did").primaryKey(), 174 + mbid: text("mbid").notNull(), 175 + type: text("type").notNull(), 105 176 });
-19
packages/jetstring/package.json
··· 1 - { 2 - "name": "@teal/jetstring", 3 - "scripts": { 4 - "dev": "tsx watch src/index.ts | pino-pretty" 5 - }, 6 - "dependencies": { 7 - "@libsql/client": "^0.14.0", 8 - "@skyware/jetstream": "^0.2.0", 9 - "@teal/db": "workspace:*", 10 - "@teal/lexicons": "workspace:*", 11 - "@teal/tsconfig": "workspace:*", 12 - "pino-pretty": "^13.0.0" 13 - }, 14 - "devDependencies": { 15 - "tsup": "^8.3.5", 16 - "tsx": "^4.19.2", 17 - "typescript": "^5.6.3" 18 - } 19 - }
-191
packages/jetstring/src/index.ts
··· 1 - import type { Database } from "@teal/db/connect"; 2 - import { db } from "@teal/db/connect"; 3 - import { status, play } from "@teal/db/schema"; 4 - import { CommitCreateEvent, Jetstream } from "@skyware/jetstream"; 5 - 6 - import { 7 - Record as XyzStatusphereStatus, 8 - isRecord as isStatusphereStatus, 9 - } from "@teal/lexicons/src/types/xyz/statusphere/status"; 10 - 11 - import { 12 - Record as FmTealAlphaPlay, 13 - isRecord as isTealAlphaPlay, 14 - } from "@teal/lexicons/src/types/fm/teal/alpha/feed/play"; 15 - 16 - class Handler { 17 - private static instance: Handler; 18 - private constructor() {} 19 - public static getInstance(): Handler { 20 - if (!Handler.instance) { 21 - Handler.instance = new Handler(); 22 - } 23 - return Handler.instance; 24 - } 25 - 26 - handle(msg_type: string, record: CommitCreateEvent<string & {}>) { 27 - // Handle message logic here 28 - const msg = record.commit.record; 29 - console.log("Handling" + msg_type + "message:", msg); 30 - if (isStatusphereStatus(msg) && msg.$type === "xyz.statusphere.status") { 31 - if (record.commit.operation === "create") { 32 - // serialize message as xyz.statusphere.status 33 - db.insert(status) 34 - .values({ 35 - createdAt: new Date().getTime().toString(), 36 - indexedAt: new Date(record.time_us).getTime().toString(), 37 - status: msg.status, 38 - // the AT path 39 - uri: record.commit.rkey, 40 - authorDid: record.did, 41 - }) 42 - .execute(); 43 - } else { 44 - // TODO: sentry 45 - console.log( 46 - "unsupported operation for xyz.statusphere.status", 47 - record.commit.operation, 48 - ); 49 - } 50 - } else if (isTealAlphaPlay(msg) && msg.$type === "fm.teal.alpha.play") { 51 - if (record.commit.operation === "create") { 52 - // serialize message as fm.teal.alpha.play 53 - console.log(record.did); 54 - db.insert(play) 55 - .values({ 56 - createdAt: new Date().getTime().toString(), 57 - indexedAt: new Date(record.time_us).getTime().toString(), 58 - // the AT path 59 - rkey: record.commit.rkey, 60 - authorDid: record.did, 61 - artistNames: msg.artistNames, 62 - trackName: msg.trackName, 63 - artistMbIds: msg.artistMbIds || [], 64 - trackMbId: msg.trackMbId || "", 65 - duration: msg.duration || null, 66 - isrc: msg.isrc || null, 67 - musicServiceBaseDomain: msg.musicServiceBaseDomain || "local", 68 - originUrl: msg.originUrl || null, 69 - playedTime: msg.playedTime ? msg.playedTime.toString() : undefined, 70 - recordingMbId: msg.recordingMbId || null, 71 - releaseMbId: msg.releaseMbId || null, 72 - releaseName: msg.releaseName || null, 73 - submissionClientAgent: 74 - msg.submissionClientAgent || "manual/unknown", 75 - }) 76 - .execute(); 77 - } else { 78 - // TODO: sentry 79 - console.log( 80 - "unsupported operation for fm.teal.alpha.play", 81 - record.commit.operation, 82 - ); 83 - } 84 - } else { 85 - console.log("Unknown message type:", msg_type); 86 - console.log("Message:", record); 87 - } 88 - } 89 - } 90 - 91 - class Streamer { 92 - private static instance: Streamer; 93 - private jetstream: Jetstream; 94 - private handler: Handler; 95 - 96 - private wantedCollections: string[]; 97 - 98 - private constructor(wantedCollections: string[]) { 99 - this.handler = Handler.getInstance(); 100 - console.log("Creating new jetstream with collections", wantedCollections); 101 - this.jetstream = new Jetstream({ 102 - wantedCollections, 103 - }); 104 - this.wantedCollections = wantedCollections; 105 - } 106 - 107 - public static getInstance(wantedCollections?: string[]): Streamer { 108 - if (!Streamer.instance && wantedCollections) { 109 - Streamer.instance = new Streamer(wantedCollections); 110 - } else if (!Streamer.instance) { 111 - throw Error( 112 - "Wanted collections are required if instance does not exist!", 113 - ); 114 - } 115 - return Streamer.instance; 116 - } 117 - 118 - async setOnCreates() { 119 - for (const collection of this.wantedCollections) { 120 - await this.setOnCreate(collection); 121 - } 122 - } 123 - 124 - async setOnCreate(collection: string) { 125 - try { 126 - this.jetstream.onCreate(collection, (event) => { 127 - console.log("Received message:", event.commit.record); 128 - this.handleCreate(collection, event); 129 - }); 130 - } catch (error) { 131 - console.error("Error setting onCreate:", error); 132 - } 133 - console.log("Started onCreate stream for", collection); 134 - } 135 - 136 - async handleCreate( 137 - collection: string, 138 - event: CommitCreateEvent<string & {}>, 139 - ) { 140 - this.handler.handle(collection, event); 141 - } 142 - 143 - // Add method to start the streamer 144 - async start() { 145 - try { 146 - await this.setOnCreates(); 147 - this.jetstream.start(); 148 - console.log("Streamer started successfully"); 149 - } catch (error) { 150 - console.error("Error starting streamer:", error); 151 - } 152 - } 153 - } 154 - 155 - // Main function to run the application 156 - async function main() { 157 - try { 158 - const streamer = Streamer.getInstance([ 159 - "xyz.statusphere.status", 160 - "fm.teal.alpha.play", 161 - ]); 162 - await streamer.start(); 163 - 164 - // Keep the process running 165 - process.on("SIGINT", () => { 166 - console.log("Received SIGINT. Graceful shutdown..."); 167 - process.exit(0); 168 - }); 169 - 170 - process.on("SIGTERM", () => { 171 - console.log("Received SIGTERM. Graceful shutdown..."); 172 - process.exit(0); 173 - }); 174 - 175 - // Prevent the Node.js process from exiting 176 - setInterval(() => { 177 - // This empty interval keeps the process running 178 - }, 1000); 179 - 180 - console.log("Application is running. Press Ctrl+C to exit."); 181 - } catch (error) { 182 - console.error("Error in main:", error); 183 - process.exit(1); 184 - } 185 - } 186 - 187 - // Run the application 188 - main().catch((error) => { 189 - console.error("Unhandled error:", error); 190 - process.exit(1); 191 - });
-3
packages/jetstring/tsconfig.json
··· 1 - { 2 - "extends": "@teal/tsconfig/base.json" 3 - }
+60
packages/lexicons/real/fm/teal/alpha/actor/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.defs", 4 + "defs": { 5 + "profileView": { 6 + "type": "object", 7 + "properties": { 8 + "did": { 9 + "type": "string", 10 + "description": "The decentralized identifier of the actor" 11 + }, 12 + "displayName": { 13 + "type": "string" 14 + }, 15 + "description": { 16 + "type": "string", 17 + "description": "Free-form profile description text." 18 + }, 19 + "descriptionFacets": { 20 + "type": "array", 21 + "description": "Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon.", 22 + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } 23 + }, 24 + "featuredItem": { 25 + "type": "ref", 26 + "description": "The user's most recent item featured on their profile.", 27 + "ref": "fm.teal.alpha.actor.profile#featuredItem" 28 + }, 29 + "avatar": { 30 + "type": "string", 31 + "description": "IPLD of the avatar" 32 + }, 33 + "banner": { 34 + "type": "string", 35 + "description": "IPLD of the banner image" 36 + }, 37 + "createdAt": { "type": "string", "format": "datetime" } 38 + } 39 + }, 40 + "miniProfileView": { 41 + "type": "object", 42 + "properties": { 43 + "did": { 44 + "type": "string", 45 + "description": "The decentralized identifier of the actor" 46 + }, 47 + "displayName": { 48 + "type": "string" 49 + }, 50 + "handle": { 51 + "type": "string" 52 + }, 53 + "avatar": { 54 + "type": "string", 55 + "description": "IPLD of the avatar" 56 + } 57 + } 58 + } 59 + } 60 + }
+34
packages/lexicons/real/fm/teal/alpha/actor/getProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.getProfile", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actor"], 11 + "properties": { 12 + "actor": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "The author's DID" 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["actor"], 24 + "properties": { 25 + "actor": { 26 + "type": "ref", 27 + "ref": "fm.teal.alpha.actor.defs#profileView" 28 + } 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
+40
packages/lexicons/real/fm/teal/alpha/actor/getProfiles.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.getProfiles", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves the associated profile.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actors"], 11 + "properties": { 12 + "actors": { 13 + "type": "array", 14 + "items": { 15 + "type": "string", 16 + "format": "at-identifier" 17 + }, 18 + "description": "Array of actor DIDs" 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["actors"], 27 + "properties": { 28 + "actors": { 29 + "type": "array", 30 + "items": { 31 + "type": "ref", 32 + "ref": "fm.teal.alpha.actor.defs#miniProfileView" 33 + } 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + }
+52
packages/lexicons/real/fm/teal/alpha/actor/searchActors.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.searchActors", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Searches for actors based on profile contents.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["q"], 11 + "properties": { 12 + "q": { 13 + "type": "string", 14 + "description": "The search query", 15 + "maxGraphemes": 128, 16 + "maxLength": 640 17 + }, 18 + "limit": { 19 + "type": "integer", 20 + "description": "The maximum number of actors to return", 21 + "minimum": 1, 22 + "maximum": 25 23 + }, 24 + "cursor": { 25 + "type": "string", 26 + "description": "Cursor for pagination" 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": ["actors"], 35 + "properties": { 36 + "actors": { 37 + "type": "array", 38 + "items": { 39 + "type": "ref", 40 + "ref": "fm.teal.alpha.actor.defs#miniProfileView" 41 + } 42 + }, 43 + "cursor": { 44 + "type": "string", 45 + "description": "Cursor for pagination" 46 + } 47 + } 48 + } 49 + } 50 + } 51 + } 52 + }
+36
packages/lexicons/src/index.ts
··· 9 9 StreamAuthVerifier, 10 10 } from '@atproto/xrpc-server' 11 11 import { schemas } from './lexicons' 12 + import * as FmTealAlphaActorGetProfile from './types/fm/teal/alpha/actor/getProfile' 13 + import * as FmTealAlphaActorGetProfiles from './types/fm/teal/alpha/actor/getProfiles' 14 + import * as FmTealAlphaActorSearchActors from './types/fm/teal/alpha/actor/searchActors' 12 15 import * as FmTealAlphaFeedGetActorFeed from './types/fm/teal/alpha/feed/getActorFeed' 13 16 import * as FmTealAlphaFeedGetPlay from './types/fm/teal/alpha/feed/getPlay' 14 17 ··· 105 108 106 109 constructor(server: Server) { 107 110 this._server = server 111 + } 112 + 113 + getProfile<AV extends AuthVerifier>( 114 + cfg: ConfigOf< 115 + AV, 116 + FmTealAlphaActorGetProfile.Handler<ExtractAuth<AV>>, 117 + FmTealAlphaActorGetProfile.HandlerReqCtx<ExtractAuth<AV>> 118 + >, 119 + ) { 120 + const nsid = 'fm.teal.alpha.actor.getProfile' // @ts-ignore 121 + return this._server.xrpc.method(nsid, cfg) 122 + } 123 + 124 + getProfiles<AV extends AuthVerifier>( 125 + cfg: ConfigOf< 126 + AV, 127 + FmTealAlphaActorGetProfiles.Handler<ExtractAuth<AV>>, 128 + FmTealAlphaActorGetProfiles.HandlerReqCtx<ExtractAuth<AV>> 129 + >, 130 + ) { 131 + const nsid = 'fm.teal.alpha.actor.getProfiles' // @ts-ignore 132 + return this._server.xrpc.method(nsid, cfg) 133 + } 134 + 135 + searchActors<AV extends AuthVerifier>( 136 + cfg: ConfigOf< 137 + AV, 138 + FmTealAlphaActorSearchActors.Handler<ExtractAuth<AV>>, 139 + FmTealAlphaActorSearchActors.HandlerReqCtx<ExtractAuth<AV>> 140 + >, 141 + ) { 142 + const nsid = 'fm.teal.alpha.actor.searchActors' // @ts-ignore 143 + return this._server.xrpc.method(nsid, cfg) 108 144 } 109 145 } 110 146
+202 -1
packages/lexicons/src/lexicons.ts
··· 140 140 }, 141 141 }, 142 142 }, 143 + FmTealAlphaActorDefs: { 144 + lexicon: 1, 145 + id: 'fm.teal.alpha.actor.defs', 146 + defs: { 147 + profileView: { 148 + type: 'object', 149 + properties: { 150 + did: { 151 + type: 'string', 152 + description: 'The decentralized identifier of the actor', 153 + }, 154 + displayName: { 155 + type: 'string', 156 + }, 157 + description: { 158 + type: 'string', 159 + description: 'Free-form profile description text.', 160 + }, 161 + descriptionFacets: { 162 + type: 'array', 163 + description: 164 + 'Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon.', 165 + items: { 166 + type: 'ref', 167 + ref: 'lex:app.bsky.richtext.facet', 168 + }, 169 + }, 170 + featuredItem: { 171 + type: 'ref', 172 + description: 173 + "The user's most recent item featured on their profile.", 174 + ref: 'lex:fm.teal.alpha.actor.profile#featuredItem', 175 + }, 176 + avatar: { 177 + type: 'string', 178 + description: 'IPLD of the avatar', 179 + }, 180 + banner: { 181 + type: 'string', 182 + description: 'IPLD of the banner image', 183 + }, 184 + createdAt: { 185 + type: 'string', 186 + format: 'datetime', 187 + }, 188 + }, 189 + }, 190 + miniProfileView: { 191 + type: 'object', 192 + properties: { 193 + did: { 194 + type: 'string', 195 + description: 'The decentralized identifier of the actor', 196 + }, 197 + displayName: { 198 + type: 'string', 199 + }, 200 + handle: { 201 + type: 'string', 202 + }, 203 + avatar: { 204 + type: 'string', 205 + description: 'IPLD of the avatar', 206 + }, 207 + }, 208 + }, 209 + }, 210 + }, 211 + FmTealAlphaActorGetProfile: { 212 + lexicon: 1, 213 + id: 'fm.teal.alpha.actor.getProfile', 214 + description: 215 + 'This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.', 216 + defs: { 217 + main: { 218 + type: 'query', 219 + parameters: { 220 + type: 'params', 221 + required: ['actor'], 222 + properties: { 223 + actor: { 224 + type: 'string', 225 + format: 'at-identifier', 226 + description: "The author's DID", 227 + }, 228 + }, 229 + }, 230 + output: { 231 + encoding: 'application/json', 232 + schema: { 233 + type: 'object', 234 + required: ['actor'], 235 + properties: { 236 + actor: { 237 + type: 'ref', 238 + ref: 'lex:fm.teal.alpha.actor.defs#profileView', 239 + }, 240 + }, 241 + }, 242 + }, 243 + }, 244 + }, 245 + }, 246 + FmTealAlphaActorGetProfiles: { 247 + lexicon: 1, 248 + id: 'fm.teal.alpha.actor.getProfiles', 249 + description: 250 + 'This lexicon is in a not officially released state. It is subject to change. | Retrieves the associated profile.', 251 + defs: { 252 + main: { 253 + type: 'query', 254 + parameters: { 255 + type: 'params', 256 + required: ['actors'], 257 + properties: { 258 + actors: { 259 + type: 'array', 260 + items: { 261 + type: 'string', 262 + format: 'at-identifier', 263 + }, 264 + description: 'Array of actor DIDs', 265 + }, 266 + }, 267 + }, 268 + output: { 269 + encoding: 'application/json', 270 + schema: { 271 + type: 'object', 272 + required: ['actors'], 273 + properties: { 274 + actors: { 275 + type: 'array', 276 + items: { 277 + type: 'ref', 278 + ref: 'lex:fm.teal.alpha.actor.defs#miniProfileView', 279 + }, 280 + }, 281 + }, 282 + }, 283 + }, 284 + }, 285 + }, 286 + }, 143 287 FmTealAlphaActorProfile: { 144 288 lexicon: 1, 145 289 id: 'fm.teal.alpha.actor.profile', ··· 211 355 type: 'string', 212 356 description: 213 357 'The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc.', 358 + }, 359 + }, 360 + }, 361 + }, 362 + }, 363 + FmTealAlphaActorSearchActors: { 364 + lexicon: 1, 365 + id: 'fm.teal.alpha.actor.searchActors', 366 + description: 367 + 'This lexicon is in a not officially released state. It is subject to change. | Searches for actors based on profile contents.', 368 + defs: { 369 + main: { 370 + type: 'query', 371 + parameters: { 372 + type: 'params', 373 + required: ['q'], 374 + properties: { 375 + q: { 376 + type: 'string', 377 + description: 'The search query', 378 + maxGraphemes: 128, 379 + maxLength: 640, 380 + }, 381 + limit: { 382 + type: 'integer', 383 + description: 'The maximum number of actors to return', 384 + minimum: 1, 385 + maximum: 25, 386 + }, 387 + cursor: { 388 + type: 'string', 389 + description: 'Cursor for pagination', 390 + }, 391 + }, 392 + }, 393 + output: { 394 + encoding: 'application/json', 395 + schema: { 396 + type: 'object', 397 + required: ['actors'], 398 + properties: { 399 + actors: { 400 + type: 'array', 401 + items: { 402 + type: 'ref', 403 + ref: 'lex:fm.teal.alpha.actor.defs#miniProfileView', 404 + }, 405 + }, 406 + cursor: { 407 + type: 'string', 408 + description: 'Cursor for pagination', 409 + }, 410 + }, 214 411 }, 215 412 }, 216 413 }, ··· 493 690 maxLength: 256, 494 691 maxGraphemes: 2560, 495 692 description: 496 - "A metadata string specifying the user agent. e.g. com.example.frontend/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if unavailable or not provided.", 693 + "A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided.", 497 694 }, 498 695 playedTime: { 499 696 type: 'string', ··· 538 735 export const ids = { 539 736 AppBskyActorProfile: 'app.bsky.actor.profile', 540 737 AppBskyRichtextFacet: 'app.bsky.richtext.facet', 738 + FmTealAlphaActorDefs: 'fm.teal.alpha.actor.defs', 739 + FmTealAlphaActorGetProfile: 'fm.teal.alpha.actor.getProfile', 740 + FmTealAlphaActorGetProfiles: 'fm.teal.alpha.actor.getProfiles', 541 741 FmTealAlphaActorProfile: 'fm.teal.alpha.actor.profile', 742 + FmTealAlphaActorSearchActors: 'fm.teal.alpha.actor.searchActors', 542 743 FmTealAlphaActorStatus: 'fm.teal.alpha.actor.status', 543 744 FmTealAlphaFeedDefs: 'fm.teal.alpha.feed.defs', 544 745 FmTealAlphaFeedGetActorFeed: 'fm.teal.alpha.feed.getActorFeed',
+60
packages/lexicons/src/types/fm/teal/alpha/actor/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { lexicons } from '../../../../../lexicons' 6 + import { isObj, hasProp } from '../../../../../util' 7 + import { CID } from 'multiformats/cid' 8 + import * as AppBskyRichtextFacet from '../../../../app/bsky/richtext/facet' 9 + import * as FmTealAlphaActorProfile from './profile' 10 + 11 + export interface ProfileView { 12 + /** The decentralized identifier of the actor */ 13 + did?: string 14 + displayName?: string 15 + /** Free-form profile description text. */ 16 + description?: string 17 + /** Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon. */ 18 + descriptionFacets?: AppBskyRichtextFacet.Main[] 19 + featuredItem?: FmTealAlphaActorProfile.FeaturedItem 20 + /** IPLD of the avatar */ 21 + avatar?: string 22 + /** IPLD of the banner image */ 23 + banner?: string 24 + createdAt?: string 25 + [k: string]: unknown 26 + } 27 + 28 + export function isProfileView(v: unknown): v is ProfileView { 29 + return ( 30 + isObj(v) && 31 + hasProp(v, '$type') && 32 + v.$type === 'fm.teal.alpha.actor.defs#profileView' 33 + ) 34 + } 35 + 36 + export function validateProfileView(v: unknown): ValidationResult { 37 + return lexicons.validate('fm.teal.alpha.actor.defs#profileView', v) 38 + } 39 + 40 + export interface MiniProfileView { 41 + /** The decentralized identifier of the actor */ 42 + did?: string 43 + displayName?: string 44 + handle?: string 45 + /** IPLD of the avatar */ 46 + avatar?: string 47 + [k: string]: unknown 48 + } 49 + 50 + export function isMiniProfileView(v: unknown): v is MiniProfileView { 51 + return ( 52 + isObj(v) && 53 + hasProp(v, '$type') && 54 + v.$type === 'fm.teal.alpha.actor.defs#miniProfileView' 55 + ) 56 + } 57 + 58 + export function validateMiniProfileView(v: unknown): ValidationResult { 59 + return lexicons.validate('fm.teal.alpha.actor.defs#miniProfileView', v) 60 + }
+48
packages/lexicons/src/types/fm/teal/alpha/actor/getProfile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import express from 'express' 5 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 6 + import { lexicons } from '../../../../../lexicons' 7 + import { isObj, hasProp } from '../../../../../util' 8 + import { CID } from 'multiformats/cid' 9 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 10 + import * as FmTealAlphaActorDefs from './defs' 11 + 12 + export interface QueryParams { 13 + /** The author's DID */ 14 + actor: string 15 + } 16 + 17 + export type InputSchema = undefined 18 + 19 + export interface OutputSchema { 20 + actor: FmTealAlphaActorDefs.ProfileView 21 + [k: string]: unknown 22 + } 23 + 24 + export type HandlerInput = undefined 25 + 26 + export interface HandlerSuccess { 27 + encoding: 'application/json' 28 + body: OutputSchema 29 + headers?: { [key: string]: string } 30 + } 31 + 32 + export interface HandlerError { 33 + status: number 34 + message?: string 35 + } 36 + 37 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 38 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 39 + auth: HA 40 + params: QueryParams 41 + input: HandlerInput 42 + req: express.Request 43 + res: express.Response 44 + resetRouteRateLimits: () => Promise<void> 45 + } 46 + export type Handler<HA extends HandlerAuth = never> = ( 47 + ctx: HandlerReqCtx<HA>, 48 + ) => Promise<HandlerOutput> | HandlerOutput
+48
packages/lexicons/src/types/fm/teal/alpha/actor/getProfiles.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import express from 'express' 5 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 6 + import { lexicons } from '../../../../../lexicons' 7 + import { isObj, hasProp } from '../../../../../util' 8 + import { CID } from 'multiformats/cid' 9 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 10 + import * as FmTealAlphaActorDefs from './defs' 11 + 12 + export interface QueryParams { 13 + /** Array of actor DIDs */ 14 + actors: string[] 15 + } 16 + 17 + export type InputSchema = undefined 18 + 19 + export interface OutputSchema { 20 + actors: FmTealAlphaActorDefs.MiniProfileView[] 21 + [k: string]: unknown 22 + } 23 + 24 + export type HandlerInput = undefined 25 + 26 + export interface HandlerSuccess { 27 + encoding: 'application/json' 28 + body: OutputSchema 29 + headers?: { [key: string]: string } 30 + } 31 + 32 + export interface HandlerError { 33 + status: number 34 + message?: string 35 + } 36 + 37 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 38 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 39 + auth: HA 40 + params: QueryParams 41 + input: HandlerInput 42 + req: express.Request 43 + res: express.Response 44 + resetRouteRateLimits: () => Promise<void> 45 + } 46 + export type Handler<HA extends HandlerAuth = never> = ( 47 + ctx: HandlerReqCtx<HA>, 48 + ) => Promise<HandlerOutput> | HandlerOutput
+54
packages/lexicons/src/types/fm/teal/alpha/actor/searchActors.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import express from 'express' 5 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 6 + import { lexicons } from '../../../../../lexicons' 7 + import { isObj, hasProp } from '../../../../../util' 8 + import { CID } from 'multiformats/cid' 9 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 10 + import * as FmTealAlphaActorDefs from './defs' 11 + 12 + export interface QueryParams { 13 + /** The search query */ 14 + q: string 15 + /** The maximum number of actors to return */ 16 + limit?: number 17 + /** Cursor for pagination */ 18 + cursor?: string 19 + } 20 + 21 + export type InputSchema = undefined 22 + 23 + export interface OutputSchema { 24 + actors: FmTealAlphaActorDefs.MiniProfileView[] 25 + /** Cursor for pagination */ 26 + cursor?: string 27 + [k: string]: unknown 28 + } 29 + 30 + export type HandlerInput = undefined 31 + 32 + export interface HandlerSuccess { 33 + encoding: 'application/json' 34 + body: OutputSchema 35 + headers?: { [key: string]: string } 36 + } 37 + 38 + export interface HandlerError { 39 + status: number 40 + message?: string 41 + } 42 + 43 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 44 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 45 + auth: HA 46 + params: QueryParams 47 + input: HandlerInput 48 + req: express.Request 49 + res: express.Response 50 + resetRouteRateLimits: () => Promise<void> 51 + } 52 + export type Handler<HA extends HandlerAuth = never> = ( 53 + ctx: HandlerReqCtx<HA>, 54 + ) => Promise<HandlerOutput> | HandlerOutput
+1 -1
packages/lexicons/src/types/fm/teal/alpha/feed/play.ts
··· 29 29 originUrl?: string 30 30 /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided. */ 31 31 musicServiceBaseDomain?: string 32 - /** A metadata string specifying the user agent. e.g. com.example.frontend/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if unavailable or not provided. */ 32 + /** A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided. */ 33 33 submissionClientAgent?: string 34 34 /** The unix timestamp of when the track was played */ 35 35 playedTime?: string
+35 -65
pnpm-lock.yaml
··· 99 99 expo-font: 100 100 specifier: ~13.0.1 101 101 version: 13.0.1(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react@18.3.1) 102 + expo-image-picker: 103 + specifier: ^16.0.6 104 + version: 16.0.6(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)) 102 105 expo-linking: 103 106 specifier: ~7.0.3 104 107 version: 7.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) ··· 264 267 version: 16.4.7 265 268 drizzle-orm: 266 269 specifier: ^0.38.3 267 - version: 0.38.4(@libsql/client@0.14.0)(@types/react@18.3.12)(expo-sqlite@15.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react@18.3.1) 270 + version: 0.38.4(@libsql/client@0.14.0)(@types/react@18.3.12)(expo-sqlite@15.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(postgres@3.4.5)(react@18.3.1) 268 271 envalid: 269 272 specifier: ^8.0.0 270 273 version: 8.0.0 ··· 325 328 version: 0.30.2 326 329 drizzle-orm: 327 330 specifier: ^0.38.3 328 - version: 0.38.4(@libsql/client@0.14.0)(@types/react@18.3.12)(expo-sqlite@15.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react@18.3.1) 331 + version: 0.38.4(@libsql/client@0.14.0)(@types/react@18.3.12)(expo-sqlite@15.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(postgres@3.4.5)(react@18.3.1) 332 + postgres: 333 + specifier: ^3.4.5 334 + version: 3.4.5 329 335 devDependencies: 330 336 '@types/node': 331 337 specifier: ^20.17.6 332 338 version: 20.17.14 333 339 334 - packages/jetstring: 335 - dependencies: 336 - '@libsql/client': 337 - specifier: ^0.14.0 338 - version: 0.14.0 339 - '@skyware/jetstream': 340 - specifier: ^0.2.0 341 - version: 0.2.2(@atcute/client@2.0.7) 342 - '@teal/db': 343 - specifier: workspace:* 344 - version: link:../db 345 - '@teal/lexicons': 346 - specifier: workspace:* 347 - version: link:../lexicons 348 - '@teal/tsconfig': 349 - specifier: workspace:* 350 - version: link:../tsconfig 351 - pino-pretty: 352 - specifier: ^13.0.0 353 - version: 13.0.0 354 - devDependencies: 355 - tsup: 356 - specifier: ^8.3.5 357 - version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.6.1) 358 - tsx: 359 - specifier: ^4.19.2 360 - version: 4.19.2 361 - typescript: 362 - specifier: ^5.6.3 363 - version: 5.7.3 364 - 365 340 packages/lexicons: 366 341 dependencies: 367 342 '@atproto/lex-cli': ··· 401 376 402 377 '@aquareum/atproto-oauth-client-react-native@0.0.1': 403 378 resolution: {integrity: sha512-IoIcUuX2rKs/AS2u+72m9UWc0mldPTR4GgBHn4LIWtHLWjGTGdECwkw6iwshCM39KA15UhKGbByNQRG415hyfQ==} 404 - 405 - '@atcute/bluesky@1.0.12': 406 - resolution: {integrity: sha512-oUM+MxD5asGYyQDOHBGay7f9ryhsBpQ8LTUmsEZvp4t/WG0ZV2AcFRWsG0DxB+CsmSTbP2DHLMZCatE3usmt+g==} 407 - peerDependencies: 408 - '@atcute/client': ^1.0.0 || ^2.0.0 409 - 410 - '@atcute/client@2.0.7': 411 - resolution: {integrity: sha512-bvNahrCGvhZw/EIx0HU/GOoKZEnUaAppbuZh7cu+VsOFA2tdFLnZJed9Hagh5Yz/eUX7QUh5NB4dRTRUdggSLQ==} 412 379 413 380 '@atproto-labs/did-resolver@0.1.5': 414 381 resolution: {integrity: sha512-uoCb+P0N4du5NiZt6ohVEbSDdijXBJlQwSlWLHX0rUDtEVV+g3aEGe7jUW94lWpqQmRlQ5xcyd9owleMibNxZw==} ··· 2922 2889 '@sinonjs/fake-timers@10.3.0': 2923 2890 resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} 2924 2891 2925 - '@skyware/jetstream@0.2.2': 2926 - resolution: {integrity: sha512-d1MtWPTIFEciSzV8OClXZCJoz0DJ7aupt4EZSwpGAASYG0ZIPmZTt7RVJkoFzQyqRPHAMD7CvEwu0ut3MHX1og==} 2927 - 2928 2892 '@ts-morph/common@0.17.0': 2929 2893 resolution: {integrity: sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==} 2930 2894 ··· 4470 4434 peerDependencies: 4471 4435 expo: '*' 4472 4436 react: '*' 4437 + 4438 + expo-image-loader@5.0.0: 4439 + resolution: {integrity: sha512-Eg+5FHtyzv3Jjw9dHwu2pWy4xjf8fu3V0Asyy42kO+t/FbvW/vjUixpTjPtgKQLQh+2/9Nk4JjFDV6FwCnF2ZA==} 4440 + peerDependencies: 4441 + expo: '*' 4442 + 4443 + expo-image-picker@16.0.6: 4444 + resolution: {integrity: sha512-HN4xZirFjsFDIsWFb12AZh19fRzuvZjj2ll17cGr19VNRP06S/VPQU3Tdccn5vwUzQhOBlLu704CnNm278boiQ==} 4445 + peerDependencies: 4446 + expo: '*' 4473 4447 4474 4448 expo-keep-awake@14.0.2: 4475 4449 resolution: {integrity: sha512-71XAMnoWjKZrN8J7Q3+u0l9Ytp4OfhNAYz8BCWF1/9aFUw09J3I7Z5DuI3MUsVMa/KWi+XhG+eDUFP8cVA19Uw==} ··· 6052 6026 resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 6053 6027 engines: {node: '>= 0.8'} 6054 6028 6055 - partysocket@1.0.3: 6056 - resolution: {integrity: sha512-7sSojS4oCRK1Fe1h+Sa0Za5dwOf+M9VksQlynD8yqwGpLvnO4oxx9ppmOSeh6CJTMbF5gbnvUQKMK525QSBdBw==} 6057 - 6058 6029 password-prompt@1.1.3: 6059 6030 resolution: {integrity: sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==} 6060 6031 ··· 6231 6202 postcss@8.4.49: 6232 6203 resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} 6233 6204 engines: {node: ^10 || ^12 || >=14} 6205 + 6206 + postgres@3.4.5: 6207 + resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} 6208 + engines: {node: '>=12'} 6234 6209 6235 6210 prelude-ls@1.2.1: 6236 6211 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} ··· 7774 7749 - expo 7775 7750 - react 7776 7751 - react-native 7777 - 7778 - '@atcute/bluesky@1.0.12(@atcute/client@2.0.7)': 7779 - dependencies: 7780 - '@atcute/client': 2.0.7 7781 - 7782 - '@atcute/client@2.0.7': {} 7783 7752 7784 7753 '@atproto-labs/did-resolver@0.1.5': 7785 7754 dependencies: ··· 10757 10726 dependencies: 10758 10727 '@sinonjs/commons': 3.0.1 10759 10728 10760 - '@skyware/jetstream@0.2.2(@atcute/client@2.0.7)': 10761 - dependencies: 10762 - '@atcute/bluesky': 1.0.12(@atcute/client@2.0.7) 10763 - partysocket: 1.0.3 10764 - transitivePeerDependencies: 10765 - - '@atcute/client' 10766 - 10767 10729 '@ts-morph/common@0.17.0': 10768 10730 dependencies: 10769 10731 fast-glob: 3.3.2 ··· 12016 11978 transitivePeerDependencies: 12017 11979 - supports-color 12018 11980 12019 - drizzle-orm@0.38.4(@libsql/client@0.14.0)(@types/react@18.3.12)(expo-sqlite@15.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react@18.3.1): 11981 + drizzle-orm@0.38.4(@libsql/client@0.14.0)(@types/react@18.3.12)(expo-sqlite@15.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(postgres@3.4.5)(react@18.3.1): 12020 11982 optionalDependencies: 12021 11983 '@libsql/client': 0.14.0 12022 11984 '@types/react': 18.3.12 12023 11985 expo-sqlite: 15.0.3(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) 11986 + postgres: 3.4.5 12024 11987 react: 18.3.1 12025 11988 12026 11989 dunder-proto@1.0.0: ··· 12629 12592 react: 18.3.1 12630 12593 optional: true 12631 12594 12595 + expo-image-loader@5.0.0(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)): 12596 + dependencies: 12597 + expo: 52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) 12598 + 12599 + expo-image-picker@16.0.6(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)): 12600 + dependencies: 12601 + expo: 52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) 12602 + expo-image-loader: 5.0.0(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)) 12603 + 12632 12604 expo-keep-awake@14.0.2(expo@52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react@18.3.1): 12633 12605 dependencies: 12634 12606 expo: 52.0.27(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) ··· 14459 14431 14460 14432 parseurl@1.3.3: {} 14461 14433 14462 - partysocket@1.0.3: 14463 - dependencies: 14464 - event-target-shim: 6.0.2 14465 - 14466 14434 password-prompt@1.1.3: 14467 14435 dependencies: 14468 14436 ansi-escapes: 4.3.2 ··· 14633 14601 nanoid: 3.3.8 14634 14602 picocolors: 1.1.1 14635 14603 source-map-js: 1.2.1 14604 + 14605 + postgres@3.4.5: {} 14636 14606 14637 14607 prelude-ls@1.2.1: {} 14638 14608