···11-import { create, StateCreator as ZustandStateCreator } from "zustand";
22-import { persist, createJSONStorage } from "zustand/middleware";
11+import { create, StateCreator as ZustandStateCreator } from 'zustand';
22+import { persist, createJSONStorage } from 'zustand/middleware';
33import {
44 AuthenticationSlice,
55 createAuthenticationSlice,
66-} from "./authenticationSlice";
77-import AsyncStorage from "@react-native-async-storage/async-storage";
88-import { createTempSlice, TempSlice } from "./tempSlice";
99-66+} from './authenticationSlice';
77+import AsyncStorage from '@react-native-async-storage/async-storage';
88+import { createTempSlice, TempSlice } from './tempSlice';
99+import { createPreferenceSlice, PreferenceSlice } from './preferenceSlice';
10101111/// Put all your non-shared slices here
1212-export type Slices = AuthenticationSlice & TempSlice;
1212+export type Slices = AuthenticationSlice & TempSlice & PreferenceSlice;
1313/// Put all your shared slices here
1414export type PlusSharedSlices = Slices;
1515/// Convenience type for creating a store. Uses the slices type defined above.
1616-/// Type parameter T is the type of the state object.
1616+/// Type parameter T is the type of the state object.
1717export type StateCreator<T> = ZustandStateCreator<Slices, [], [], T>;
18181919export const useStore = create<PlusSharedSlices>()(
···2121 (...a) => ({
2222 ...createAuthenticationSlice(...a),
2323 ...createTempSlice(...a),
2424+ ...createPreferenceSlice(...a),
2425 }),
2526 {
2627 partialize: ({ pdsAgent, isAgentReady, ...state }) => state,
27282829 onRehydrateStorage: () => (state) => {
2929- state?.restorePdsAgent()
3030+ state?.restorePdsAgent();
3031 },
3131- name: "mainStore",
3232+ name: 'mainStore',
3233 storage: createJSONStorage(() => AsyncStorage),
3334 },
3435 ),
···11+import { TealContext } from '@/ctx';
22+import { db, profiles } from '@teal/db';
33+import { eq } from 'drizzle-orm';
44+import { OutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile';
55+66+export default async function getProfile(c: TealContext) {
77+ const params = c.req.query();
88+ if (!params.actor) {
99+ throw new Error('actor is required');
1010+ }
1111+1212+ // Assuming 'user' can be either a DID or a handle. We'll try to resolve
1313+ // the DID first, and if that fails, try to resolve the handle.
1414+ let profile;
1515+1616+ //First try to get by did
1717+ profile = await db
1818+ .select()
1919+ .from(profiles)
2020+ .where(eq(profiles.did, params.actor))
2121+ .limit(1);
2222+2323+ //If not found, try to get by handle
2424+ if (!profile) {
2525+ profile = await db
2626+ .select()
2727+ .from(profiles)
2828+ .where(eq(profiles.handle, params.actor))
2929+ .limit(1);
3030+ }
3131+3232+ if (!profile) {
3333+ throw new Error('Profile not found');
3434+ }
3535+3636+ profile = profile[0];
3737+3838+ const res: OutputSchema = {
3939+ actor: {
4040+ did: profile.did,
4141+ handle: profile.handle,
4242+ displayName: profile.displayName || undefined,
4343+ description: profile.description || undefined,
4444+ descriptionFacets: [],
4545+ avatar: profile.avatar || undefined,
4646+ banner: profile.banner || undefined,
4747+ createdAt: profile.createdAt?.toISOString(),
4848+ },
4949+ };
5050+5151+ return res;
5252+}
+98
apps/aqua/src/xrpc/actor/searchActors.ts
···11+import { TealContext } from '@/ctx';
22+import { db, profiles } from '@teal/db';
33+import { like, or, sql, lt, gt, and } from 'drizzle-orm';
44+import { OutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/searchActors';
55+66+export default async function searchActors(c: TealContext) {
77+ const params = c.req.query();
88+ const limit = (params.limit ? parseInt(params.limit) : 25) || 25; // Ensure limit is a number
99+ const requestedLimit = limit + 1; // Fetch one extra for cursor detection
1010+1111+ if (!params.q) {
1212+ c.status(400);
1313+ c.error = new Error('q is required');
1414+ return;
1515+ }
1616+1717+ const query = params.q.toLowerCase();
1818+1919+ try {
2020+ let queryBuilder = db
2121+ .select({
2222+ did: profiles.did,
2323+ handle: profiles.handle,
2424+ displayName: profiles.displayName,
2525+ description: profiles.description,
2626+ avatar: profiles.avatar,
2727+ banner: profiles.banner,
2828+ createdAt: profiles.createdAt,
2929+ })
3030+ .from(profiles);
3131+3232+ // Base WHERE clause (always applied)
3333+ const baseWhere = or(
3434+ like(sql`lower(${profiles.handle})`, `%${query}%`),
3535+ like(sql`lower(${profiles.displayName})`, `%${query}%`),
3636+ like(sql`lower(${profiles.description})`, `%${query}%`),
3737+ sql`${profiles.handle} = ${params.q}`,
3838+ sql`${profiles.displayName} = ${params.q}`,
3939+ );
4040+4141+ if (params.cursor) {
4242+ // Decode the cursor
4343+ const [createdAtStr, didStr] = Buffer.from(params.cursor, 'base64')
4444+ .toString('utf-8')
4545+ .split(':');
4646+4747+ const createdAt = new Date(createdAtStr);
4848+4949+ // Cursor condition: (createdAt > cursor.createdAt) OR (createdAt == cursor.createdAt AND did > cursor.did)
5050+ queryBuilder.where(
5151+ and(
5252+ baseWhere, // Apply the base search terms
5353+ or(
5454+ gt(profiles.createdAt, createdAt),
5555+ and(
5656+ sql`${profiles.createdAt} = ${createdAt}`,
5757+ gt(profiles.did, didStr), // Compare did as string
5858+ ),
5959+ ),
6060+ ),
6161+ );
6262+ } else {
6363+ queryBuilder.where(baseWhere); // Just the base search if no cursor
6464+ }
6565+6666+ queryBuilder
6767+ .orderBy(profiles.createdAt, profiles.did)
6868+ .limit(requestedLimit); // Order by both, limit + 1
6969+7070+ const results = await queryBuilder;
7171+7272+ // Build the next cursor (if there are more results)
7373+ let nextCursor = null;
7474+ if (results.length > limit) {
7575+ const lastResult = results[limit - 1]; // Get the *limit*-th, not limit+1-th
7676+ nextCursor = Buffer.from(
7777+ `${lastResult.createdAt?.toISOString() || ''}:${lastResult.did}`,
7878+ ).toString('base64');
7979+ results.pop(); // Remove the extra record we fetched
8080+ }
8181+ const res: OutputSchema = {
8282+ actors: results.map((profile) => ({
8383+ did: profile.did,
8484+ handle: profile.handle ?? undefined,
8585+ displayName: profile.displayName ?? undefined,
8686+ avatar: profile.avatar ?? undefined,
8787+ banner: profile.banner ?? undefined,
8888+ })),
8989+ cursor: nextCursor || undefined,
9090+ };
9191+9292+ return res;
9393+ } catch (error) {
9494+ console.error('Database error:', error);
9595+ c.status(500);
9696+ throw new Error('Internal server error');
9797+ }
9898+}
+102-53
apps/aqua/src/xrpc/feed/getActorFeed.ts
···11-import { TealContext } from "@/ctx";
22-import { db, tealSession, play } from "@teal/db";
33-import { eq, and, lt } from "drizzle-orm";
44-import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
11+import { TealContext } from '@/ctx';
22+import { artists, db, plays, playToArtists } from '@teal/db';
33+import { eq, and, lt, desc, sql } from 'drizzle-orm';
44+import { OutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed';
5566export default async function getActorFeed(c: TealContext) {
77 const params = c.req.query();
88- if (!params.authorDid) {
99- throw new Error("authorDid is required");
88+ if (!params.authorDID) {
99+ throw new Error('authorDID is required');
1010 }
11111212- let limit = 20
1212+ let limit = 20;
13131414- if(params.limit) {
1515- limit = Number(params.limit)
1616- if(limit > 50) throw new Error("Limit is over max allowed.")
1414+ if (params.limit) {
1515+ limit = Number(params.limit);
1616+ if (limit > 50) throw new Error('Limit is over max allowed.');
1717 }
18181919 // 'and' is here for typing reasons
2020- let whereClause = and(eq(play.authorDid, params.authorDid));
2020+ let whereClause = and(eq(plays.did, params.authorDID));
21212222 // Add cursor pagination if provided
2323 if (params.cursor) {
2424- const [cursorPlay] = await db
2525- .select({ createdAt: play.createdAt })
2626- .from(play)
2727- .where(eq(play.uri, params.cursor))
2424+ const cursorResult = await db
2525+ .select()
2626+ .from(plays)
2727+ .where(eq(plays.uri, params.cursor))
2828 .limit(1);
29293030+ const cursorPlay = cursorResult[0]?.playedTime;
3131+3032 if (!cursorPlay) {
3131- throw new Error("Cursor not found");
3333+ throw new Error('Cursor not found');
3234 }
33353434- whereClause = and(whereClause, lt(play.createdAt, cursorPlay.createdAt));
3636+ whereClause = and(whereClause, lt(plays.playedTime, cursorPlay as any));
3537 }
36383737- const plays = await db
3838- .select()
3939- .from(play)
3939+ const playRes = await db
4040+ .select({
4141+ uri: plays.uri,
4242+ did: plays.did,
4343+ playedTime: plays.playedTime,
4444+ trackName: plays.trackName,
4545+ cid: plays.cid,
4646+ recordingMbid: plays.recordingMbid,
4747+ duration: plays.duration,
4848+ releaseName: plays.releaseName,
4949+ releaseMbid: plays.releaseMbid,
5050+ isrc: plays.isrc,
5151+ originUrl: plays.originUrl,
5252+ processedTime: plays.processedTime,
5353+ submissionClientAgent: plays.submissionClientAgent,
5454+ musicServiceBaseDomain: plays.musicServiceBaseDomain,
5555+ artists: sql<Array<{ mbid: string; name: string }>>`
5656+ COALESCE(
5757+ (
5858+ SELECT jsonb_agg(jsonb_build_object('mbid', pa.artist_mbid, 'name', pa.artist_name))
5959+ FROM ${playToArtists} pa
6060+ WHERE pa.play_uri = ${plays.uri}
6161+ AND pa.artist_mbid IS NOT NULL
6262+ AND pa.artist_name IS NOT NULL -- Ensure both are non-null
6363+ ),
6464+ '[]'::jsonb -- Correct empty JSONB array literal
6565+ )`.as('artists'),
6666+ })
6767+ .from(plays)
6868+ .leftJoin(playToArtists, sql`${plays.uri} = ${playToArtists.playUri}`)
4069 .where(whereClause)
4141- .orderBy(play.createdAt)
4242- .limit(10);
4343-4444- if (plays.length === 0) {
4545- throw new Error("Play not found");
4646- }
7070+ .groupBy(
7171+ plays.uri,
7272+ plays.cid,
7373+ plays.did,
7474+ plays.duration,
7575+ plays.isrc,
7676+ plays.musicServiceBaseDomain,
7777+ plays.originUrl,
7878+ plays.playedTime,
7979+ plays.processedTime,
8080+ plays.rkey,
8181+ plays.recordingMbid,
8282+ plays.releaseMbid,
8383+ plays.releaseName,
8484+ plays.submissionClientAgent,
8585+ plays.trackName,
8686+ )
8787+ .orderBy(desc(plays.playedTime))
8888+ .limit(limit);
8989+ const cursor =
9090+ playRes.length === limit ? playRes[playRes.length - 1]?.uri : undefined;
47914892 return {
4949- plays: plays.map(
9393+ cursor: cursor ?? undefined, // Ensure cursor itself can be undefined
9494+ plays: playRes.map(
5095 ({
5151- uri,
5252- authorDid,
5353- createdAt,
5454- indexedAt,
9696+ // Destructure fields from the DB result
5597 trackName,
5656- trackMbId,
5757- recordingMbId,
9898+ cid: trackMbId, // Note the alias was used here in the DB query select
9999+ recordingMbid,
58100 duration,
5959- artistNames,
6060- artistMbIds,
101101+ artists, // This is guaranteed to be an array '[]' if no artists, due to COALESCE
61102 releaseName,
6262- releaseMbId,
103103+ releaseMbid,
63104 isrc,
64105 originUrl,
65106 musicServiceBaseDomain,
66107 submissionClientAgent,
67108 playedTime,
109109+ // Other destructured fields like uri, did, etc. are not directly used here by name
68110 }) => ({
6969- uri,
7070- authorDid,
7171- createdAt,
7272- indexedAt,
7373- trackName,
7474- trackMbId,
7575- recordingMbId,
7676- duration,
7777- artistNames,
7878- artistMbIds,
7979- releaseName,
8080- releaseMbId,
8181- isrc,
8282- originUrl,
8383- musicServiceBaseDomain,
8484- submissionClientAgent,
8585- playedTime,
111111+ // Apply '?? undefined' to each potentially nullable/undefined scalar field
112112+ trackName: trackName ?? undefined,
113113+ trackMbId: trackMbId ?? undefined,
114114+ recordingMbId: recordingMbid ?? undefined,
115115+ duration: duration ?? undefined,
116116+117117+ // For arrays derived from a guaranteed array, map is safe.
118118+ // The SQL query ensures `artists` is '[]'::jsonb if empty.
119119+ // The SQL query also ensures artist.name/mbid are NOT NULL within the jsonb_agg
120120+ artistNames: artists.map((artist) => artist.name), // Will be [] if artists is []
121121+ artistMbIds: artists.map((artist) => artist.mbid), // Will be [] if artists is []
122122+123123+ releaseName: releaseName ?? undefined,
124124+ releaseMbId: releaseMbid ?? undefined,
125125+ isrc: isrc ?? undefined,
126126+ originUrl: originUrl ?? undefined,
127127+ musicServiceBaseDomain: musicServiceBaseDomain ?? undefined,
128128+ submissionClientAgent: submissionClientAgent ?? undefined,
129129+130130+ // playedTime specific handling: convert to ISO string if exists, else undefined
131131+ playedTime: playedTime ? playedTime.toISOString() : undefined,
132132+ // Alternative using optional chaining (effectively the same)
133133+ // playedTime: playedTime?.toISOString(),
86134 }),
87135 ),
136136+ // Explicitly cast to OutputSchema. Make sure OutputSchema allows undefined for these fields.
88137 } as OutputSchema;
89138}
+89-34
apps/aqua/src/xrpc/feed/getPlay.ts
···11import { TealContext } from "@/ctx";
22-import { db, tealSession, play } from "@teal/db";
33-import { eq, and } from "drizzle-orm";
44-import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getPlay";
22+import { db, plays, playToArtists, artists } from "@teal/db";
33+import { eq, and, lt, desc, sql } from "drizzle-orm";
44+import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
5566-export default async function getPlay(c: TealContext) {
77- // do we have required query params?
66+export default async function getActorFeed(c: TealContext) {
87 const params = c.req.query();
99- if (params.authorDid === undefined) {
88+ if (!params.authorDid) {
109 throw new Error("authorDid is required");
1110 }
1211 if (!params.rkey) {
1312 throw new Error("rkey is required");
1413 }
15141616- let res = await db
1717- .select()
1818- .from(play)
1919- .where(
2020- and(eq(play.authorDid, params.authorDid), and(eq(play.uri, params.rkey))),
1515+ // Get plays with artists as arrays
1616+ const playRes = await db
1717+ .select({
1818+ play: plays,
1919+ artists: sql<Array<{ mbid: string; name: string }>>`
2020+ COALESCE(
2121+ array_agg(
2222+ CASE WHEN ${artists.mbid} IS NOT NULL THEN
2323+ jsonb_build_object(
2424+ 'mbid', ${artists.mbid},
2525+ 'name', ${artists.name}
2626+ )
2727+ END
2828+ ) FILTER (WHERE ${artists.mbid} IS NOT NULL),
2929+ ARRAY[]::jsonb[]
3030+ )
3131+ `.as("artists"),
3232+ })
3333+ .from(plays)
3434+ .leftJoin(playToArtists, sql`${plays.uri} = ${playToArtists.playUri}`)
3535+ .leftJoin(artists, sql`${playToArtists.artistMbid} = ${artists.mbid}`)
3636+ .where(and(eq(plays.did, params.authorDid), eq(plays.rkey, params.rkey)))
3737+ .groupBy(
3838+ plays.uri,
3939+ plays.cid,
4040+ plays.did,
4141+ plays.duration,
4242+ plays.isrc,
4343+ plays.musicServiceBaseDomain,
4444+ plays.originUrl,
4545+ plays.playedTime,
4646+ plays.processedTime,
4747+ plays.rkey,
4848+ plays.recordingMbid,
4949+ plays.releaseMbid,
5050+ plays.releaseName,
5151+ plays.submissionClientAgent,
5252+ plays.trackName,
2153 )
2222- .execute();
5454+ .orderBy(desc(plays.playedTime))
5555+ .limit(1);
23562424- if (res.length === 0) {
5757+ if (playRes.length === 0) {
2558 throw new Error("Play not found");
2659 }
2727- res[0];
28602929- // return a PlayView
3061 return {
3131- play: {
3232- uri: res[0].uri,
3333- authorDid: res[0].authorDid,
3434- createdAt: res[0].createdAt,
3535- indexedAt: res[0].indexedAt,
3636- trackName: res[0].trackName,
3737- trackMbId: res[0].trackMbId,
3838- recordingMbId: res[0].recordingMbId,
3939- duration: res[0].duration,
4040- artistNames: res[0].artistNames,
4141- artistMbIds: res[0].artistMbIds,
4242- releaseName: res[0].releaseName,
4343- releaseMbId: res[0].releaseMbId,
4444- isrc: res[0].isrc,
4545- originUrl: res[0].originUrl,
4646- musicServiceBaseDomain: res[0].musicServiceBaseDomain,
4747- submissionClientAgent: res[0].submissionClientAgent,
4848- playedTime: res[0].playedTime,
4949- },
6262+ plays: playRes.map(({ play, artists }) => {
6363+ const {
6464+ uri,
6565+ did: authorDid,
6666+ processedTime: createdAt,
6767+ processedTime: indexedAt,
6868+ trackName,
6969+ cid: trackMbId,
7070+ cid: recordingMbId,
7171+ duration,
7272+ rkey,
7373+ releaseName,
7474+ cid: releaseMbId,
7575+ isrc,
7676+ originUrl,
7777+ musicServiceBaseDomain,
7878+ submissionClientAgent,
7979+ playedTime,
8080+ } = play;
8181+8282+ return {
8383+ uri,
8484+ authorDid,
8585+ createdAt: createdAt?.toISOString(),
8686+ indexedAt: indexedAt?.toISOString(),
8787+ trackName,
8888+ trackMbId,
8989+ recordingMbId,
9090+ duration,
9191+ // Replace these with actual artist data from the array
9292+ artistNames: artists.map((artist) => artist.name),
9393+ artistMbIds: artists.map((artist) => artist.mbid),
9494+ // Or, if you want to keep the full artist objects:
9595+ // artists: artists,
9696+ releaseName,
9797+ releaseMbId,
9898+ isrc,
9999+ originUrl,
100100+ musicServiceBaseDomain,
101101+ submissionClientAgent,
102102+ playedTime: playedTime?.toISOString(),
103103+ };
104104+ }),
50105 } as OutputSchema;
51106}
+16-6
apps/aqua/src/xrpc/route.ts
···11-import { EnvWithCtx } from "@/ctx";
22-import { Hono } from "hono";
33-import getPlay from "./feed/getPlay";
44-import getActorFeed from "./feed/getActorFeed";
11+import { EnvWithCtx } from '@/ctx';
22+import { Hono } from 'hono';
33+import getPlay from './feed/getPlay';
44+import getActorFeed from './feed/getActorFeed';
55+import getProfile from './actor/getProfile';
66+import searchActors from './actor/searchActors';
5768// mount this on /xrpc
79const app = new Hono<EnvWithCtx>();
81099-app.get("fm.teal.alpha.getPlay", async (c) => c.json(await getPlay(c)));
1010-app.get("fm.teal.alpha.feed.getActorFeed", async (c) =>
1111+app.get('fm.teal.alpha.feed.getPlay', async (c) => c.json(await getPlay(c)));
1212+app.get('fm.teal.alpha.feed.getActorFeed', async (c) =>
1113 c.json(await getActorFeed(c)),
1414+);
1515+1616+app.get('fm.teal.alpha.actor.getProfile', async (c) =>
1717+ c.json(await getProfile(c)),
1818+);
1919+2020+app.get('fm.teal.alpha.actor.searchActors', async (c) =>
2121+ c.json(await searchActors(c)),
1222);
13231424export const getXrpcRouter = () => {
+46
packages/db/.drizzle/0000_perfect_war_machine.sql
···11+CREATE TABLE "artists" (
22+ "mbid" uuid PRIMARY KEY NOT NULL,
33+ "name" text NOT NULL,
44+ "play_count" integer DEFAULT 0
55+);
66+--> statement-breakpoint
77+CREATE TABLE "play_to_artists" (
88+ "play_uri" text NOT NULL,
99+ "artist_mbid" uuid NOT NULL,
1010+ "artist_name" text,
1111+ CONSTRAINT "play_to_artists_play_uri_artist_mbid_pk" PRIMARY KEY("play_uri","artist_mbid")
1212+);
1313+--> statement-breakpoint
1414+CREATE TABLE "plays" (
1515+ "uri" text PRIMARY KEY NOT NULL,
1616+ "did" text NOT NULL,
1717+ "rkey" text NOT NULL,
1818+ "cid" text NOT NULL,
1919+ "isrc" text,
2020+ "duration" integer,
2121+ "track_name" text NOT NULL,
2222+ "played_time" timestamp with time zone,
2323+ "processed_time" timestamp with time zone DEFAULT now(),
2424+ "release_mbid" uuid,
2525+ "release_name" text,
2626+ "recording_mbid" uuid,
2727+ "submission_client_agent" text,
2828+ "music_service_base_domain" text
2929+);
3030+--> statement-breakpoint
3131+CREATE TABLE "recordings" (
3232+ "mbid" uuid PRIMARY KEY NOT NULL,
3333+ "name" text NOT NULL,
3434+ "play_count" integer DEFAULT 0
3535+);
3636+--> statement-breakpoint
3737+CREATE TABLE "releases" (
3838+ "mbid" uuid PRIMARY KEY NOT NULL,
3939+ "name" text NOT NULL,
4040+ "play_count" integer DEFAULT 0
4141+);
4242+--> statement-breakpoint
4343+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
4444+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
4545+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
4646+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
···11-CREATE TABLE `auth_session` (
22- `key` text PRIMARY KEY NOT NULL,
33- `session` text NOT NULL
44-);
55---> statement-breakpoint
66-CREATE TABLE `auth_state` (
77- `key` text PRIMARY KEY NOT NULL,
88- `state` text NOT NULL
99-);
1010---> statement-breakpoint
1111-CREATE TABLE `status` (
1212- `uri` text PRIMARY KEY NOT NULL,
1313- `authorDid` text NOT NULL,
1414- `status` text NOT NULL,
1515- `createdAt` text NOT NULL,
1616- `indexedAt` text NOT NULL
1717-);
-3
packages/db/.drizzle/0001_fresh_tana_nile.sql
···11-ALTER TABLE `status` RENAME COLUMN "authorDid" TO "author_did";--> statement-breakpoint
22-ALTER TABLE `status` RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
33-ALTER TABLE `status` RENAME COLUMN "indexedAt" TO "indexed_at";
+6
packages/db/.drizzle/0001_swift_maddog.sql
···11+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
22+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
33+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
44+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
55+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
66+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
···11-ALTER TABLE `auth_session` RENAME TO `atp_session`;
···11-CREATE TABLE `teal_session` (
22- `key` text PRIMARY KEY NOT NULL,
33- `session` text NOT NULL,
44- `provider` text NOT NULL
55-);
66---> statement-breakpoint
77-CREATE TABLE `teal_user` (
88- `did` text PRIMARY KEY NOT NULL,
99- `handle` text NOT NULL,
1010- `email` text NOT NULL,
1111- `created_at` text NOT NULL
1212-);
+15
packages/db/.drizzle/0003_worried_unicorn.sql
···11+CREATE TABLE "profiles" (
22+ "did" text PRIMARY KEY NOT NULL,
33+ "display_name" text NOT NULL,
44+ "description" text NOT NULL,
55+ "description_facets" jsonb NOT NULL,
66+ "avatar" text NOT NULL,
77+ "banner" text NOT NULL,
88+ "created_at" timestamp NOT NULL
99+);
1010+--> statement-breakpoint
1111+CREATE TABLE "featured_items" (
1212+ "did" text PRIMARY KEY NOT NULL,
1313+ "mbid" text NOT NULL,
1414+ "type" text NOT NULL
1515+);
-29
packages/db/.drizzle/0004_exotic_ironclad.sql
···11-CREATE TABLE `follow` (
22- `follower` text PRIMARY KEY NOT NULL,
33- `followed` text NOT NULL,
44- `created_at` text NOT NULL
55-);
66---> statement-breakpoint
77-CREATE TABLE `play` (
88- `uri` text PRIMARY KEY NOT NULL,
99- `author_did` text NOT NULL,
1010- `created_at` text NOT NULL,
1111- `indexed_at` text NOT NULL,
1212- `track_name` text NOT NULL,
1313- `track_mb_id` text,
1414- `recording_mb_id` text,
1515- `duration` integer,
1616- `artist_name` text NOT NULL,
1717- `artist_mb_ids` text,
1818- `release_name` text,
1919- `release_mb_id` text,
2020- `isrc` text,
2121- `origin_url` text,
2222- `music_service_base_domain` text,
2323- `submission_client_agent` text,
2424- `played_time` text
2525-);
2626---> statement-breakpoint
2727-ALTER TABLE `teal_user` ADD `avatar` text NOT NULL;--> statement-breakpoint
2828-ALTER TABLE `teal_user` ADD `bio` text;--> statement-breakpoint
2929-ALTER TABLE `teal_user` DROP COLUMN `email`;
+7
packages/db/.drizzle/0004_furry_gravity.sql
···11+ALTER TABLE "profiles" ALTER COLUMN "display_name" DROP NOT NULL;--> statement-breakpoint
22+ALTER TABLE "profiles" ALTER COLUMN "description" DROP NOT NULL;--> statement-breakpoint
33+ALTER TABLE "profiles" ALTER COLUMN "description_facets" DROP NOT NULL;--> statement-breakpoint
44+ALTER TABLE "profiles" ALTER COLUMN "avatar" DROP NOT NULL;--> statement-breakpoint
55+ALTER TABLE "profiles" ALTER COLUMN "banner" DROP NOT NULL;--> statement-breakpoint
66+ALTER TABLE "profiles" ALTER COLUMN "created_at" DROP NOT NULL;--> statement-breakpoint
77+ALTER TABLE "profiles" ADD COLUMN "handle" text;
···2929 originUrl?: string
3030 /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided. */
3131 musicServiceBaseDomain?: string
3232- /** 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. */
3232+ /** 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. */
3333 submissionClientAgent?: string
3434 /** The unix timestamp of when the track was played */
3535 playedTime?: string