From 66b4fa5fd4876a74803a5eebc2e02ab0a339988d Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Tue, 7 Oct 2025 13:06:16 +0300 Subject: [PATCH] Remove unused dependency for @xata.io/client in bun.lock and replace all xata client usage with drizzle orm --- apps/api/package.json | 1 - apps/api/src/apikeys/app.ts | 102 +- apps/api/src/bsky/app.ts | 106 +- apps/api/src/context.ts | 4 - apps/api/src/dropbox/app.ts | 36 +- apps/api/src/googledrive/app.ts | 148 +- apps/api/src/index.ts | 245 +- .../src/lovedtracks/lovedtracks.service.ts | 450 +- apps/api/src/nowplaying/nowplaying.service.ts | 565 +- apps/api/src/scripts/avatar.ts | 30 +- apps/api/src/scripts/sync.ts | 225 +- apps/api/src/search/app.ts | 50 - apps/api/src/shouts/shouts.service.ts | 259 +- apps/api/src/spotify/app.ts | 274 +- apps/api/src/tracks/tracks.service.ts | 265 +- apps/api/src/users/app.ts | 805 +-- apps/api/src/webscrobbler/app.ts | 85 +- apps/api/src/xata.ts | 5265 ----------------- bun.lock | 6 - 19 files changed, 2207 insertions(+), 6714 deletions(-) delete mode 100644 apps/api/src/search/app.ts delete mode 100644 apps/api/src/xata.ts diff --git a/apps/api/package.json b/apps/api/package.json index 036801be..7a6fc687 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -45,7 +45,6 @@ "@opentelemetry/sdk-node": "^0.200.0", "@opentelemetry/semantic-conventions": "^1.32.0", "@pyroscope/nodejs": "^0.4.5", - "@xata.io/client": "^0.0.0-next.va121e4207b94bfe0a3c025fc00b247b923880930", "assert": "^2.1.0", "axios": "^1.7.9", "better-sqlite3": "^11.8.1", diff --git a/apps/api/src/apikeys/app.ts b/apps/api/src/apikeys/app.ts index 671111ff..9548fd1b 100644 --- a/apps/api/src/apikeys/app.ts +++ b/apps/api/src/apikeys/app.ts @@ -1,4 +1,3 @@ -import { equals } from "@xata.io/client"; import { ctx } from "context"; import { and, eq } from "drizzle-orm"; import { Hono } from "hono"; @@ -6,7 +5,8 @@ import jwt from "jsonwebtoken"; import { env } from "lib/env"; import crypto from "node:crypto"; import * as R from "ramda"; -import tables from "schema"; +import apiKeys from "schema/api-keys"; +import users from "schema/users"; import { apiKeySchema } from "types/apikey"; const app = new Hono(); @@ -23,7 +23,13 @@ app.get("/", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -32,15 +38,14 @@ app.get("/", async (c) => { const size = +c.req.query("size") || 20; const offset = +c.req.query("offset") || 0; - const apikeys = await ctx.db + const apikeysData = await ctx.db .select() - .from(tables.apiKeys) - .where(eq(tables.apiKeys.userId, user.xata_id)) + .from(apiKeys) + .where(eq(apiKeys.userId, user.id)) .limit(size) - .offset(offset) - .execute(); + .offset(offset); - return c.json(apikeys.map((x) => R.omit(["userId"])(x))); + return c.json(apikeysData.map((x) => R.omit(["userId"])(x))); }); app.post("/", async (c) => { @@ -55,7 +60,13 @@ app.post("/", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -70,22 +81,32 @@ app.post("/", async (c) => { } const newApiKey = parsed.data; - const api_key = crypto.randomBytes(16).toString("hex"); - const shared_secret = crypto.randomBytes(16).toString("hex"); + if (!newApiKey.name) { + c.status(400); + return c.text("Missing required field: name"); + } - const record = await ctx.client.db.api_keys.create({ - ...newApiKey, - api_key, - shared_secret, - user_id: user.xata_id, - }); + const apiKey = crypto.randomBytes(16).toString("hex"); + const sharedSecret = crypto.randomBytes(16).toString("hex"); + + const [record] = await ctx.db + .insert(apiKeys) + .values({ + name: newApiKey.name, + description: newApiKey.description ?? "", + enabled: newApiKey.enabled ?? true, + apiKey, + sharedSecret, + userId: user.id, + }) + .returning(); return c.json({ - id: record.xata_id, + id: record.id, name: record.name, description: record.description, - api_key: record.api_key, - shared_secret: record.shared_secret, + apiKey: record.apiKey, + sharedSecret: record.sharedSecret, }); }); @@ -101,7 +122,13 @@ app.put("/:id", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -110,20 +137,18 @@ app.put("/:id", async (c) => { const data = await c.req.json(); const id = c.req.param("id"); - const record = await ctx.db - .update(tables.apiKeys) + const [record] = await ctx.db + .update(apiKeys) .set(data) - .where( - and(eq(tables.apiKeys.id, id), eq(tables.apiKeys.userId, user.xata_id)), - ) - .execute(); + .where(and(eq(apiKeys.id, id), eq(apiKeys.userId, user.id))) + .returning(); return c.json({ - id: record.xata_id, + id: record.id, name: record.name, description: record.description, - api_key: record.api_key, - shared_secret: record.shared_secret, + apiKey: record.apiKey, + sharedSecret: record.sharedSecret, }); }); @@ -139,7 +164,13 @@ app.delete("/:id", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -148,11 +179,8 @@ app.delete("/:id", async (c) => { const id = c.req.param("id"); await ctx.db - .delete(tables.apiKeys) - .where( - and(eq(tables.apiKeys.id, id), eq(tables.apiKeys.userId, user.xata_id)), - ) - .execute(); + .delete(apiKeys) + .where(and(eq(apiKeys.id, id), eq(apiKeys.userId, user.id))); return c.json({ success: true }); }); diff --git a/apps/api/src/bsky/app.ts b/apps/api/src/bsky/app.ts index b73426e4..bdba1cfa 100644 --- a/apps/api/src/bsky/app.ts +++ b/apps/api/src/bsky/app.ts @@ -1,14 +1,18 @@ import type { BlobRef } from "@atproto/lexicon"; import { isValidHandle } from "@atproto/syntax"; -import { equals } from "@xata.io/client"; import { ctx } from "context"; -import { desc, eq } from "drizzle-orm"; +import { and, desc, eq } from "drizzle-orm"; import { Hono } from "hono"; import jwt from "jsonwebtoken"; import * as Profile from "lexicon/types/app/bsky/actor/profile"; +import { deepSnakeCaseKeys } from "lib"; import { createAgent } from "lib/agent"; import { env } from "lib/env"; import { requestCounter } from "metrics"; +import dropboxAccounts from "schema/dropbox-accounts"; +import googleDriveAccounts from "schema/google-drive-accounts"; +import spotifyAccounts from "schema/spotify-accounts"; +import spotifyTokens from "schema/spotify-tokens"; import users from "schema/users"; const app = new Hono(); @@ -61,7 +65,7 @@ app.post("/login", async (c) => { app.get("/oauth/callback", async (c) => { requestCounter.add(1, { method: "GET", route: "/oauth/callback" }); const params = new URLSearchParams(c.req.url.split("?")[1]); - let did, cli; + let did: string, cli: string; try { const { session } = await ctx.oauthClient.callback(params); @@ -77,7 +81,7 @@ app.get("/oauth/callback", async (c) => { ? Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 1000 : Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, }, - env.JWT_SECRET, + env.JWT_SECRET ); ctx.kv.set(did, token); } catch (err) { @@ -85,10 +89,14 @@ app.get("/oauth/callback", async (c) => { return c.redirect(`${env.FRONTEND_URL}?error=1`); } - const spotifyUser = await ctx.client.db.spotify_accounts - .filter("user_id.did", equals(did)) - .filter("is_beta_user", equals(true)) - .getFirst(); + const [spotifyUser] = await ctx.db + .select() + .from(spotifyAccounts) + .where( + and(eq(spotifyAccounts.userId, did), eq(spotifyAccounts.isBetaUser, true)) + ) + .limit(1) + .execute(); if (spotifyUser?.email) { ctx.nc.publish("rocksky.spotify.user", Buffer.from(spotifyUser.email)); @@ -134,12 +142,15 @@ app.get("/profile", async (c) => { if (profile.handle) { try { - await ctx.client.db.users.create({ - did, - handle, - display_name: profile.displayName, - avatar: `https://cdn.bsky.app/img/avatar/plain/${did}/${profile.avatar.ref.toString()}@jpeg`, - }); + await ctx.db + .insert(users) + .values({ + did, + handle, + displayName: profile.displayName, + avatar: `https://cdn.bsky.app/img/avatar/plain/${did}/${profile.avatar.ref.toString()}@jpeg`, + }) + .execute(); } catch (e) { if (!e.message.includes("invalid record: column [did]: is not unique")) { console.error(e.message); @@ -156,40 +167,65 @@ app.get("/profile", async (c) => { } } - const [user, lastUser, previousLastUser] = await Promise.all([ - ctx.client.db.users.select(["*"]).filter("did", equals(did)).getFirst(), + const [user, lastUser] = await Promise.all([ + ctx.db.select().from(users).where(eq(users.did, did)).limit(1).execute(), ctx.db .select() .from(users) .orderBy(desc(users.createdAt)) .limit(1) .execute(), - ctx.kv.get("lastUser"), ]); - ctx.nc.publish("rocksky.user", Buffer.from(JSON.stringify(user))); + ctx.nc.publish( + "rocksky.user", + Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))) + ); await ctx.kv.set("lastUser", lastUser[0].id); - // if (lastUser[0].id !== previousLastUser) { - // ctx.nc.publish("rocksky.user", Buffer.from(JSON.stringify(user))); - // } } const [spotifyUser, spotifyToken, googledrive, dropbox] = await Promise.all([ - ctx.client.db.spotify_accounts - .select(["user_id.*", "email", "is_beta_user"]) - .filter("user_id.did", equals(did)) - .getFirst(), - ctx.client.db.spotify_tokens.filter("user_id.did", equals(did)).getFirst(), - ctx.client.db.google_drive_accounts - .select(["user_id.*", "email", "is_beta_user"]) - .filter("user_id.did", equals(did)) - .getFirst(), - ctx.client.db.dropbox_accounts - .select(["user_id.*", "email", "is_beta_user"]) - .filter("user_id.did", equals(did)) - .getFirst(), - ]); + ctx.db + .select() + .from(spotifyAccounts) + .where( + and( + eq(spotifyAccounts.userId, did), + eq(spotifyAccounts.isBetaUser, true) + ) + ) + .limit(1) + .execute(), + ctx.db + .select() + .from(spotifyTokens) + .where(eq(spotifyTokens.userId, did)) + .limit(1) + .execute(), + ctx.db + .select() + .from(googleDriveAccounts) + .where( + and( + eq(googleDriveAccounts.userId, did), + eq(googleDriveAccounts.isBetaUser, true) + ) + ) + .limit(1) + .execute(), + ctx.db + .select() + .from(dropboxAccounts) + .where( + and( + eq(dropboxAccounts.userId, did), + eq(dropboxAccounts.isBetaUser, true) + ) + ) + .limit(1) + .execute(), + ]).then(([s, t, g, d]) => deepSnakeCaseKeys([s[0], t[0], g[0], d[0]])); return c.json({ ...profile, diff --git a/apps/api/src/context.ts b/apps/api/src/context.ts index 9f8d5129..048c2162 100644 --- a/apps/api/src/context.ts +++ b/apps/api/src/context.ts @@ -9,7 +9,6 @@ import { connect } from "nats"; import redis from "redis"; import sqliteKv from "sqliteKv"; import { createStorage } from "unstorage"; -import { getXataClient } from "xata"; const { DB_PATH } = env; export const db = createDb(DB_PATH); @@ -21,14 +20,11 @@ const kv = createStorage({ const baseIdResolver = createIdResolver(kv); -const client = getXataClient(); - export const ctx = { oauthClient: await createClient(db), resolver: createBidirectionalResolver(baseIdResolver), baseIdResolver, kv: new Map(), - client, db: drizzle.db, nc: await connect({ servers: env.NATS_URL }), analytics: axios.create({ baseURL: env.ANALYTICS }), diff --git a/apps/api/src/dropbox/app.ts b/apps/api/src/dropbox/app.ts index 81dc5a52..77d441b8 100644 --- a/apps/api/src/dropbox/app.ts +++ b/apps/api/src/dropbox/app.ts @@ -1,4 +1,3 @@ -import { equals } from "@xata.io/client"; import axios from "axios"; import { ctx } from "context"; import { eq } from "drizzle-orm"; @@ -191,7 +190,12 @@ app.get("/files", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const [user] = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, did)) + .limit(1) + .execute(); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -238,7 +242,12 @@ app.get("/temporary-link", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const [user] = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, did)) + .limit(1) + .execute(); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -271,7 +280,12 @@ app.get("/files/:id", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const [user] = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, did)) + .limit(1) + .execute(); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -300,7 +314,12 @@ app.get("/file", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const [user] = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, did)) + .limit(1) + .execute(); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -334,7 +353,12 @@ app.get("/download", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const [user] = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, did)) + .limit(1) + .execute(); if (!user) { c.status(401); return c.text("Unauthorized"); diff --git a/apps/api/src/googledrive/app.ts b/apps/api/src/googledrive/app.ts index 62f6745f..d2aff780 100644 --- a/apps/api/src/googledrive/app.ts +++ b/apps/api/src/googledrive/app.ts @@ -1,6 +1,6 @@ -import { equals } from "@xata.io/client"; import axios from "axios"; import { ctx } from "context"; +import { eq } from "drizzle-orm"; import fs from "fs"; import { google } from "googleapis"; import { Hono } from "hono"; @@ -8,6 +8,10 @@ import jwt from "jsonwebtoken"; import { encrypt } from "lib/crypto"; import { env } from "lib/env"; import { requestCounter } from "metrics"; +import googleDriveAccounts from "schema/google-drive-accounts"; +import googleDriveTokens from "schema/google-drive-tokens"; +import googleDrive from "schema/googledrive"; +import users from "schema/users"; import { emailSchema } from "types/email"; const app = new Hono(); @@ -25,20 +29,26 @@ app.get("/login", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); } const credentials = JSON.parse( - fs.readFileSync("credentials.json").toString("utf-8"), + fs.readFileSync("credentials.json").toString("utf-8") ); const { client_id, client_secret } = credentials.installed || credentials.web; const oAuth2Client = new google.auth.OAuth2( client_id, client_secret, - env.GOOGLE_REDIRECT_URI, + env.GOOGLE_REDIRECT_URI ); // Generate Auth URL @@ -46,7 +56,7 @@ app.get("/login", async (c) => { access_type: "offline", prompt: "consent", scope: ["https://www.googleapis.com/auth/drive"], - state: user.xata_id, + state: user.id, }); return c.json({ authUrl }); }); @@ -60,7 +70,7 @@ app.get("/oauth/callback", async (c) => { const entries = Object.fromEntries(params.entries()); const credentials = JSON.parse( - fs.readFileSync("credentials.json").toString("utf-8"), + fs.readFileSync("credentials.json").toString("utf-8") ); const { client_id, client_secret } = credentials.installed || credentials.web; @@ -72,26 +82,62 @@ app.get("/oauth/callback", async (c) => { grant_type: "authorization_code", }); - const googledrive = await ctx.client.db.google_drive - .select(["*", "user_id.*", "google_drive_token_id.*"]) - .filter("user_id.xata_id", equals(entries.state)) - .getFirst(); - - const newGoogleDriveToken = - await ctx.client.db.google_drive_tokens.createOrUpdate( - googledrive?.google_drive_token_id?.xata_id, - { - refresh_token: encrypt( + const existingGoogleDrive = await ctx.db + .select({ + googleDrive: googleDrive, + user: users, + token: googleDriveTokens, + }) + .from(googleDrive) + .innerJoin(users, eq(googleDrive.userId, users.id)) + .leftJoin( + googleDriveTokens, + eq(googleDrive.googleDriveTokenId, googleDriveTokens.id) + ) + .where(eq(users.id, entries.state)) + .limit(1) + .then((rows) => rows[0]); + + let tokenId: string; + if (existingGoogleDrive?.token) { + const [updatedToken] = await ctx.db + .update(googleDriveTokens) + .set({ + refreshToken: encrypt( response.data.refresh_token, - env.SPOTIFY_ENCRYPTION_KEY, + env.SPOTIFY_ENCRYPTION_KEY ), - }, - ); + }) + .where(eq(googleDriveTokens.id, existingGoogleDrive.token.id)) + .returning(); + tokenId = updatedToken.id; + } else { + const [newToken] = await ctx.db + .insert(googleDriveTokens) + .values({ + refreshToken: encrypt( + response.data.refresh_token, + env.SPOTIFY_ENCRYPTION_KEY + ), + }) + .returning(); + tokenId = newToken.id; + } - await ctx.client.db.google_drive.createOrUpdate(googledrive?.xata_id, { - google_drive_token_id: newGoogleDriveToken.xata_id, - user_id: entries.state, - }); + if (existingGoogleDrive?.googleDrive) { + await ctx.db + .update(googleDrive) + .set({ + googleDriveTokenId: tokenId, + userId: entries.state, + }) + .where(eq(googleDrive.id, existingGoogleDrive.googleDrive.id)); + } else { + await ctx.db.insert(googleDrive).values({ + googleDriveTokenId: tokenId, + userId: entries.state, + }); + } return c.redirect(`${env.FRONTEND_URL}/googledrive`); }); @@ -109,7 +155,13 @@ app.post("/join", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -126,15 +178,13 @@ app.post("/join", async (c) => { const { email } = parsed.data; try { - await ctx.client.db.google_drive_accounts.create({ - user_id: user.xata_id, + await ctx.db.insert(googleDriveAccounts).values({ + userId: user.id, email, - is_beta_user: false, + isBetaUser: false, }); } catch (e) { - if ( - !e.message.includes("invalid record: column [user_id]: is not unique") - ) { + if (!e.message.includes("duplicate key value violates unique constraint")) { console.error(e.message); } else { throw e; @@ -166,7 +216,13 @@ app.get("/files", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -181,7 +237,7 @@ app.get("/files", async (c) => { { did, parent_id, - }, + } ); return c.json(data); } @@ -202,7 +258,7 @@ app.get("/files", async (c) => { { did, parent_id: response.data.files[0].id, - }, + } ); return c.json(data); } catch (error) { @@ -210,14 +266,14 @@ app.get("/files", async (c) => { console.error("Axios error:", error.response?.data || error.message); const credentials = JSON.parse( - fs.readFileSync("credentials.json").toString("utf-8"), + fs.readFileSync("credentials.json").toString("utf-8") ); const { client_id, client_secret } = credentials.installed || credentials.web; const oAuth2Client = new google.auth.OAuth2( client_id, client_secret, - env.GOOGLE_REDIRECT_URI, + env.GOOGLE_REDIRECT_URI ); // Generate Auth URL @@ -225,7 +281,7 @@ app.get("/files", async (c) => { access_type: "offline", prompt: "consent", scope: ["https://www.googleapis.com/auth/drive"], - state: user.xata_id, + state: user.id, }); return c.json({ @@ -249,7 +305,13 @@ app.get("/files/:id", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -280,7 +342,13 @@ app.get("/files/:id/download", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -294,11 +362,11 @@ app.get("/files/:id/download", async (c) => { c.header( "Content-Type", - response.headers["content-type"] || "application/octet-stream", + response.headers["content-type"] || "application/octet-stream" ); c.header( "Content-Disposition", - response.headers["content-disposition"] || "attachment", + response.headers["content-disposition"] || "attachment" ); return new Response(response.data, { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 2246f8aa..d1d5dd53 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,8 +1,8 @@ import { serve } from "@hono/node-server"; import { createNodeWebSocket } from "@hono/node-ws"; import { trace } from "@opentelemetry/api"; -import { equals } from "@xata.io/client"; import { ctx } from "context"; +import { and, desc, eq, isNotNull, or } from "drizzle-orm"; import { Hono } from "hono"; import { cors } from "hono/cors"; import jwt from "jsonwebtoken"; @@ -25,10 +25,16 @@ import googledrive from "./googledrive/app"; import { env } from "./lib/env"; import { requestCounter, requestDuration } from "./metrics"; import "./profiling"; -import search from "./search/app"; +import albumTracks from "./schema/album-tracks"; +import albums from "./schema/albums"; +import artistTracks from "./schema/artist-tracks"; +import artists from "./schema/artists"; +import scrobbles from "./schema/scrobbles"; +import tracks from "./schema/tracks"; +import users from "./schema/users"; import spotify from "./spotify/app"; import "./tracing"; -import users from "./users/app"; +import usersApp from "./users/app"; import webscrobbler from "./webscrobbler/app"; subscribe(ctx); @@ -41,7 +47,7 @@ app.use( rateLimiter({ limit: 1000, window: 30, // 👈 30 seconds - }), + }) ); app.use("*", async (c, next) => { @@ -91,7 +97,13 @@ app.post("/now-playing", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -133,11 +145,12 @@ app.get("/now-playing", async (c) => { return c.text("Unauthorized"); } - const user = await ctx.client.db.users - .filter({ - $any: [{ did }, { handle: did }], - }) - .getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(or(eq(users.did, did), eq(users.handle, did))) + .limit(1) + .then((rows) => rows[0]); if (!user) { c.status(401); @@ -149,7 +162,7 @@ app.get("/now-playing", async (c) => { ctx.redis.get(`nowplaying:${user.did}:status`), ]); return c.json( - nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {}, + nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {} ); }); @@ -180,7 +193,13 @@ app.post("/likes", async (c) => { }); const agent = await createAgent(ctx.oauthClient, did); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -213,7 +232,13 @@ app.delete("/likes/:sha256", async (c) => { }); const agent = await createAgent(ctx.oauthClient, did); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -237,7 +262,13 @@ app.get("/likes", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -256,31 +287,34 @@ app.get("/public/scrobbles", async (c) => { const size = +c.req.query("size") || 10; const offset = +c.req.query("offset") || 0; - const scrobbles = await ctx.client.db.scrobbles - .select(["track_id.*", "user_id.*", "timestamp", "xata_createdat", "uri"]) - .sort("timestamp", "desc") - .getPaginated({ - pagination: { - size, - offset, - }, - }); + const scrobbleRecords = await ctx.db + .select({ + scrobble: scrobbles, + track: tracks, + user: users, + }) + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .orderBy(desc(scrobbles.timestamp)) + .limit(size) + .offset(offset); return c.json( - scrobbles.records.map((item) => ({ - cover: item.track_id.album_art, - artist: item.track_id.artist, - title: item.track_id.title, - date: item.timestamp, - user: item.user_id.handle, - uri: item.uri, - albumUri: item.track_id.album_uri, - artistUri: item.track_id.artist_uri, + scrobbleRecords.map((item) => ({ + cover: item.track.albumArt, + artist: item.track.artist, + title: item.track.title, + date: item.scrobble.timestamp, + user: item.user.handle, + uri: item.scrobble.uri, + albumUri: item.track.albumUri, + artistUri: item.track.artistUri, tags: [], listeners: 1, - sha256: item.track_id.sha256, - id: item.xata_id, - })), + sha256: item.track.sha256, + id: item.scrobble.id, + })) ); }); @@ -316,12 +350,18 @@ app.get("/public/scrobbleschart", async (c) => { if (songuri) { let uri = songuri; if (songuri.includes("app.rocksky.scrobble")) { - const scrobble = await ctx.client.db.scrobbles - .select(["track_id.*", "uri"]) - .filter("uri", equals(songuri)) - .getFirst(); - - uri = scrobble.track_id.uri; + const scrobble = await ctx.db + .select({ + scrobble: scrobbles, + track: tracks, + }) + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .where(eq(scrobbles.uri, songuri)) + .limit(1) + .then((rows) => rows[0]); + + uri = scrobble.track.uri; } const chart = await ctx.analytics.post("library.getTrackScrobbles", { track_id: uri, @@ -347,7 +387,13 @@ app.get("/scrobbles", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -356,25 +402,19 @@ app.get("/scrobbles", async (c) => { const size = +c.req.query("size") || 10; const offset = +c.req.query("offset") || 0; - const scrobbles = await ctx.client.db.scrobbles - .select(["track_id.*", "uri"]) - .filter("user_id", equals(user.xata_id)) - .filter({ - $not: [ - { - uri: null, - }, - ], + const userScrobbles = await ctx.db + .select({ + scrobble: scrobbles, + track: tracks, }) - .sort("xata_createdat", "desc") - .getPaginated({ - pagination: { - size, - offset, - }, - }); - - return c.json(scrobbles.records); + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .where(and(eq(scrobbles.userId, user.id), isNotNull(scrobbles.uri))) + .orderBy(desc(scrobbles.createdAt)) + .limit(size) + .offset(offset); + + return c.json(userScrobbles); }); app.post("/tracks", async (c) => { @@ -391,7 +431,13 @@ app.post("/tracks", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -416,8 +462,8 @@ app.post("/tracks", async (c) => { try { await saveTrack(ctx, track, agent); } catch (e) { - if (!e.message.includes("invalid record: column [sha256]: is not unique")) { - console.error("[spotify user]", e.message); + if (!e.message.includes("duplicate key value violates unique constraint")) { + console.error("[tracks]", e.message); } } @@ -430,14 +476,14 @@ app.get("/tracks", async (c) => { const size = +c.req.query("size") || 100; const offset = +c.req.query("offset") || 0; - const tracks = await ctx.analytics.post("library.getTracks", { + const tracksData = await ctx.analytics.post("library.getTracks", { pagination: { skip: offset, take: size, }, }); - return c.json(tracks.data); + return c.json(tracksData.data); }); app.get("/albums", async (c) => { @@ -446,14 +492,14 @@ app.get("/albums", async (c) => { const size = +c.req.query("size") || 100; const offset = +c.req.query("offset") || 0; - const albums = await ctx.analytics.post("library.getAlbums", { + const albumsData = await ctx.analytics.post("library.getAlbums", { pagination: { skip: offset, take: size, }, }); - return c.json(albums.data); + return c.json(albumsData.data); }); app.get("/artists", async (c) => { @@ -462,23 +508,27 @@ app.get("/artists", async (c) => { const size = +c.req.query("size") || 100; const offset = +c.req.query("offset") || 0; - const artists = await ctx.analytics.post("library.getArtists", { + const artistsData = await ctx.analytics.post("library.getArtists", { pagination: { skip: offset, take: size, }, }); - return c.json(artists.data); + return c.json(artistsData.data); }); app.get("/tracks/:sha256", async (c) => { requestCounter.add(1, { method: "GET", route: "/tracks/:sha256" }); const sha256 = c.req.param("sha256"); - const track = await ctx.client.db.tracks - .filter("sha256", equals(sha256)) - .getFirst(); + const track = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.sha256, sha256)) + .limit(1) + .then((rows) => rows[0]); + return c.json(track); }); @@ -486,9 +536,12 @@ app.get("/albums/:sha256", async (c) => { requestCounter.add(1, { method: "GET", route: "/albums/:sha256" }); const sha256 = c.req.param("sha256"); - const album = await ctx.client.db.albums - .filter("sha256", equals(sha256)) - .getFirst(); + const album = await ctx.db + .select() + .from(albums) + .where(eq(albums.sha256, sha256)) + .limit(1) + .then((rows) => rows[0]); return c.json(album); }); @@ -497,9 +550,12 @@ app.get("/artists/:sha256", async (c) => { requestCounter.add(1, { method: "GET", route: "/artists/:sha256" }); const sha256 = c.req.param("sha256"); - const artist = await ctx.client.db.artists - .filter("sha256", equals(sha256)) - .getFirst(); + const artist = await ctx.db + .select() + .from(artists) + .where(eq(artists.sha256, sha256)) + .limit(1) + .then((rows) => rows[0]); return c.json(artist); }); @@ -508,28 +564,35 @@ app.get("/artists/:sha256/tracks", async (c) => { requestCounter.add(1, { method: "GET", route: "/artists/:sha256/tracks" }); const sha256 = c.req.param("sha256"); - const tracks = await ctx.client.db.artist_tracks - .select(["track_id.*"]) - .filter("artist_id.sha256", equals(sha256)) - .getAll(); + const artistTracksData = await ctx.db + .select({ + track: tracks, + }) + .from(artistTracks) + .innerJoin(tracks, eq(artistTracks.trackId, tracks.id)) + .innerJoin(artists, eq(artistTracks.artistId, artists.id)) + .where(eq(artists.sha256, sha256)); - return c.json(tracks); + return c.json(artistTracksData.map((item) => item.track)); }); app.get("/albums/:sha256/tracks", async (c) => { requestCounter.add(1, { method: "GET", route: "/albums/:sha256/tracks" }); const sha256 = c.req.param("sha256"); - const tracks = await ctx.client.db.album_tracks - .select(["track_id.*"]) - .filter("album_id.sha256", equals(sha256)) - .getAll(); - return c.json(tracks); -}); + const albumTracksData = await ctx.db + .select({ + track: tracks, + }) + .from(albumTracks) + .innerJoin(tracks, eq(albumTracks.trackId, tracks.id)) + .innerJoin(albums, eq(albumTracks.albumId, albums.id)) + .where(eq(albums.sha256, sha256)); -app.route("/users", users); + return c.json(albumTracksData.map((item) => item.track)); +}); -app.route("/search", search); +app.route("/users", usersApp); app.route("/webscrobbler", webscrobbler); diff --git a/apps/api/src/lovedtracks/lovedtracks.service.ts b/apps/api/src/lovedtracks/lovedtracks.service.ts index 813751bb..85e5deae 100644 --- a/apps/api/src/lovedtracks/lovedtracks.service.ts +++ b/apps/api/src/lovedtracks/lovedtracks.service.ts @@ -1,166 +1,261 @@ import type { Agent } from "@atproto/api"; import { TID } from "@atproto/common"; -import { equals } from "@xata.io/client"; import type { Context } from "context"; +import { and, desc, eq, type SQLWrapper } from "drizzle-orm"; import * as LikeLexicon from "lexicon/types/app/rocksky/like"; import { validateMain } from "lexicon/types/com/atproto/repo/strongRef"; import { createHash } from "node:crypto"; import type { Track } from "types/track"; +import albumTracks from "../schema/album-tracks"; +import albums from "../schema/albums"; +import artistAlbums from "../schema/artist-albums"; +import artistTracks from "../schema/artist-tracks"; +import artists from "../schema/artists"; +import lovedTracks from "../schema/loved-tracks"; +import tracks from "../schema/tracks"; export async function likeTrack( ctx: Context, track: Track, user, - agent: Agent, + agent: Agent ) { - const existingTrack = await ctx.client.db.tracks - .filter( - "sha256", - equals( - createHash("sha256") - .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), - ) - .digest("hex"), - ), + const trackSha256 = createHash("sha256") + .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) + .digest("hex"); + + const existingTrack = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.sha256, trackSha256)) + .limit(1) + .then((rows) => rows[0]); + + let trackId: string; + if (existingTrack) { + const [updatedTrack] = await ctx.db + .update(tracks) + .set({ + title: track.title, + artist: track.artist, + album: track.album, + albumArt: track.albumArt, + albumArtist: track.albumArtist, + trackNumber: track.trackNumber, + duration: track.duration, + mbId: track.mbId, + composer: track.composer, + lyrics: track.lyrics, + discNumber: track.discNumber, + sha256: trackSha256, + }) + .where(eq(tracks.id, existingTrack.id)) + .returning(); + trackId = updatedTrack.id; + } else { + const [createdTrack] = await ctx.db + .insert(tracks) + .values({ + title: track.title, + artist: track.artist, + album: track.album, + albumArt: track.albumArt, + albumArtist: track.albumArtist, + trackNumber: track.trackNumber, + duration: track.duration, + mbId: track.mbId, + composer: track.composer, + lyrics: track.lyrics, + discNumber: track.discNumber, + sha256: trackSha256, + }) + .returning(); + trackId = createdTrack.id; + } + + const artistSha256 = createHash("sha256") + .update(track.albumArtist.toLowerCase()) + .digest("hex"); + + const existingArtist = await ctx.db + .select() + .from(artists) + .where(eq(artists.sha256, artistSha256)) + .limit(1) + .then((rows) => rows[0]); + + let artistId: string; + if (existingArtist) { + const [updatedArtist] = await ctx.db + .update(artists) + .set({ + name: track.albumArtist, + sha256: artistSha256, + }) + .where(eq(artists.id, existingArtist.id)) + .returning(); + artistId = updatedArtist.id; + } else { + const [createdArtist] = await ctx.db + .insert(artists) + .values({ + name: track.albumArtist, + sha256: artistSha256, + }) + .returning(); + artistId = createdArtist.id; + } + + const albumSha256 = createHash("sha256") + .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) + .digest("hex"); + + const existingAlbum = await ctx.db + .select() + .from(albums) + .where(eq(albums.sha256, albumSha256)) + .limit(1) + .then((rows) => rows[0]); + + let albumId: string; + if (existingAlbum) { + const [updatedAlbum] = await ctx.db + .update(albums) + .set({ + title: track.album, + artist: track.albumArtist, + albumArt: track.albumArt, + year: track.year, + releaseDate: track.releaseDate + ? track.releaseDate.toISOString() + : undefined, + sha256: albumSha256, + }) + .where(eq(albums.id, existingAlbum.id)) + .returning(); + albumId = updatedAlbum.id; + } else { + const [createdAlbum] = await ctx.db + .insert(albums) + .values({ + title: track.album, + artist: track.albumArtist, + albumArt: track.albumArt, + year: track.year, + releaseDate: track.releaseDate + ? track.releaseDate.toISOString() + : undefined, + sha256: albumSha256, + }) + .returning(); + albumId = createdAlbum.id; + } + + // Create or update album_tracks relationship + const existingAlbumTrack = await ctx.db + .select() + .from(albumTracks) + .where( + and(eq(albumTracks.albumId, albumId), eq(albumTracks.trackId, trackId)) ) - .getFirst(); - - const { xata_id: track_id } = await ctx.client.db.tracks.createOrUpdate( - existingTrack?.xata_id, - { - title: track.title, - artist: track.artist, - album: track.album, - album_art: track.albumArt, - album_artist: track.albumArtist, - track_number: track.trackNumber, - duration: track.duration, - mb_id: track.mbId, - composer: track.composer, - lyrics: track.lyrics, - disc_number: track.discNumber, - // compute sha256 (lowercase(title + artist + album)) - sha256: createHash("sha256") - .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), - ) - .digest("hex"), - }, - ); - - const existingArtist = await ctx.client.db.artists - .filter( - "sha256", - equals( - createHash("sha256") - .update(track.albumArtist.toLocaleLowerCase()) - .digest("hex"), - ), + .limit(1) + .then((rows) => rows[0]); + + if (!existingAlbumTrack) { + await ctx.db.insert(albumTracks).values({ + albumId, + trackId, + }); + } + + // Create or update artist_tracks relationship + const existingArtistTrack = await ctx.db + .select() + .from(artistTracks) + .where( + and( + eq(artistTracks.artistId, artistId), + eq(artistTracks.trackId, trackId) + ) ) - .getFirst(); - const { xata_id: artist_id } = await ctx.client.db.artists.createOrUpdate( - existingArtist?.xata_id, - { - name: track.albumArtist, - // compute sha256 (lowercase(name)) - sha256: createHash("sha256") - .update(track.albumArtist.toLowerCase()) - .digest("hex"), - }, - ); - - const existingAlbum = await ctx.client.db.albums - .filter( - "sha256", - equals( - createHash("sha256") - .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) - .digest("hex"), - ), + .limit(1) + .then((rows) => rows[0]); + + if (!existingArtistTrack) { + await ctx.db.insert(artistTracks).values({ + artistId, + trackId, + }); + } + + // Create or update artist_albums relationship + const existingArtistAlbum = await ctx.db + .select() + .from(artistAlbums) + .where( + and( + eq(artistAlbums.artistId, artistId), + eq(artistAlbums.albumId, albumId) + ) + ) + .limit(1) + .then((rows) => rows[0]); + + if (!existingArtistAlbum) { + await ctx.db.insert(artistAlbums).values({ + artistId, + albumId, + }); + } + + // Create or update loved track + const existingLovedTrack = await ctx.db + .select() + .from(lovedTracks) + .where( + and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, trackId)) ) - .getFirst(); - - const { xata_id: album_id } = await ctx.client.db.albums.createOrUpdate( - existingAlbum?.xata_id, - { - title: track.album, - artist: track.albumArtist, - album_art: track.albumArt, - year: track.year, - release_date: track.releaseDate - ? track.releaseDate.toISOString() - : undefined, - // compute sha256 (lowercase(title + artist)) - sha256: createHash("sha256") - .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) - .digest("hex"), - }, - ); - - const existingAlbumTrack = await ctx.client.db.album_tracks - .filter("album_id", equals(album_id)) - .filter("track_id", equals(track_id)) - .getFirst(); - - await ctx.client.db.album_tracks.createOrUpdate(existingAlbumTrack?.xata_id, { - album_id, - track_id, - }); - - const existingArtistTrack = await ctx.client.db.artist_tracks - .filter("artist_id", equals(artist_id)) - .filter("track_id", equals(track_id)) - .getFirst(); - - await ctx.client.db.artist_tracks.createOrUpdate( - existingArtistTrack?.xata_id, - { - artist_id, - track_id, - }, - ); - - const existingArtistAlbum = await ctx.client.db.artist_albums - .filter("artist_id", equals(artist_id)) - .filter("album_id", equals(album_id)) - .getFirst(); - - await ctx.client.db.artist_albums.createOrUpdate( - existingArtistAlbum?.xata_id, - { - artist_id, - album_id, - }, - ); - - const lovedTrack = await ctx.client.db.loved_tracks - .filter("user_id", equals(user.xata_id)) - .filter("track_id", equals(track_id)) - .getFirst(); - - let created = await ctx.client.db.loved_tracks.createOrUpdate( - lovedTrack?.xata_id, - { - user_id: user.xata_id, - track_id, - }, - ); - - if (existingTrack.uri) { + .limit(1) + .then((rows) => rows[0]); + + let created: { id: string | SQLWrapper }; + if (existingLovedTrack) { + [created] = await ctx.db + .update(lovedTracks) + .set({ + userId: user.id, + trackId, + }) + .where(eq(lovedTracks.id, existingLovedTrack.id)) + .returning(); + } else { + [created] = await ctx.db + .insert(lovedTracks) + .values({ + userId: user.id, + trackId, + }) + .returning(); + } + + // Get the track with uri for ATProto operations + const trackWithUri = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.id, trackId)) + .limit(1) + .then((rows) => rows[0]); + + if (trackWithUri?.uri) { const rkey = TID.nextStr(); const subjectRecord = await agent.com.atproto.repo.getRecord({ - repo: existingTrack.uri - .split("/") - .slice(0, 3) - .join("/") - .split("at://")[1], + repo: trackWithUri.uri.split("/").slice(0, 3).join("/").split("at://")[1], collection: "app.rocksky.song", - rkey: existingTrack.uri.split("/").pop(), + rkey: trackWithUri.uri.split("/").pop(), }); const subjectRef = validateMain({ - uri: existingTrack.uri, + uri: trackWithUri.uri, cid: subjectRecord.data.cid, }); if (!subjectRef.success) { @@ -188,9 +283,12 @@ export async function likeTrack( }); const uri = res.data.uri; console.log(`Like record created at: ${uri}`); - created = await ctx.client.db.loved_tracks.update(created.xata_id, { - uri, - }); + + [created] = await ctx.db + .update(lovedTracks) + .set({ uri }) + .where(eq(lovedTracks.id, created.id)) + .returning(); } catch (e) { console.error(`Error creating like record: ${e.message}`); } @@ -206,34 +304,43 @@ export async function unLikeTrack( ctx: Context, trackSha256: string, user, - agent: Agent, + agent: Agent ) { - const track = await ctx.client.db.tracks - .filter("sha256", equals(trackSha256)) - .getFirst(); + const track = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.sha256, trackSha256)) + .limit(1) + .then((rows) => rows[0]); if (!track) { return; } - const lovedTrack = await ctx.client.db.loved_tracks - .filter("user_id", equals(user.xata_id)) - .filter("track_id", equals(track.xata_id)) - .getFirst(); + const lovedTrack = await ctx.db + .select() + .from(lovedTracks) + .where( + and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, track.id)) + ) + .limit(1) + .then((rows) => rows[0]); if (!lovedTrack) { return; } - const rkey = lovedTrack.uri.split("/").pop(); + const rkey = lovedTrack.uri?.split("/").pop(); await Promise.all([ - agent.com.atproto.repo.deleteRecord({ - repo: agent.assertDid, - collection: "app.rocksky.like", - rkey, - }), - ctx.client.db.loved_tracks.delete(lovedTrack.xata_id), + rkey + ? agent.com.atproto.repo.deleteRecord({ + repo: agent.assertDid, + collection: "app.rocksky.like", + rkey, + }) + : Promise.resolve(), + ctx.db.delete(lovedTracks).where(eq(lovedTracks.id, lovedTrack.id)), ]); const message = JSON.stringify(lovedTrack); @@ -244,18 +351,19 @@ export async function getLovedTracks( ctx: Context, user, size = 10, - offset = 0, + offset = 0 ) { - const lovedTracks = await ctx.client.db.loved_tracks - .select(["track_id.*"]) - .filter("user_id", equals(user.xata_id)) - .sort("xata_createdat", "desc") - .getPaginated({ - pagination: { - size, - offset, - }, - }); + const lovedTracksData = await ctx.db + .select({ + lovedTrack: lovedTracks, + track: tracks, + }) + .from(lovedTracks) + .innerJoin(tracks, eq(lovedTracks.trackId, tracks.id)) + .where(eq(lovedTracks.userId, user.id)) + .orderBy(desc(lovedTracks.createdAt)) + .limit(size) + .offset(offset); - return lovedTracks.records; + return lovedTracksData.map((item) => item.track); } diff --git a/apps/api/src/nowplaying/nowplaying.service.ts b/apps/api/src/nowplaying/nowplaying.service.ts index d66a7e16..73d45391 100644 --- a/apps/api/src/nowplaying/nowplaying.service.ts +++ b/apps/api/src/nowplaying/nowplaying.service.ts @@ -1,15 +1,27 @@ import type { Agent } from "@atproto/api"; import { TID } from "@atproto/common"; -import { equals } from "@xata.io/client"; import chalk from "chalk"; import type { Context } from "context"; import dayjs from "dayjs"; +import { and, eq, gte, lte, or } from "drizzle-orm"; import * as Album from "lexicon/types/app/rocksky/album"; import * as Artist from "lexicon/types/app/rocksky/artist"; import * as Scrobble from "lexicon/types/app/rocksky/scrobble"; import * as Song from "lexicon/types/app/rocksky/song"; +import { deepSnakeCaseKeys } from "lib"; import { createHash } from "node:crypto"; import type { Track } from "types/track"; +import albumTracks from "../schema/album-tracks"; +import albums from "../schema/albums"; +import artistAlbums from "../schema/artist-albums"; +import artistTracks from "../schema/artist-tracks"; +import artists from "../schema/artists"; +import scrobbles from "../schema/scrobbles"; +import tracks from "../schema/tracks"; +import userAlbums from "../schema/user-albums"; +import userArtists from "../schema/user-artists"; +import userTracks from "../schema/user-tracks"; +import users from "../schema/users"; export async function putArtistRecord( track: Track, @@ -200,10 +212,22 @@ async function putScrobbleRecord( } export async function publishScrobble(ctx: Context, id: string) { - const scrobble = await ctx.client.db.scrobbles - .select(["*", "track_id.*", "album_id.*", "artist_id.*", "user_id.*"]) - .filter("xata_id", equals(id)) - .getFirst(); + const scrobble = await ctx.db + .select({ + scrobble: scrobbles, + track: tracks, + album: albums, + artist: artists, + user: users, + }) + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .innerJoin(albums, eq(scrobbles.albumId, albums.id)) + .innerJoin(artists, eq(scrobbles.artistId, artists.id)) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .where(eq(scrobbles.id, id)) + .limit(1) + .then((rows) => rows[0]); const [ _user_album, @@ -213,93 +237,119 @@ export async function publishScrobble(ctx: Context, id: string) { artist_track, artist_album, ] = await Promise.all([ - ctx.client.db.user_albums - .select(["*"]) - .filter("album_id.xata_id", equals(scrobble.album_id.xata_id)) - .getFirst(), - ctx.client.db.user_artists - .select(["*"]) - .filter("artist_id.xata_id", equals(scrobble.artist_id.xata_id)) - .getFirst(), - ctx.client.db.user_tracks - .select(["*"]) - .filter("track_id.xata_id", equals(scrobble.track_id.xata_id)) - .getFirst(), - ctx.client.db.album_tracks - .select(["*"]) - .filter("track_id.xata_id", equals(scrobble.track_id.xata_id)) - .getFirst(), - ctx.client.db.artist_tracks - .select(["*"]) - .filter("track_id.xata_id", equals(scrobble.track_id.xata_id)) - .getFirst(), - ctx.client.db.artist_albums - .select(["*"]) - .filter("album_id.xata_id", equals(scrobble.album_id.xata_id)) - .filter("artist_id.xata_id", equals(scrobble.artist_id.xata_id)) - .getFirst(), + ctx.db + .select() + .from(userAlbums) + .where(eq(userAlbums.albumId, scrobble.album.id)) + .limit(1) + .then((rows) => rows[0]), + ctx.db + .select() + .from(userArtists) + .where(eq(userArtists.artistId, scrobble.artist.id)) + .limit(1) + .then((rows) => rows[0]), + ctx.db + .select() + .from(userTracks) + .where(eq(userTracks.trackId, scrobble.track.id)) + .limit(1) + .then((rows) => rows[0]), + ctx.db + .select() + .from(albumTracks) + .where(eq(albumTracks.trackId, scrobble.track.id)) + .limit(1) + .then((rows) => rows[0]), + ctx.db + .select() + .from(artistTracks) + .where(eq(artistTracks.trackId, scrobble.track.id)) + .limit(1) + .then((rows) => rows[0]), + ctx.db + .select() + .from(artistAlbums) + .where( + and( + eq(artistAlbums.albumId, scrobble.album.id), + eq(artistAlbums.artistId, scrobble.artist.id) + ) + ) + .limit(1) + .then((rows) => rows[0]), ]); let user_artist = _user_artist; if (!user_artist) { - await ctx.client.db.user_artists.create({ - user_id: scrobble.user_id.xata_id, - artist_id: scrobble.artist_id.xata_id, - uri: scrobble.artist_id.uri, + await ctx.db.insert(userArtists).values({ + userId: scrobble.user.id, + artistId: scrobble.artist.id, + uri: scrobble.artist.uri, scrobbles: 1, }); - user_artist = await ctx.client.db.user_artists - .select(["*"]) - .filter("artist_id.xata_id", equals(scrobble.artist_id.xata_id)) - .getFirst(); + user_artist = await ctx.db + .select() + .from(userArtists) + .where(eq(userArtists.artistId, scrobble.artist.id)) + .limit(1) + .then((rows) => rows[0]); } let user_album = _user_album; if (!user_album) { - await ctx.client.db.user_albums.create({ - user_id: scrobble.user_id.xata_id, - album_id: scrobble.album_id.xata_id, - uri: scrobble.album_id.uri, + await ctx.db.insert(userAlbums).values({ + userId: scrobble.user.id, + albumId: scrobble.album.id, + uri: scrobble.album.uri, scrobbles: 1, }); - user_album = await ctx.client.db.user_albums - .select(["*"]) - .filter("album_id.xata_id", equals(scrobble.album_id.xata_id)) - .getFirst(); + user_album = await ctx.db + .select() + .from(userAlbums) + .where(eq(userAlbums.albumId, scrobble.album.id)) + .limit(1) + .then((rows) => rows[0]); } let user_track = _user_track; if (!user_track) { - await ctx.client.db.user_tracks.create({ - user_id: scrobble.user_id.xata_id, - track_id: scrobble.track_id.xata_id, - uri: scrobble.track_id.uri, + await ctx.db.insert(userTracks).values({ + userId: scrobble.user.id, + trackId: scrobble.track.id, + uri: scrobble.track.uri, scrobbles: 1, }); - user_track = await ctx.client.db.user_tracks - .select(["*"]) - .filter("track_id.xata_id", equals(scrobble.track_id.xata_id)) - .getFirst(); + user_track = await ctx.db + .select() + .from(userTracks) + .where(eq(userTracks.trackId, scrobble.track.id)) + .limit(1) + .then((rows) => rows[0]); } - const message = JSON.stringify({ - scrobble, - user_album, - user_artist, - user_track, - album_track, - artist_track, - artist_album, - }); + const message = JSON.stringify( + deepSnakeCaseKeys({ + scrobble, + user_album, + user_artist, + user_track, + album_track, + artist_track, + artist_album, + }) + ); ctx.nc.publish("rocksky.scrobble", Buffer.from(message)); - const trackMessage = JSON.stringify({ - track: scrobble.track_id, - album_track, - artist_track, - artist_album, - }); + const trackMessage = JSON.stringify( + deepSnakeCaseKeys({ + track: scrobble.track, + album_track, + artist_track, + artist_album, + }) + ); ctx.nc.publish("rocksky.track", Buffer.from(trackMessage)); } @@ -312,21 +362,26 @@ export async function scrobbleTrack( ): Promise { // check if scrobble already exists (user did + timestamp) const scrobbleTime = dayjs.unix(track.timestamp || dayjs().unix()); - const existingScrobble = await ctx.client.db.scrobbles - .filter("user_id.did", equals(userDid)) - .filter("track_id.title", equals(track.title)) - .filter("track_id.artist", equals(track.artist)) - .filter({ - $all: [ - { - timestamp: { - $ge: scrobbleTime.subtract(5, "seconds").toISOString(), - }, - }, - { timestamp: { $le: scrobbleTime.add(5, "seconds").toISOString() } }, - ], + const existingScrobble = await ctx.db + .select({ + scrobble: scrobbles, + user: users, + track: tracks, }) - .getFirst(); + .from(scrobbles) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .where( + and( + eq(users.did, userDid), + eq(tracks.title, track.title), + eq(tracks.artist, track.artist), + gte(scrobbles.timestamp, scrobbleTime.subtract(5, "seconds").toDate()), + lte(scrobbles.timestamp, scrobbleTime.add(5, "seconds").toDate()) + ) + ) + .limit(1) + .then((rows) => rows[0]); if (existingScrobble) { console.log( @@ -337,10 +392,12 @@ export async function scrobbleTrack( return; } - let existingTrack = await ctx.client.db.tracks - .filter( - "sha256", - equals( + let existingTrack = await ctx.db + .select() + .from(tracks) + .where( + eq( + tracks.sha256, createHash("sha256") .update( `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() @@ -348,73 +405,93 @@ export async function scrobbleTrack( .digest("hex") ) ) - .getFirst(); - - if (existingTrack && !existingTrack.album_uri) { - const album = await ctx.client.db.albums - .filter( - "sha256", - equals( + .limit(1) + .then((rows) => rows[0]); + + if (existingTrack && !existingTrack.albumUri) { + const album = await ctx.db + .select() + .from(albums) + .where( + eq( + albums.sha256, createHash("sha256") .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); if (album) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - album_uri: album.uri, - }); + await ctx.db + .update(tracks) + .set({ albumUri: album.uri }) + .where(eq(tracks.id, existingTrack.id)); } } - if (existingTrack && !existingTrack.artist_uri) { - const artist = await ctx.client.db.artists - .filter( - "sha256", - equals( + if (existingTrack && !existingTrack.artistUri) { + const artist = await ctx.db + .select() + .from(artists) + .where( + eq( + artists.sha256, createHash("sha256") .update(track.albumArtist.toLowerCase()) .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); if (artist) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - artist_uri: artist.uri, - }); + await ctx.db + .update(tracks) + .set({ artistUri: artist.uri }) + .where(eq(tracks.id, existingTrack.id)); } } - const userTrack = await ctx.client.db.user_tracks - .filter({ - "track_id.xata_id": existingTrack?.xata_id, - "user_id.did": userDid, + const userTrack = await ctx.db + .select({ + userTrack: userTracks, + track: tracks, + user: users, }) - .getFirst(); - - if (!existingTrack?.uri || !userTrack?.uri?.includes(userDid)) { + .from(userTracks) + .innerJoin(tracks, eq(userTracks.trackId, tracks.id)) + .innerJoin(users, eq(userTracks.userId, users.id)) + .where(and(eq(tracks.id, existingTrack?.id || ""), eq(users.did, userDid))) + .limit(1) + .then((rows) => rows[0]); + + if (!existingTrack?.uri || !userTrack?.userTrack.uri?.includes(userDid)) { await putSongRecord(track, agent); } - const existingAlbum = await ctx.client.db.albums - .filter( - "sha256", - equals( + const existingAlbum = await ctx.db + .select() + .from(albums) + .where( + eq( + albums.sha256, createHash("sha256") .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); let tries = 0; while (!existingTrack && tries < 30) { console.log(`Song not found, trying again: ${chalk.magenta(tries + 1)}`); - existingTrack = await ctx.client.db.tracks - .filter( - "sha256", - equals( + existingTrack = await ctx.db + .select() + .from(tracks) + .where( + eq( + tracks.sha256, createHash("sha256") .update( `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() @@ -422,7 +499,8 @@ export async function scrobbleTrack( .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); await new Promise((resolve) => setTimeout(resolve, 1000)); tries += 1; } @@ -433,54 +511,73 @@ export async function scrobbleTrack( if (existingTrack) { console.log( - `Song found: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries` + `Song found: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries` ); } - const existingArtist = await ctx.client.db.artists - .filter({ - $any: [ - { - sha256: createHash("sha256") - .update(track.albumArtist.toLocaleLowerCase()) - .digest("hex"), - }, - { - sha256: createHash("sha256") - .update(track.artist.toLocaleLowerCase()) - .digest("hex"), - }, - ], - }) - .getFirst(); - - const userArtist = await ctx.client.db.user_artists - .filter({ - "artist_id.xata_id": existingArtist?.xata_id, - "user_id.did": userDid, + const existingArtist = await ctx.db + .select() + .from(artists) + .where( + or( + eq( + artists.sha256, + createHash("sha256") + .update(track.albumArtist.toLowerCase()) + .digest("hex") + ), + eq( + artists.sha256, + createHash("sha256").update(track.artist.toLowerCase()).digest("hex") + ) + ) + ) + .limit(1) + .then((rows) => rows[0]); + + const userArtist = await ctx.db + .select({ + userArtist: userArtists, + artist: artists, + user: users, }) - .getFirst(); + .from(userArtists) + .innerJoin(artists, eq(userArtists.artistId, artists.id)) + .innerJoin(users, eq(userArtists.userId, users.id)) + .where( + and(eq(artists.id, existingArtist?.id || ""), eq(users.did, userDid)) + ) + .limit(1) + .then((rows) => rows[0]); - if (!existingArtist?.uri || !userArtist?.uri?.includes(userDid)) { + if (!existingArtist?.uri || !userArtist?.userArtist.uri?.includes(userDid)) { await putArtistRecord(track, agent); } - const userAlbum = await ctx.client.db.user_albums - .filter({ - "album_id.xata_id": existingAlbum?.xata_id, - "user_id.did": userDid, + const userAlbum = await ctx.db + .select({ + userAlbum: userAlbums, + album: albums, + user: users, }) - .getFirst(); - - if (!existingAlbum?.uri || !userAlbum?.uri?.includes(userDid)) { + .from(userAlbums) + .innerJoin(albums, eq(userAlbums.albumId, albums.id)) + .innerJoin(users, eq(userAlbums.userId, users.id)) + .where(and(eq(albums.id, existingAlbum?.id || ""), eq(users.did, userDid))) + .limit(1) + .then((rows) => rows[0]); + + if (!existingAlbum?.uri || !userAlbum?.userAlbum.uri?.includes(userDid)) { await putAlbumRecord(track, agent); } tries = 0; - existingTrack = await ctx.client.db.tracks - .filter( - "sha256", - equals( + existingTrack = await ctx.db + .select() + .from(tracks) + .where( + eq( + tracks.sha256, createHash("sha256") .update( `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() @@ -488,20 +585,19 @@ export async function scrobbleTrack( .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); - while ( - !existingTrack?.artist_uri && - !existingTrack?.album_uri && - tries < 30 - ) { + while (!existingTrack?.artistUri && !existingTrack?.albumUri && tries < 30) { console.log( `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}` ); - existingTrack = await ctx.client.db.tracks - .filter( - "sha256", - equals( + existingTrack = await ctx.db + .select() + .from(tracks) + .where( + eq( + tracks.sha256, createHash("sha256") .update( `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() @@ -509,49 +605,59 @@ export async function scrobbleTrack( .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); // start update artist uri if it is not set - if (existingTrack && !existingTrack.artist_uri) { - const artist = await ctx.client.db.artists - .filter( - "sha256", - equals( + if (existingTrack && !existingTrack.artistUri) { + const artist = await ctx.db + .select() + .from(artists) + .where( + eq( + artists.sha256, createHash("sha256") .update(track.albumArtist.toLowerCase()) .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); if (artist) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - artist_uri: artist.uri, - }); + await ctx.db + .update(tracks) + .set({ artistUri: artist.uri }) + .where(eq(tracks.id, existingTrack.id)); } } // end update artist uri // start update album uri if it is not set - if (existingTrack && !existingTrack.album_uri) { - const album = await ctx.client.db.albums - .filter( - "sha256", - equals( + if (existingTrack && !existingTrack.albumUri) { + const album = await ctx.db + .select() + .from(albums) + .where( + eq( + albums.sha256, createHash("sha256") .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) .digest("hex") ) ) - .getFirst(); + .limit(1) + .then((rows) => rows[0]); if (album) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - album_uri: album.uri, - }); - - if (!album.artist_uri && existingTrack?.artist_uri) { - await ctx.client.db.albums.update(album.xata_id, { - artist_uri: existingTrack.artist_uri, - }); + await ctx.db + .update(tracks) + .set({ albumUri: album.uri }) + .where(eq(tracks.id, existingTrack.id)); + + if (!album.artistUri && existingTrack?.artistUri) { + await ctx.db + .update(albums) + .set({ artistUri: existingTrack.artistUri }) + .where(eq(albums.id, album.id)); } } } @@ -561,13 +667,13 @@ export async function scrobbleTrack( tries += 1; } - if (tries === 30 && !existingTrack?.artist_uri) { + if (tries === 30 && !existingTrack?.artistUri) { console.log(`Artist uri not ready after ${chalk.magenta("30 tries")}`); } - if (existingTrack?.artist_uri) { + if (existingTrack?.artistUri) { console.log( - `Artist uri ready: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries` + `Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries` ); } @@ -577,38 +683,63 @@ export async function scrobbleTrack( tries = 0; let scrobble = null; while (!scrobble && tries < 30) { - scrobble = await ctx.client.db.scrobbles - .select(["*", "track_id.*", "album_id.*", "artist_id.*", "user_id.*"]) - .filter("uri", equals(scrobbleUri)) - .getFirst(); + scrobble = await ctx.db + .select({ + scrobble: scrobbles, + track: tracks, + album: albums, + artist: artists, + user: users, + }) + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .innerJoin(albums, eq(scrobbles.albumId, albums.id)) + .innerJoin(artists, eq(scrobbles.artistId, artists.id)) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .where(eq(scrobbles.uri, scrobbleUri)) + .limit(1) + .then((rows) => rows[0]); if ( scrobble && - scrobble.album_id && - !scrobble.album_id.artist_uri && - scrobble.artist_id.uri + scrobble.album && + !scrobble.album.artistUri && + scrobble.artist.uri ) { - await ctx.client.db.albums.update(scrobble.album_id.xata_id, { - artist_uri: scrobble.artist_id.uri, - }); + await ctx.db + .update(albums) + .set({ artistUri: scrobble.artist.uri }) + .where(eq(albums.id, scrobble.album.id)); } - scrobble = await ctx.client.db.scrobbles - .select(["*", "track_id.*", "album_id.*", "artist_id.*", "user_id.*"]) - .filter("uri", equals(scrobbleUri)) - .getFirst(); + scrobble = await ctx.db + .select({ + scrobble: scrobbles, + track: tracks, + album: albums, + artist: artists, + user: users, + }) + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .innerJoin(albums, eq(scrobbles.albumId, albums.id)) + .innerJoin(artists, eq(scrobbles.artistId, artists.id)) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .where(eq(scrobbles.uri, scrobbleUri)) + .limit(1) + .then((rows) => rows[0]); if ( scrobble && - scrobble.track_id && - scrobble.album_id && - scrobble.artist_id && - scrobble.album_id.artist_uri && - scrobble.track_id.artist_uri && - scrobble.track_id.album_uri + scrobble.track && + scrobble.album && + scrobble.artist && + scrobble.album.artistUri && + scrobble.track.artistUri && + scrobble.track.albumUri ) { console.log("Scrobble found after ", chalk.magenta(tries + 1), " tries"); - await publishScrobble(ctx, scrobble.xata_id); + await publishScrobble(ctx, scrobble.scrobble.id); console.log("Scrobble published"); break; } diff --git a/apps/api/src/scripts/avatar.ts b/apps/api/src/scripts/avatar.ts index 0f0ff95d..d79f7237 100644 --- a/apps/api/src/scripts/avatar.ts +++ b/apps/api/src/scripts/avatar.ts @@ -1,17 +1,18 @@ -import { equals } from "@xata.io/client"; import { ctx } from "context"; -import { eq } from "drizzle-orm"; +import { eq, or } from "drizzle-orm"; +import { deepSnakeCaseKeys } from "lib"; import _ from "lodash"; import users from "schema/users"; const args = process.argv.slice(2); for (const did of args) { - const user = await ctx.client.db.users - .filter({ - $any: [{ did }, { handle: did }], - }) - .getFirst(); + const [user] = await ctx.db + .select() + .from(users) + .where(or(eq(users.did, did), eq(users.handle, did))) + .limit(1) + .execute(); if (!user) { console.log(`User ${did} not found`); continue; @@ -41,14 +42,19 @@ for (const did of args) { .where(eq(users.did, user.did)) .execute(); - const u = await ctx.client.db.users - .select(["*"]) - .filter("did", equals(user.did)) - .getFirst(); + const [u] = await ctx.db + .select() + .from(users) + .where(eq(users.did, user.did)) + .limit(1) + .execute(); console.log(u); - ctx.nc.publish("rocksky.user", Buffer.from(JSON.stringify(u))); + ctx.nc.publish( + "rocksky.user", + Buffer.from(JSON.stringify(deepSnakeCaseKeys(u))) + ); } console.log("Done"); diff --git a/apps/api/src/scripts/sync.ts b/apps/api/src/scripts/sync.ts index 6f3f53e7..4bf4fad7 100644 --- a/apps/api/src/scripts/sync.ts +++ b/apps/api/src/scripts/sync.ts @@ -1,102 +1,116 @@ -import { equals } from "@xata.io/client"; import chalk from "chalk"; import { ctx } from "context"; +import { desc, eq, or } from "drizzle-orm"; import { createHash } from "node:crypto"; import { publishScrobble } from "nowplaying/nowplaying.service"; +import albums from "../schema/albums"; +import artists from "../schema/artists"; +import scrobbles from "../schema/scrobbles"; +import tracks from "../schema/tracks"; +import users from "../schema/users"; const args = process.argv.slice(2); async function updateUris(did: string) { - const { records } = await ctx.client.db.scrobbles - .select(["track_id.*", "user_id.*"]) - .filter({ - $any: [{ "user_id.did": did }, { "user_id.handle": did }], + // Get scrobbles with track and user data + const records = await ctx.db + .select({ + track: tracks, + user: users, }) - .getPaginated({ - pagination: { - size: process.env.SYNC_SIZE ? parseInt(process.env.SYNC_SIZE, 10) : 20, - }, - sort: [{ xata_createdat: "desc" }], - }); - for (const { track_id: track } of records) { - const existingTrack = await ctx.client.db.tracks - .filter( - "sha256", - equals( - createHash("sha256") - .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() - ) - .digest("hex") - ) - ) - .getFirst(); - - if (existingTrack && !existingTrack.album_uri) { - console.log(`Updating album uri for ${chalk.cyan(track.xata_id)} ...`); - const album = await ctx.client.db.albums - .filter( - "sha256", - equals( - createHash("sha256") - .update(`${track.album} - ${track.album_artist}`.toLowerCase()) - .digest("hex") - ) - ) - .getFirst(); + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .where(or(eq(users.did, did), eq(users.handle, did))) + .orderBy(desc(scrobbles.createdAt)) + .limit(process.env.SYNC_SIZE ? parseInt(process.env.SYNC_SIZE, 10) : 20); + + for (const { track } of records) { + const trackHash = createHash("sha256") + .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) + .digest("hex"); + + const existingTrack = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.sha256, trackHash)) + .limit(1) + .then((rows) => rows[0]); + + if (existingTrack && !existingTrack.albumUri) { + console.log(`Updating album uri for ${chalk.cyan(track.id)} ...`); + + const albumHash = createHash("sha256") + .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) + .digest("hex"); + + const album = await ctx.db + .select() + .from(albums) + .where(eq(albums.sha256, albumHash)) + .limit(1) + .then((rows) => rows[0]); + if (album) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - album_uri: album.uri, - }); + await ctx.db + .update(tracks) + .set({ albumUri: album.uri }) + .where(eq(tracks.id, existingTrack.id)); } } - if (existingTrack && !existingTrack.artist_uri) { - console.log(`Updating artist uri for ${chalk.cyan(track.xata_id)} ...`); - const artist = await ctx.client.db.artists - .filter( - "sha256", - equals( - createHash("sha256") - .update(track.album_artist.toLowerCase()) - .digest("hex") - ) - ) - .getFirst(); + if (existingTrack && !existingTrack.artistUri) { + console.log(`Updating artist uri for ${chalk.cyan(track.id)} ...`); + + const artistHash = createHash("sha256") + .update(track.albumArtist.toLowerCase()) + .digest("hex"); + + const artist = await ctx.db + .select() + .from(artists) + .where(eq(artists.sha256, artistHash)) + .limit(1) + .then((rows) => rows[0]); + if (artist) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - artist_uri: artist.uri, - }); + await ctx.db + .update(tracks) + .set({ artistUri: artist.uri }) + .where(eq(tracks.id, existingTrack.id)); } } - const album = await ctx.client.db.albums - .filter( - "sha256", - equals( - createHash("sha256") - .update(`${track.album} - ${track.album_artist}`.toLowerCase()) - .digest("hex") - ) - ) - .getFirst(); - - if (existingTrack && !album.artist_uri) { - console.log(`Updating artist uri for ${chalk.cyan(album.xata_id)} ...`); - const artist = await ctx.client.db.artists - .filter( - "sha256", - equals( - createHash("sha256") - .update(track.album_artist.toLowerCase()) - .digest("hex") - ) - ) - .getFirst(); + const albumHash = createHash("sha256") + .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) + .digest("hex"); + + const album = await ctx.db + .select() + .from(albums) + .where(eq(albums.sha256, albumHash)) + .limit(1) + .then((rows) => rows[0]); + + if (existingTrack && album && !album.artistUri) { + console.log(`Updating artist uri for ${chalk.cyan(album.id)} ...`); + + const artistHash = createHash("sha256") + .update(track.albumArtist.toLowerCase()) + .digest("hex"); + + const artist = await ctx.db + .select() + .from(artists) + .where(eq(artists.sha256, artistHash)) + .limit(1) + .then((rows) => rows[0]); + if (artist) { - await ctx.client.db.albums.update(album.xata_id, { - artist_uri: artist.uri, - }); + await ctx.db + .update(albums) + .set({ artistUri: artist.uri }) + .where(eq(albums.id, album.id)); } } } @@ -111,19 +125,20 @@ if (args.includes("--background")) { await new Promise((resolve) => setTimeout(resolve, 15000)); console.log(`Syncing scrobbles ${chalk.magenta(did)} ...`); await updateUris(did); - const { records } = await ctx.client.db.scrobbles - .filter({ - $any: [{ "user_id.did": did }, { "user_id.handle": did }], + + const records = await ctx.db + .select({ + scrobble: scrobbles, }) - .getPaginated({ - pagination: { - size: 5, - }, - sort: [{ xata_createdat: "desc" }], - }); - for (const scrobble of records) { - console.log(`Syncing scrobble ${chalk.cyan(scrobble.xata_id)} ...`); - await publishScrobble(ctx, scrobble.xata_id); + .from(scrobbles) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .where(or(eq(users.did, did), eq(users.handle, did))) + .orderBy(desc(scrobbles.createdAt)) + .limit(5); + + for (const { scrobble } of records) { + console.log(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`); + await publishScrobble(ctx, scrobble.id); } } process.exit(0); @@ -133,19 +148,19 @@ for (const arg of args) { console.log(`Syncing scrobbles ${chalk.magenta(arg)} ...`); await updateUris(arg); - const { records } = await ctx.client.db.scrobbles - .filter({ - $any: [{ "user_id.did": arg }, { "user_id.handle": arg }], + const records = await ctx.db + .select({ + scrobble: scrobbles, }) - .getPaginated({ - pagination: { - size: process.env.SYNC_SIZE ? parseInt(process.env.SYNC_SIZE) : 20, - }, - sort: [{ xata_createdat: "desc" }], - }); - for (const scrobble of records) { - console.log(`Syncing scrobble ${chalk.cyan(scrobble.xata_id)} ...`); - await publishScrobble(ctx, scrobble.xata_id); + .from(scrobbles) + .innerJoin(users, eq(scrobbles.userId, users.id)) + .where(or(eq(users.did, arg), eq(users.handle, arg))) + .orderBy(desc(scrobbles.createdAt)) + .limit(process.env.SYNC_SIZE ? parseInt(process.env.SYNC_SIZE) : 20); + + for (const { scrobble } of records) { + console.log(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`); + await publishScrobble(ctx, scrobble.id); } console.log(`Synced ${chalk.greenBright(records.length)} scrobbles`); } diff --git a/apps/api/src/search/app.ts b/apps/api/src/search/app.ts deleted file mode 100644 index 798b3e28..00000000 --- a/apps/api/src/search/app.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ctx } from "context"; -import { Hono } from "hono"; -import { requestCounter } from "metrics"; - -const app = new Hono(); - -app.get("/", async (c) => { - requestCounter.add(1, { method: "GET", route: "/search" }); - const query = c.req.query("q"); - const size = +c.req.query("size") || 10; - const offset = +c.req.query("offset") || 0; - - if (!query) { - return c.json([]); - } - - const results = await ctx.client.search.all(query, { - tables: [ - { - table: "users", - target: ["handle"], - }, - { - table: "albums", - target: ["title"], - }, - { - table: "artists", - target: ["name"], - }, - { - table: "tracks", - target: ["title", "composer", "copyright_message"], - }, - { - table: "playlists", - target: ["name"], - }, - ], - fuzziness: 1, - prefix: "phrase", - page: { - size, - offset, - }, - }); - return c.json(results); -}); - -export default app; diff --git a/apps/api/src/shouts/shouts.service.ts b/apps/api/src/shouts/shouts.service.ts index 8d9b6b82..4f6ed358 100644 --- a/apps/api/src/shouts/shouts.service.ts +++ b/apps/api/src/shouts/shouts.service.ts @@ -1,44 +1,92 @@ import { type Agent, AtpAgent } from "@atproto/api"; import { TID } from "@atproto/common"; import type { Context } from "context"; +import { and, eq } from "drizzle-orm"; import * as LikeLexicon from "lexicon/types/app/rocksky/like"; import * as ShoutLexicon from "lexicon/types/app/rocksky/shout"; import { validateMain } from "lexicon/types/com/atproto/repo/strongRef"; import _ from "lodash"; import type { Shout } from "types/shout"; +import albums, { type SelectAlbum } from "../schema/albums"; +import artists, { type SelectArtist } from "../schema/artists"; +import profileShouts from "../schema/profile-shouts"; +import scrobbles, { type SelectScrobble } from "../schema/scrobbles"; +import shoutLikes from "../schema/shout-likes"; +import shouts from "../schema/shouts"; +import tracks, { type SelectTrack } from "../schema/tracks"; +import users, { type SelectUser } from "../schema/users"; export async function createShout( ctx: Context, shout: Shout, uri: string, user, - agent: Agent, + agent: Agent ) { - let album, artist, track, scrobble, profile, collection; + let album: SelectAlbum, + artist: SelectArtist, + track: SelectTrack, + scrobble: { + scrobble: SelectScrobble; + track: SelectTrack; + album: SelectAlbum; + artist: SelectArtist; + }, + profile: SelectUser, + collection: string; + if (uri.includes("app.rocksky.song")) { - track = await ctx.client.db.tracks.filter("uri", uri).getFirst(); + track = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.uri, uri)) + .limit(1) + .then((rows) => rows[0]); collection = "app.rocksky.song"; } else if (uri.includes("app.rocksky.album")) { - album = await ctx.client.db.albums.filter("uri", uri).getFirst(); + album = await ctx.db + .select() + .from(albums) + .where(eq(albums.uri, uri)) + .limit(1) + .then((rows) => rows[0]); collection = "app.rocksky.album"; } else if (uri.includes("app.rocksky.artist")) { - artist = await ctx.client.db.artists.filter("uri", uri).getFirst(); + artist = await ctx.db + .select() + .from(artists) + .where(eq(artists.uri, uri)) + .limit(1) + .then((rows) => rows[0]); collection = "app.rocksky.artist"; } else if (uri.includes("app.rocksky.scrobble")) { - scrobble = await ctx.client.db.scrobbles - .select(["track_id.*", "album_id.*", "artist_id.*", "uri"]) - .filter("uri", uri) - .getFirst(); + scrobble = await ctx.db + .select({ + scrobble: scrobbles, + track: tracks, + album: albums, + artist: artists, + }) + .from(scrobbles) + .innerJoin(tracks, eq(scrobbles.trackId, tracks.id)) + .innerJoin(albums, eq(scrobbles.albumId, albums.id)) + .innerJoin(artists, eq(scrobbles.artistId, artists.id)) + .where(eq(scrobbles.uri, uri)) + .limit(1) + .then((rows) => rows[0]); collection = "app.rocksky.scrobble"; } else { - profile = await ctx.client.db.users - .filter("did", uri.split("at://").pop()) - .getFirst(); + profile = await ctx.db + .select() + .from(users) + .where(eq(users.did, uri.split("at://").pop())) + .limit(1) + .then((rows) => rows[0]); collection = "app.bsky.actor.profile"; } const subjectUri = - album?.uri || track?.uri || artist?.uri || scrobble?.uri || "self"; + album?.uri || track?.uri || artist?.uri || scrobble?.scrobble.uri || "self"; const subjectRecord = await agent.com.atproto.repo.getRecord({ repo: agent.assertDid, collection, @@ -79,20 +127,24 @@ export async function createShout( console.log(`Shout record created at: ${uri}`); - const createdShout = await ctx.client.db.shouts.create({ - content: shout.message, - uri, - author_id: user.xata_id, - album_id: album?.xata_id, - artist_id: artist?.xata_id, - track_id: track?.xata_id, - scrobble_id: scrobble?.xata_id, - }); + const createdShout = await ctx.db + .insert(shouts) + .values({ + content: shout.message, + uri, + authorId: user.id, + albumId: album?.id, + artistId: artist?.id, + trackId: track?.id, + scrobbleId: scrobble?.scrobble.id, + }) + .returning() + .then((rows) => rows[0]); if (profile) { - await ctx.client.db.profile_shouts.create({ - shout_id: createdShout.xata_id, - user_id: profile.xata_id, + await ctx.db.insert(profileShouts).values({ + shoutId: createdShout.id, + userId: profile.id, }); } } catch (e) { @@ -105,12 +157,25 @@ export async function replyShout( reply: Shout, shoutUri: string, user, - agent: Agent, + agent: Agent ) { - const shout = await ctx.client.db.shouts - .select(["track_id.*", "album_id.*", "artist_id.*", "scrobble_id.*", "uri"]) - .filter("uri", shoutUri) - .getFirst(); + const shout = await ctx.db + .select({ + shout: shouts, + track: tracks, + album: albums, + artist: artists, + scrobble: scrobbles, + }) + .from(shouts) + .leftJoin(tracks, eq(shouts.trackId, tracks.id)) + .leftJoin(albums, eq(shouts.albumId, albums.id)) + .leftJoin(artists, eq(shouts.artistId, artists.id)) + .leftJoin(scrobbles, eq(shouts.scrobbleId, scrobbles.id)) + .where(eq(shouts.uri, shoutUri)) + .limit(1) + .then((rows) => rows[0]); + if (!shout) { throw new Error("Shout not found"); } @@ -123,33 +188,33 @@ export async function replyShout( let collection = "app.bsky.actor.profile"; - if (shout.track_id) { + if (shout.track) { collection = "app.rocksky.song"; } - if (shout.album_id) { + if (shout.album) { collection = "app.rocksky.album"; } - if (shout.artist_id) { + if (shout.artist) { collection = "app.rocksky.artist"; } - if (shout.scrobble_id) { + if (shout.scrobble) { collection = "app.rocksky.scrobble"; } const subjectUri = - shout.track_id?.uri || - shout.album_id?.uri || - shout.artist_id?.uri || - shout.scrobble_id?.uri || + shout.track?.uri || + shout.album?.uri || + shout.artist?.uri || + shout.scrobble?.uri || profileRecord.uri; let service = await fetch( - `https://plc.directory/${subjectUri.split("/").slice(0, 3).join("/").split("at://")[1]}`, + `https://plc.directory/${subjectUri.split("/").slice(0, 3).join("/").split("at://")[1]}` ) - .then((res) => res.json()) + .then((res) => res.json<{ service: { seviceEndpoint: string }[] }>()) .then((data) => data.service); let atpAgent = new AtpAgent({ @@ -171,9 +236,9 @@ export async function replyShout( } service = await fetch( - `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`, + `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}` ) - .then((res) => res.json()) + .then((res) => res.json<{ service: { seviceEndpoint: string }[] }>()) .then((data) => data.service); atpAgent = new AtpAgent({ @@ -187,7 +252,7 @@ export async function replyShout( }); const parentRef = validateMain({ - uri: shout.uri, + uri: shout.shout.uri, cid: parentRecord.data.cid, }); if (!parentRef.success) { @@ -220,29 +285,32 @@ export async function replyShout( console.log(`Reply record created at: ${uri}`); - const createdShout = await ctx.client.db.shouts.create({ - content: reply.message, - uri, - parent_id: shout.xata_id, - author_id: user.xata_id, - track_id: shout.track_id?.xata_id, - album_id: shout.album_id?.xata_id, - artist_id: shout.artist_id?.xata_id, - scrobble_id: shout.scrobble_id?.xata_id, - }); - - if ( - !shout.track_id && - !shout.album_id && - !shout.artist_id && - !shout.scrobble_id - ) { - const profileShout = await ctx.client.db.profile_shouts - .filter("shout_id", shout.xata_id) - .getFirst(); - await ctx.client.db.profile_shouts.create({ - shout_id: createdShout.xata_id, - user_id: profileShout.user_id, + const createdShout = await ctx.db + .insert(shouts) + .values({ + content: reply.message, + uri, + parentId: shout.shout.id, + authorId: user.id, + trackId: shout.track?.id, + albumId: shout.album?.id, + artistId: shout.artist?.id, + scrobbleId: shout.scrobble?.id, + }) + .returning() + .then((rows) => rows[0]); + + if (!shout.track && !shout.album && !shout.artist && !shout.scrobble) { + const profileShout = await ctx.db + .select() + .from(profileShouts) + .where(eq(profileShouts.shoutId, shout.shout.id)) + .limit(1) + .then((rows) => rows[0]); + + await ctx.db.insert(profileShouts).values({ + shoutId: createdShout.id, + userId: profileShout.userId, }); } } catch (e) { @@ -254,23 +322,28 @@ export async function likeShout( ctx: Context, shoutUri: string, user, - agent: Agent, + agent: Agent ) { const rkey = TID.nextStr(); - const likes = await ctx.client.db.shout_likes - .filter({ - "shout_id.uri": shoutUri, - "user_id.xata_id": user.xata_id, + + const likes = await ctx.db + .select({ + like: shoutLikes, + shout: shouts, }) - .getFirst(); + .from(shoutLikes) + .innerJoin(shouts, eq(shoutLikes.shoutId, shouts.id)) + .where(and(eq(shouts.uri, shoutUri), eq(shoutLikes.userId, user.id))) + .limit(1) + .then((rows) => rows[0]); if (likes) { return; } const { service } = await fetch( - `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`, - ).then((res) => res.json()); + `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}` + ).then((res) => res.json<{ service: [{ serviceEndpoint: string }] }>()); const atpAgent = new AtpAgent({ service: _.get(service, "0.serviceEndpoint"), @@ -311,17 +384,21 @@ export async function likeShout( }); const uri = res.data.uri; console.log(`Like record created at: ${uri}`); - const shout = await ctx.client.db.shouts - .select(["xata_id", "uri"]) - .filter("uri", shoutUri) - .getFirst(); + + const shout = await ctx.db + .select() + .from(shouts) + .where(eq(shouts.uri, shoutUri)) + .limit(1) + .then((rows) => rows[0]); + if (!shout) { throw new Error("Shout not found"); } - await ctx.client.db.shout_likes.create({ - shout_id: shout.xata_id, - user_id: user.xata_id, + await ctx.db.insert(shoutLikes).values({ + shoutId: shout.id, + userId: user.id, uri, }); } catch (e) { @@ -333,20 +410,24 @@ export async function unlikeShout( ctx: Context, shoutUri: string, user, - agent: Agent, + agent: Agent ) { - const likes = await ctx.client.db.shout_likes - .filter({ - "shout_id.uri": shoutUri, - "user_id.xata_id": user.xata_id, + const likes = await ctx.db + .select({ + like: shoutLikes, + shout: shouts, }) - .getFirst(); + .from(shoutLikes) + .innerJoin(shouts, eq(shoutLikes.shoutId, shouts.id)) + .where(and(eq(shouts.uri, shoutUri), eq(shoutLikes.userId, user.id))) + .limit(1) + .then((rows) => rows[0]); if (!likes) { return; } - const rkey = likes.uri.split("/").pop(); + const rkey = likes.like.uri.split("/").pop(); await Promise.all([ agent.com.atproto.repo.deleteRecord({ @@ -354,6 +435,6 @@ export async function unlikeShout( collection: "app.rocksky.like", rkey, }), - ctx.client.db.shout_likes.delete(likes.xata_id), + ctx.db.delete(shoutLikes).where(eq(shoutLikes.id, likes.like.id)), ]); } diff --git a/apps/api/src/spotify/app.ts b/apps/api/src/spotify/app.ts index 87b02741..d35ac15a 100644 --- a/apps/api/src/spotify/app.ts +++ b/apps/api/src/spotify/app.ts @@ -1,5 +1,5 @@ -import { equals } from "@xata.io/client"; import { ctx } from "context"; +import { and, eq, or } from "drizzle-orm"; import { Hono } from "hono"; import jwt from "jsonwebtoken"; import { decrypt, encrypt } from "lib/crypto"; @@ -7,6 +7,11 @@ import { env } from "lib/env"; import { requestCounter } from "metrics"; import crypto, { createHash } from "node:crypto"; import { rateLimiter } from "ratelimiter"; +import lovedTracks from "schema/loved-tracks"; +import spotifyAccounts from "schema/spotify-accounts"; +import spotifyTokens from "schema/spotify-tokens"; +import tracks from "schema/tracks"; +import users from "schema/users"; import { emailSchema } from "types/email"; const app = new Hono(); @@ -17,7 +22,7 @@ app.use( limit: 10, // max Spotify API calls window: 15, // per 10 seconds keyPrefix: "spotify-ratelimit", - }), + }) ); app.get("/login", async (c) => { @@ -33,7 +38,13 @@ app.get("/login", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -44,7 +55,7 @@ app.get("/login", async (c) => { const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${env.SPOTIFY_CLIENT_ID}&response_type=code&redirect_uri=${env.SPOTIFY_REDIRECT_URI}&scope=user-read-private%20user-read-email%20user-read-playback-state%20user-read-currently-playing%20user-modify-playback-state%20playlist-modify-public%20playlist-modify-private%20playlist-read-private%20playlist-read-collaborative&state=${state}`; c.header( "Set-Cookie", - `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`, + `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure` ); return c.json({ redirectUrl }); }); @@ -67,7 +78,10 @@ app.get("/callback", async (c) => { client_secret: env.SPOTIFY_CLIENT_SECRET, }), }); - const { access_token, refresh_token } = await response.json(); + const { access_token, refresh_token } = await response.json<{ + access_token: string; + refresh_token: string; + }>(); if (!state) { return c.redirect(env.FRONTEND_URL); @@ -79,26 +93,51 @@ app.get("/callback", async (c) => { } ctx.kv.delete(state); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); if (!user) { return c.redirect(env.FRONTEND_URL); } - const spotifyToken = await ctx.client.db.spotify_tokens - .filter("user_id", equals(user.xata_id)) - .getFirst(); - - await ctx.client.db.spotify_tokens.createOrUpdate(spotifyToken?.xata_id, { - user_id: user.xata_id, - access_token: encrypt(access_token, env.SPOTIFY_ENCRYPTION_KEY), - refresh_token: encrypt(refresh_token, env.SPOTIFY_ENCRYPTION_KEY), - }); + const existingSpotifyToken = await ctx.db + .select() + .from(spotifyTokens) + .where(eq(spotifyTokens.userId, user.id)) + .limit(1) + .then((rows) => rows[0]); + + if (existingSpotifyToken) { + await ctx.db + .update(spotifyTokens) + .set({ + accessToken: encrypt(access_token, env.SPOTIFY_ENCRYPTION_KEY), + refreshToken: encrypt(refresh_token, env.SPOTIFY_ENCRYPTION_KEY), + }) + .where(eq(spotifyTokens.id, existingSpotifyToken.id)); + } else { + await ctx.db.insert(spotifyTokens).values({ + userId: user.id, + accessToken: encrypt(access_token, env.SPOTIFY_ENCRYPTION_KEY), + refreshToken: encrypt(refresh_token, env.SPOTIFY_ENCRYPTION_KEY), + }); + } - const spotifyUser = await ctx.client.db.spotify_accounts - .filter("user_id", equals(user.xata_id)) - .filter("is_beta_user", equals(true)) - .getFirst(); + const spotifyUser = await ctx.db + .select() + .from(spotifyAccounts) + .where( + and( + eq(spotifyAccounts.userId, user.id), + eq(spotifyAccounts.isBetaUser, true) + ) + ) + .limit(1) + .then((rows) => rows[0]); if (spotifyUser?.email) { ctx.nc.publish("rocksky.spotify.user", Buffer.from(spotifyUser.email)); @@ -120,7 +159,13 @@ app.post("/join", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -137,15 +182,13 @@ app.post("/join", async (c) => { const { email } = parsed.data; try { - await ctx.client.db.spotify_accounts.create({ - user_id: user.xata_id, + await ctx.db.insert(spotifyAccounts).values({ + userId: user.id, email, - is_beta_user: false, + isBetaUser: false, }); } catch (e) { - if ( - !e.message.includes("invalid record: column [user_id]: is not unique") - ) { + if (!e.message.includes("duplicate key value violates unique constraint")) { console.error(e.message); } else { throw e; @@ -179,29 +222,37 @@ app.get("/currently-playing", async (c) => { return c.text("Unauthorized"); } - const user = await ctx.client.db.users - .filter({ - $any: [{ did }, { handle: did }], - }) - .getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(or(eq(users.did, did), eq(users.handle, did))) + .limit(1) + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); } - const spotifyAccount = await ctx.client.db.spotify_accounts - .filter({ - $any: [{ "user_id.did": did }, { "user_id.handle": did }], + const spotifyAccount = await ctx.db + .select({ + spotifyAccount: spotifyAccounts, + user: users, }) - .getFirst(); + .from(spotifyAccounts) + .innerJoin(users, eq(spotifyAccounts.userId, users.id)) + .where(or(eq(users.did, did), eq(users.handle, did))) + .limit(1) + .then((rows) => rows[0]); if (!spotifyAccount) { c.status(401); return c.text("Unauthorized"); } - const cached = await ctx.redis.get(`${spotifyAccount.email}:current`); + const cached = await ctx.redis.get( + `${spotifyAccount.spotifyAccount.email}:current` + ); if (!cached) { return c.json({}); } @@ -210,23 +261,34 @@ app.get("/currently-playing", async (c) => { const sha256 = createHash("sha256") .update( - `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase(), + `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase() ) .digest("hex"); const [result, liked] = await Promise.all([ - ctx.client.db.tracks.filter("sha256", equals(sha256)).getFirst(), - ctx.client.db.loved_tracks - .filter("user_id", equals(user.xata_id)) - .filter("track_id.sha256", equals(sha256)) - .getFirst(), + ctx.db + .select() + .from(tracks) + .where(eq(tracks.sha256, sha256)) + .limit(1) + .then((rows) => rows[0]), + ctx.db + .select({ + lovedTrack: lovedTracks, + track: tracks, + }) + .from(lovedTracks) + .innerJoin(tracks, eq(lovedTracks.trackId, tracks.id)) + .where(and(eq(lovedTracks.userId, user.id), eq(tracks.sha256, sha256))) + .limit(1) + .then((rows) => rows[0]), ]); return c.json({ ...track, songUri: result?.uri, - artistUri: result?.artist_uri, - albumUri: result?.album_uri, + artistUri: result?.artistUri, + albumUri: result?.albumUri, liked: !!liked, sha256, }); @@ -246,16 +308,24 @@ app.put("/pause", async (c) => { return c.text("Unauthorized"); } - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); } - const spotifyToken = await ctx.client.db.spotify_tokens - .filter("user_id", equals(user.xata_id)) - .getFirst(); + const spotifyToken = await ctx.db + .select() + .from(spotifyTokens) + .where(eq(spotifyTokens.userId, user.id)) + .limit(1) + .then((rows) => rows[0]); if (!spotifyToken) { c.status(401); @@ -263,8 +333,8 @@ app.put("/pause", async (c) => { } const refreshToken = decrypt( - spotifyToken.refresh_token, - env.SPOTIFY_ENCRYPTION_KEY, + spotifyToken.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY ); // get new access token @@ -281,7 +351,9 @@ app.put("/pause", async (c) => { }), }); - const { access_token } = await newAccessToken.json(); + const { access_token } = await newAccessToken.json<{ + access_token: string; + }>(); const response = await fetch("https://api.spotify.com/v1/me/player/pause", { method: "PUT", @@ -312,16 +384,24 @@ app.put("/play", async (c) => { return c.text("Unauthorized"); } - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); } - const spotifyToken = await ctx.client.db.spotify_tokens - .filter("user_id", equals(user.xata_id)) - .getFirst(); + const spotifyToken = await ctx.db + .select() + .from(spotifyTokens) + .where(eq(spotifyTokens.userId, user.id)) + .limit(1) + .then((rows) => rows[0]); if (!spotifyToken) { c.status(401); @@ -329,8 +409,8 @@ app.put("/play", async (c) => { } const refreshToken = decrypt( - spotifyToken.refresh_token, - env.SPOTIFY_ENCRYPTION_KEY, + spotifyToken.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY ); // get new access token @@ -347,7 +427,9 @@ app.put("/play", async (c) => { }), }); - const { access_token } = await newAccessToken.json(); + const { access_token } = await newAccessToken.json<{ + access_token: string; + }>(); const response = await fetch("https://api.spotify.com/v1/me/player/play", { method: "PUT", @@ -378,16 +460,24 @@ app.post("/next", async (c) => { return c.text("Unauthorized"); } - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); } - const spotifyToken = await ctx.client.db.spotify_tokens - .filter("user_id", equals(user.xata_id)) - .getFirst(); + const spotifyToken = await ctx.db + .select() + .from(spotifyTokens) + .where(eq(spotifyTokens.userId, user.id)) + .limit(1) + .then((rows) => rows[0]); if (!spotifyToken) { c.status(401); @@ -395,8 +485,8 @@ app.post("/next", async (c) => { } const refreshToken = decrypt( - spotifyToken.refresh_token, - env.SPOTIFY_ENCRYPTION_KEY, + spotifyToken.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY ); // get new access token @@ -413,7 +503,9 @@ app.post("/next", async (c) => { }), }); - const { access_token } = await newAccessToken.json(); + const { access_token } = await newAccessToken.json<{ + access_token: string; + }>(); const response = await fetch("https://api.spotify.com/v1/me/player/next", { method: "POST", @@ -444,16 +536,24 @@ app.post("/previous", async (c) => { return c.text("Unauthorized"); } - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); } - const spotifyToken = await ctx.client.db.spotify_tokens - .filter("user_id", equals(user.xata_id)) - .getFirst(); + const spotifyToken = await ctx.db + .select() + .from(spotifyTokens) + .where(eq(spotifyTokens.userId, user.id)) + .limit(1) + .then((rows) => rows[0]); if (!spotifyToken) { c.status(401); @@ -461,8 +561,8 @@ app.post("/previous", async (c) => { } const refreshToken = decrypt( - spotifyToken.refresh_token, - env.SPOTIFY_ENCRYPTION_KEY, + spotifyToken.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY ); // get new access token @@ -479,7 +579,9 @@ app.post("/previous", async (c) => { }), }); - const { access_token } = await newAccessToken.json(); + const { access_token } = await newAccessToken.json<{ + access_token: string; + }>(); const response = await fetch( "https://api.spotify.com/v1/me/player/previous", @@ -488,7 +590,7 @@ app.post("/previous", async (c) => { headers: { Authorization: `Bearer ${access_token}`, }, - }, + } ); if (response.status === 403) { @@ -513,16 +615,24 @@ app.put("/seek", async (c) => { return c.text("Unauthorized"); } - const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(eq(users.did, did)) + .limit(1) + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); } - const spotifyToken = await ctx.client.db.spotify_tokens - .filter("user_id", equals(user.xata_id)) - .getFirst(); + const spotifyToken = await ctx.db + .select() + .from(spotifyTokens) + .where(eq(spotifyTokens.userId, user.id)) + .limit(1) + .then((rows) => rows[0]); if (!spotifyToken) { c.status(401); @@ -530,8 +640,8 @@ app.put("/seek", async (c) => { } const refreshToken = decrypt( - spotifyToken.refresh_token, - env.SPOTIFY_ENCRYPTION_KEY, + spotifyToken.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY ); // get new access token @@ -548,7 +658,9 @@ app.put("/seek", async (c) => { }), }); - const { access_token } = await newAccessToken.json(); + const { access_token } = await newAccessToken.json<{ + access_token: string; + }>(); const position = c.req.query("position_ms"); const response = await fetch( @@ -558,7 +670,7 @@ app.put("/seek", async (c) => { headers: { Authorization: `Bearer ${access_token}`, }, - }, + } ); if (response.status === 403) { diff --git a/apps/api/src/tracks/tracks.service.ts b/apps/api/src/tracks/tracks.service.ts index 79101a54..e90921be 100644 --- a/apps/api/src/tracks/tracks.service.ts +++ b/apps/api/src/tracks/tracks.service.ts @@ -1,27 +1,30 @@ import type { Agent } from "@atproto/api"; -import { equals } from "@xata.io/client"; import type { Context } from "context"; +import { and, eq } from "drizzle-orm"; +import { deepSnakeCaseKeys } from "lib"; import { createHash } from "node:crypto"; import { putAlbumRecord, putArtistRecord, putSongRecord, } from "nowplaying/nowplaying.service"; +import tables from "schema"; import type { Track } from "types/track"; +const { tracks, albums, artists, albumTracks, artistTracks, artistAlbums } = + tables; + export async function saveTrack(ctx: Context, track: Track, agent: Agent) { - const existingTrack = await ctx.client.db.tracks - .filter( - "sha256", - equals( - createHash("sha256") - .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), - ) - .digest("hex"), - ), - ) - .getFirst(); + const trackHash = createHash("sha256") + .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) + .digest("hex"); + + const existingTrack = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.sha256, trackHash)) + .limit(1) + .then((results) => results[0]); let trackUri = existingTrack?.uri; if (!existingTrack?.uri) { @@ -29,69 +32,73 @@ export async function saveTrack(ctx: Context, track: Track, agent: Agent) { } // start update existing track with album and artist uri - if (existingTrack && !existingTrack.album_uri) { - const album = await ctx.client.db.albums - .filter( - "sha256", - equals( - createHash("sha256") - .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) - .digest("hex"), - ), - ) - .getFirst(); + if (existingTrack && !existingTrack.albumUri) { + const albumHash = createHash("sha256") + .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) + .digest("hex"); + + const album = await ctx.db + .select() + .from(albums) + .where(eq(albums.sha256, albumHash)) + .limit(1) + .then((results) => results[0]); + if (album) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - album_uri: album.uri, - }); + await ctx.db + .update(tracks) + .set({ albumUri: album.uri }) + .where(eq(tracks.id, existingTrack.id)); } } - if (existingTrack && !existingTrack.artist_uri) { - const artist = await ctx.client.db.artists - .filter( - "sha256", - equals( - createHash("sha256") - .update(track.albumArtist.toLowerCase()) - .digest("hex"), - ), - ) - .getFirst(); + if (existingTrack && !existingTrack.artistUri) { + const artistHash = createHash("sha256") + .update(track.albumArtist.toLowerCase()) + .digest("hex"); + + const artist = await ctx.db + .select() + .from(artists) + .where(eq(artists.sha256, artistHash)) + .limit(1) + .then((results) => results[0]); + if (artist) { - await ctx.client.db.tracks.update(existingTrack.xata_id, { - artist_uri: artist.uri, - }); + await ctx.db + .update(tracks) + .set({ artistUri: artist.uri }) + .where(eq(tracks.id, existingTrack.id)); } } // end - const existingArtist = await ctx.client.db.artists - .filter( - "sha256", - equals( - createHash("sha256") - .update(track.albumArtist.toLocaleLowerCase()) - .digest("hex"), - ), - ) - .getFirst(); + const artistHash = createHash("sha256") + .update(track.albumArtist.toLowerCase()) + .digest("hex"); + + const existingArtist = await ctx.db + .select() + .from(artists) + .where(eq(artists.sha256, artistHash)) + .limit(1) + .then((results) => results[0]); let artistUri = existingArtist?.uri; if (!existingArtist?.uri) { artistUri = await putArtistRecord(track, agent); } - const existingAlbum = await ctx.client.db.albums - .filter( - "sha256", - equals( - createHash("sha256") - .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) - .digest("hex"), - ), - ) - .getFirst(); + const albumHash = createHash("sha256") + .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) + .digest("hex"); + + const existingAlbum = await ctx.db + .select() + .from(albums) + .where(eq(albums.sha256, albumHash)) + .limit(1) + .then((results) => results[0]); let albumUri = existingAlbum?.uri; if (!existingAlbum?.uri) { @@ -101,74 +108,106 @@ export async function saveTrack(ctx: Context, track: Track, agent: Agent) { let tries = 0; while (tries < 15) { - const track_id = await ctx.client.db.tracks - .filter("uri", equals(trackUri)) - .getFirst(); + const track_id = await ctx.db + .select() + .from(tracks) + .where(eq(tracks.uri, trackUri)) + .limit(1) + .then((results) => results[0]); - const album_id = await ctx.client.db.albums - .filter("uri", equals(albumUri)) - .getFirst(); + const album_id = await ctx.db + .select() + .from(albums) + .where(eq(albums.uri, albumUri)) + .limit(1) + .then((results) => results[0]); - const artist_id = await ctx.client.db.artists - .filter("uri", equals(artistUri)) - .getFirst(); + const artist_id = await ctx.db + .select() + .from(artists) + .where(eq(artists.uri, artistUri)) + .limit(1) + .then((results) => results[0]); if (!track_id || !album_id || !artist_id) { console.log( "Track not yet saved (uri not saved), retrying...", - tries + 1, + tries + 1 ); await new Promise((resolve) => setTimeout(resolve, 1000)); tries += 1; continue; } - const album_track = await ctx.client.db.album_tracks - .filter("album_id", equals(album_id.xata_id)) - .filter("track_id", equals(track_id.xata_id)) - .getFirst(); + const album_track = await ctx.db + .select() + .from(albumTracks) + .where( + and( + eq(albumTracks.albumId, album_id.id), + eq(albumTracks.trackId, track_id.id) + ) + ) + .limit(1) + .then((results) => results[0]); - const artist_track = await ctx.client.db.artist_tracks - .filter("artist_id", equals(artist_id.xata_id)) - .filter("track_id", equals(track_id.xata_id)) - .getFirst(); + const artist_track = await ctx.db + .select() + .from(artistTracks) + .where( + and( + eq(artistTracks.artistId, artist_id.id), + eq(artistTracks.trackId, track_id.id) + ) + ) + .limit(1) + .then((results) => results[0]); - const artist_album = await ctx.client.db.artist_albums - .filter("artist_id", equals(artist_id.xata_id)) - .filter("album_id", equals(album_id.xata_id)) - .getFirst(); + const artist_album = await ctx.db + .select() + .from(artistAlbums) + .where( + and( + eq(artistAlbums.artistId, artist_id.id), + eq(artistAlbums.albumId, album_id.id) + ) + ) + .limit(1) + .then((results) => results[0]); if (!album_track) { - await ctx.client.db.album_tracks.create({ - album_id: album_id.xata_id, - track_id: track_id.xata_id, + await ctx.db.insert(albumTracks).values({ + albumId: album_id.id, + trackId: track_id.id, }); } if (!artist_track) { - await ctx.client.db.artist_tracks.create({ - artist_id: artist_id.xata_id, - track_id: track_id.xata_id, + await ctx.db.insert(artistTracks).values({ + artistId: artist_id.id, + trackId: track_id.id, }); } if (!artist_album) { - await ctx.client.db.artist_albums.create({ - artist_id: artist_id.xata_id, - album_id: album_id.xata_id, + await ctx.db.insert(artistAlbums).values({ + artistId: artist_id.id, + albumId: album_id.id, }); } - if (track_id && !track_id.album_uri) { - await ctx.client.db.tracks.update(track_id.xata_id, { - album_uri: album_id.uri, - }); + if (track_id && !track_id.albumUri) { + await ctx.db + .update(tracks) + .set({ albumUri: album_id.uri }) + .where(eq(tracks.id, track_id.id)); } - if (track_id && !track_id.artist_uri) { - await ctx.client.db.tracks.update(track_id.xata_id, { - artist_uri: artist_id.uri, - }); + if (track_id && !track_id.artistUri) { + await ctx.db + .update(tracks) + .set({ artistUri: artist_id.uri }) + .where(eq(tracks.id, track_id.id)); } if ( @@ -176,17 +215,19 @@ export async function saveTrack(ctx: Context, track: Track, agent: Agent) { artist_track && artist_album && track_id && - track_id.album_uri && - track_id.artist_uri + track_id.albumUri && + track_id.artistUri ) { console.log("Track saved successfully after", tries + 1, "tries"); - const message = JSON.stringify({ - track: track_id, - album_track, - artist_track, - artist_album, - }); + const message = JSON.stringify( + deepSnakeCaseKeys({ + track: track_id, + album_track, + artist_track, + artist_album, + }) + ); ctx.nc.publish("rocksky.track", Buffer.from(message)); break; @@ -194,7 +235,7 @@ export async function saveTrack(ctx: Context, track: Track, agent: Agent) { tries += 1; console.log("Track not yet saved, retrying...", tries + 1); - if (tries == 15) { + if (tries === 15) { console.log(">>>"); console.log(album_track); console.log(artist_track); @@ -202,14 +243,14 @@ export async function saveTrack(ctx: Context, track: Track, agent: Agent) { console.log(artist_id); console.log(album_id); console.log(track_id); - console.log(track_id.album_uri); - console.log(track_id.artist_uri); + console.log(track_id.albumUri); + console.log(track_id.artistUri); console.log("<<<"); } await new Promise((resolve) => setTimeout(resolve, 1000)); } - if (tries == 15) { + if (tries === 15) { console.log("Failed to save track after 15 tries"); } } diff --git a/apps/api/src/users/app.ts b/apps/api/src/users/app.ts index 5a382c03..6d2a54e6 100644 --- a/apps/api/src/users/app.ts +++ b/apps/api/src/users/app.ts @@ -1,8 +1,8 @@ import type { BlobRef } from "@atproto/lexicon"; -import { equals } from "@xata.io/client"; import { ctx } from "context"; import { aliasedTable, + and, asc, count, desc, @@ -16,11 +16,11 @@ import jwt from "jsonwebtoken"; import * as Profile from "lexicon/types/app/bsky/actor/profile"; import { createAgent } from "lib/agent"; import { env } from "lib/env"; -import _ from "lodash"; import { likeTrack, unLikeTrack } from "lovedtracks/lovedtracks.service"; import { requestCounter } from "metrics"; import * as R from "ramda"; import tables from "schema"; +import { SelectUser } from "schema/users"; import { createShout, likeShout, @@ -39,25 +39,17 @@ app.get("/:did/likes", async (c) => { const size = +c.req.query("size") || 10; const offset = +c.req.query("offset") || 0; - const lovedTracks = await ctx.client.db.loved_tracks - .select(["track_id.*", "user_id.*"]) - .filter({ - $any: [ - { - "user_id.did": did, - }, - { - "user_id.handle": did, - }, - ], - }) - .sort("xata_createdat", "desc") - .getPaginated({ - pagination: { - size, - offset, - }, - }); + const lovedTracks = await ctx.db + .select() + .from(tables.lovedTracks) + .leftJoin(tables.tracks, eq(tables.lovedTracks.trackId, tables.tracks.id)) + .leftJoin(tables.users, eq(tables.lovedTracks.userId, tables.users.id)) + .where(or(eq(tables.users.did, did), eq(tables.users.handle, did))) + .orderBy(desc(tables.lovedTracks.createdAt)) + .limit(size) + .offset(offset) + .execute(); + return c.json(lovedTracks); }); @@ -131,7 +123,7 @@ app.get("/:did/tracks", async (c) => { data.map((item) => ({ ...item, tags: [], - })), + })) ); }); @@ -161,7 +153,7 @@ app.get("/:did/playlists", async (c) => { results.map((x) => ({ ...x.playlists, trackCount: +x.trackCount, - })), + })) ); }); @@ -174,10 +166,15 @@ app.get("/:did/app.rocksky.scrobble/:rkey", async (c) => { const rkey = c.req.param("rkey"); const uri = `at://${did}/app.rocksky.scrobble/${rkey}`; - const scrobble = await ctx.client.db.scrobbles - .select(["track_id.*", "user_id.*", "xata_createdat", "uri"]) - .filter("uri", equals(uri)) - .getFirst(); + const scrobble = await ctx.db + .select() + .from(tables.scrobbles) + .leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id)) + .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)) + .where(eq(tables.scrobbles.uri, uri)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!scrobble) { c.status(404); @@ -185,35 +182,25 @@ app.get("/:did/app.rocksky.scrobble/:rkey", async (c) => { } const [listeners, scrobbles] = await Promise.all([ - ctx.client.db.user_tracks.select(["track_id.*"]).summarize({ - filter: { - "track_id.xata_id": scrobble.track_id.xata_id, - }, - columns: ["track_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), - ctx.client.db.scrobbles.select(["track_id.*", "xata_createdat"]).summarize({ - filter: { - "track_id.xata_id": scrobble.track_id.xata_id, - }, - columns: ["track_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.userTracks) + .where(eq(tables.userTracks.trackId, scrobble.tracks.id)) + .execute() + .then((rows) => rows[0].count), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.scrobbles) + .where(eq(tables.scrobbles.trackId, scrobble.tracks.id)) + .execute() + .then((rows) => rows[0].count), ]); return c.json({ - ...R.omit(["xata_id"], scrobble), - id: scrobble.xata_id, - listeners: _.get(listeners.summaries, "0.total", 1), - scrobbles: _.get(scrobbles.summaries, "0.total", 1), + ...scrobble, + id: scrobble.scrobbles.id, + listeners: listeners || 1, + scrobbles: scrobbles || 1, tags: [], }); }); @@ -227,12 +214,17 @@ app.get("/:did/app.rocksky.artist/:rkey", async (c) => { const rkey = c.req.param("rkey"); const uri = `at://${did}/app.rocksky.artist/${rkey}`; - const artist = await ctx.client.db.user_artists - .select(["artist_id.*"]) - .filter({ - $any: [{ uri }, { "artist_id.uri": uri }], - }) - .getFirst(); + const artist = await ctx.db + .select() + .from(tables.userArtists) + .leftJoin( + tables.artists, + eq(tables.userArtists.artistId, tables.artists.id) + ) + .where(or(eq(tables.userArtists.uri, uri), eq(tables.artists.uri, uri))) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!artist) { c.status(404); @@ -240,37 +232,25 @@ app.get("/:did/app.rocksky.artist/:rkey", async (c) => { } const [listeners, scrobbles] = await Promise.all([ - ctx.client.db.user_artists.select(["artist_id.*"]).summarize({ - filter: { - "artist_id.xata_id": equals(artist.artist_id.xata_id), - }, - columns: ["artist_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), - ctx.client.db.scrobbles - .select(["artist_id.*", "xata_createdat"]) - .summarize({ - filter: { - "artist_id.xata_id": artist.artist_id.xata_id, - }, - columns: ["artist_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.userArtists) + .where(eq(tables.userArtists.artistId, artist.artists.id)) + .execute() + .then((rows) => rows[0].count), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.scrobbles) + .where(eq(tables.scrobbles.artistId, artist.artists.id)) + .execute() + .then((rows) => rows[0].count), ]); return c.json({ - ...R.omit(["xata_id"], artist.artist_id), - id: artist.artist_id.xata_id, - listeners: _.get(listeners.summaries, "0.total", 1), - scrobbles: _.get(scrobbles.summaries, "0.total", 1), + ...R.omit(["id"], artist.artists), + id: artist.artists.id, + listeners: listeners || 1, + scrobbles: scrobbles || 1, tags: [], }); }); @@ -285,57 +265,51 @@ app.get("/:did/app.rocksky.album/:rkey", async (c) => { const rkey = c.req.param("rkey"); const uri = `at://${did}/app.rocksky.album/${rkey}`; - const album = await ctx.client.db.user_albums - .select(["album_id.*"]) - .filter({ - $any: [{ uri }, { "album_id.uri": uri }], - }) - .getFirst(); + const album = await ctx.db + .select() + .from(tables.userAlbums) + .leftJoin(tables.albums, eq(tables.userAlbums.albumId, tables.albums.id)) + .where(or(eq(tables.userAlbums.uri, uri), eq(tables.albums.uri, uri))) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!album) { c.status(404); return c.text("Album not found"); } - const tracks = await ctx.client.db.album_tracks - .select(["track_id.*"]) - .filter("album_id.xata_id", equals(album.album_id.xata_id)) - .sort("track_id.track_number", "asc") - .getAll(); + const tracks = await ctx.db + .select() + .from(tables.albumTracks) + .leftJoin(tables.tracks, eq(tables.albumTracks.trackId, tables.tracks.id)) + .where(eq(tables.albumTracks.albumId, album.albums.id)) + .orderBy(asc(tables.tracks.trackNumber)) + .execute(); const [listeners, scrobbles] = await Promise.all([ - ctx.client.db.user_albums.select(["album_id.*"]).summarize({ - filter: { - "album_id.xata_id": equals(album.album_id.xata_id), - }, - columns: ["album_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), - ctx.client.db.scrobbles.select(["album_id.*", "xata_createdat"]).summarize({ - filter: { - "album_id.xata_id": album.album_id.xata_id, - }, - columns: ["album_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.userAlbums) + .where(eq(tables.userAlbums.albumId, album.albums.id)) + .execute() + .then((rows) => rows[0].count), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.scrobbles) + .where(eq(tables.scrobbles.albumId, album.albums.id)) + .execute() + .then((rows) => rows[0].count), ]); return c.json({ - ...R.omit(["xata_id"], album.album_id), - id: album.album_id.xata_id, - listeners: _.get(listeners.summaries, "0.total", 1), - scrobbles: _.get(scrobbles.summaries, "0.total", 1), - label: _.get(tracks, "0.track_id.label", ""), - tracks: dedupeTracksKeepLyrics(tracks.map((track) => track.track_id)).sort( - (a, b) => a.track_number - b.track_number, + ...R.omit(["id"], album.albums), + id: album.albums.id, + listeners: listeners || 1, + scrobbles: scrobbles || 1, + label: tracks[0]?.tracks.label || "", + tracks: dedupeTracksKeepLyrics(tracks.map((track) => track.tracks)).sort( + (a, b) => a.track_number - b.track_number ), tags: [], }); @@ -351,13 +325,23 @@ app.get("/:did/app.rocksky.song/:rkey", async (c) => { const uri = `at://${did}/app.rocksky.song/${rkey}`; const [_track, user_track] = await Promise.all([ - ctx.client.db.tracks.filter("uri", equals(uri)).getFirst(), - ctx.client.db.user_tracks - .select(["track_id.*"]) - .filter("uri", equals(uri)) - .getFirst(), + ctx.db + .select() + .from(tables.tracks) + .where(eq(tables.tracks.uri, uri)) + .limit(1) + .execute() + .then((rows) => rows[0]), + ctx.db + .select() + .from(tables.userTracks) + .leftJoin(tables.tracks, eq(tables.userTracks.trackId, tables.tracks.id)) + .where(eq(tables.userTracks.uri, uri)) + .limit(1) + .execute() + .then((rows) => rows[0]), ]); - const track = _track || user_track.track_id; + const track = _track || user_track?.tracks; if (!track) { c.status(404); @@ -365,36 +349,26 @@ app.get("/:did/app.rocksky.song/:rkey", async (c) => { } const [listeners, scrobbles] = await Promise.all([ - ctx.client.db.user_tracks.select(["track_id.*"]).summarize({ - filter: { - "track_id.xata_id": equals(track.xata_id), - }, - columns: ["track_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), - ctx.client.db.scrobbles.select(["track_id.*", "xata_createdat"]).summarize({ - filter: { - "track_id.xata_id": track.xata_id, - }, - columns: ["track_id.*"], - summaries: { - total: { - count: "*", - }, - }, - }), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.userTracks) + .where(eq(tables.userTracks.trackId, track.id)) + .execute() + .then((rows) => rows[0].count), + ctx.db + .select({ count: sql`COUNT(*)` }) + .from(tables.scrobbles) + .where(eq(tables.scrobbles.trackId, track.id)) + .execute() + .then((rows) => rows[0].count), ]); return c.json({ - ...R.omit(["xata_id"], track), - id: track.xata_id, + ...R.omit(["id"], track), + id: track.id, tags: [], - listeners: _.get(listeners.summaries, "0.total", 1), - scrobbles: _.get(scrobbles.summaries, "0.total", 1), + listeners: listeners || 1, + scrobbles: scrobbles || 1, }); }); @@ -409,24 +383,22 @@ app.get("/:did/app.rocksky.artist/:rkey/tracks", async (c) => { const size = +c.req.query("size") || 10; const offset = +c.req.query("offset") || 0; - const tracks = await ctx.client.db.artist_tracks - .select(["track_id.*", "xata_version"]) - .filter({ - "artist_id.uri": equals(uri), - }) - .sort("xata_version", "desc") - .getPaginated({ - pagination: { - size, - offset, - }, - }); + const tracks = await ctx.db + .select() + .from(tables.artistTracks) + .leftJoin(tables.tracks, eq(tables.artistTracks.trackId, tables.tracks.id)) + .where(eq(tables.artistTracks.artistId, uri)) // Assuming artist_id is the URI or ID; adjust if needed + .orderBy(desc(tables.artistTracks.xataVersion)) + .limit(size) + .offset(offset) + .execute(); + return c.json( - tracks.records.map((item) => ({ - ...R.omit(["xata_id"], item.track_id), - id: item.track_id.xata_id, - xata_version: item.xata_version, - })), + tracks.map((item) => ({ + ...R.omit(["id"], item.tracks), + id: item.tracks.id, + xata_version: item.artist_tracks.xataVersion, + })) ); }); @@ -441,27 +413,25 @@ app.get("/:did/app.rocksky.artist/:rkey/albums", async (c) => { const size = +c.req.query("size") || 10; const offset = +c.req.query("offset") || 0; - const albums = await ctx.client.db.artist_albums - .select(["album_id.*", "xata_version"]) - .filter({ - "artist_id.uri": equals(uri), - }) - .sort("xata_version", "desc") - .getPaginated({ - pagination: { - size, - offset, - }, - }); + const albums = await ctx.db + .select() + .from(tables.artistAlbums) + .leftJoin(tables.albums, eq(tables.artistAlbums.albumId, tables.albums.id)) + .where(eq(tables.artistAlbums.artistId, uri)) // Assuming artist_id is the URI or ID; adjust if needed + .orderBy(desc(tables.artistAlbums.xataVersion)) + .limit(size) + .offset(offset) + .execute(); + return c.json( R.uniqBy( (item) => item.id, - albums.records.map((item) => ({ - ...R.omit(["xata_id"], item.album_id), - id: item.album_id.xata_id, - xata_version: item.xata_version, - })), - ), + albums.map((item) => ({ + ...R.omit(["id"], item.albums), + id: item.albums.id, + xata_version: item.artist_albums.xataVersion, + })) + ) ); }); @@ -491,11 +461,11 @@ app.get("/:did/app.rocksky.playlist/:rkey", async (c) => { .from(tables.playlistTracks) .leftJoin( tables.playlists, - eq(tables.playlistTracks.playlistId, tables.playlists.id), + eq(tables.playlistTracks.playlistId, tables.playlists.id) ) .leftJoin( tables.tracks, - eq(tables.playlistTracks.trackId, tables.tracks.id), + eq(tables.playlistTracks.trackId, tables.tracks.id) ) .where(eq(tables.playlists.uri, uri)) .groupBy( @@ -539,7 +509,7 @@ app.get("/:did/app.rocksky.playlist/:rkey", async (c) => { tables.playlists.picture, tables.playlists.spotifyLink, tables.playlists.tidalLink, - tables.playlists.appleMusicLink, + tables.playlists.appleMusicLink ) .orderBy(asc(tables.playlistTracks.createdAt)) .execute(); @@ -589,21 +559,26 @@ app.get("/:did", async (c) => { .where(eq(tables.users.did, did)) .execute(); - const user = await ctx.client.db.users - .select(["*"]) - .filter("did", equals(did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, did)) + .limit(1) + .execute() + .then((rows) => rows[0]); ctx.nc.publish("rocksky.user", Buffer.from(JSON.stringify(user))); } } } - const user = await ctx.client.db.users - .filter({ - $any: [{ did }, { handle: did }], - }) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(or(eq(tables.users.did, did), eq(tables.users.handle, did))) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(404); @@ -630,9 +605,13 @@ app.post("/:did/app.rocksky.artist/:rkey/shouts", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -653,7 +632,7 @@ app.post("/:did/app.rocksky.artist/:rkey/shouts", async (c) => { parsed.data, `at://${did}/app.rocksky.artist/${rkey}`, user, - agent, + agent ); return c.json({}); }); @@ -675,9 +654,13 @@ app.post("/:did/app.rocksky.album/:rkey/shouts", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -698,7 +681,7 @@ app.post("/:did/app.rocksky.album/:rkey/shouts", async (c) => { parsed.data, `at://${did}/app.rocksky.album/${rkey}`, user, - agent, + agent ); return c.json({}); }); @@ -720,9 +703,13 @@ app.post("/:did/app.rocksky.song/:rkey/shouts", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -743,7 +730,7 @@ app.post("/:did/app.rocksky.song/:rkey/shouts", async (c) => { parsed.data, `at://${did}/app.rocksky.song/${rkey}`, user, - agent, + agent ); return c.json({}); @@ -766,9 +753,13 @@ app.post("/:did/app.rocksky.scrobble/:rkey/shouts", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -789,7 +780,7 @@ app.post("/:did/app.rocksky.scrobble/:rkey/shouts", async (c) => { parsed.data, `at://${did}/app.rocksky.scrobble/${rkey}`, user, - agent, + agent ); return c.json({}); @@ -809,9 +800,13 @@ app.post("/:did/shouts", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -825,11 +820,13 @@ app.post("/:did/shouts", async (c) => { return c.text("Invalid shout data: " + parsed.error.message); } - const _user = await ctx.client.db.users - .filter({ - $any: [{ did }, { handle: did }], - }) - .getFirst(); + const _user = await ctx.db + .select() + .from(tables.users) + .where(or(eq(tables.users.did, did), eq(tables.users.handle, did))) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!_user) { c.status(404); @@ -858,9 +855,13 @@ app.post("/:did/app.rocksky.shout/:rkey/likes", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -890,9 +891,13 @@ app.delete("/:did/app.rocksky.shout/:rkey/likes", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -923,9 +928,13 @@ app.post("/:did/app.rocksky.song/:rkey/likes", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -934,21 +943,25 @@ app.post("/:did/app.rocksky.song/:rkey/likes", async (c) => { const did = c.req.param("did"); const rkey = c.req.param("rkey"); - const result = await ctx.client.db.tracks - .filter("uri", equals(`at://${did}/app.rocksky.song/${rkey}`)) - .getFirst(); + const result = await ctx.db + .select() + .from(tables.tracks) + .where(eq(tables.tracks.uri, `at://${did}/app.rocksky.song/${rkey}`)) + .limit(1) + .execute() + .then((rows) => rows[0]); const track: Track = { title: result.title, artist: result.artist, album: result.album, - albumArt: result.album_art, - albumArtist: result.album_artist, - trackNumber: result.track_number, + albumArt: result.albumArt, + albumArtist: result.albumArtist, + trackNumber: result.trackNumber, duration: result.duration, composer: result.composer, lyrics: result.lyrics, - discNumber: result.disc_number, + discNumber: result.discNumber, }; await likeTrack(ctx, track, user, agent); @@ -972,9 +985,13 @@ app.delete("/:did/app.rocksky.song/:rkey/likes", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -983,9 +1000,13 @@ app.delete("/:did/app.rocksky.song/:rkey/likes", async (c) => { const did = c.req.param("did"); const rkey = c.req.param("rkey"); - const track = await ctx.client.db.tracks - .filter("uri", equals(`at://${did}/app.rocksky.song/${rkey}`)) - .getFirst(); + const track = await ctx.db + .select() + .from(tables.tracks) + .where(eq(tables.tracks.uri, `at://${did}/app.rocksky.song/${rkey}`)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!track) { c.status(404); @@ -1014,9 +1035,13 @@ app.post("/:did/app.rocksky.shout/:rkey/replies", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); @@ -1038,7 +1063,7 @@ app.post("/:did/app.rocksky.shout/:rkey/replies", async (c) => { parsed.data, `at://${did}/app.rocksky.shout/${rkey}`, user, - agent, + agent ); return c.json({}); }); @@ -1053,15 +1078,19 @@ app.get("/:did/app.rocksky.artist/:rkey/shouts", async (c) => { const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); - let user; + let user: SelectUser | undefined; if (bearer && bearer !== "null") { const payload = jwt.verify(bearer, env.JWT_SECRET, { ignoreExpiration: true, }); - user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); } const shouts = await ctx.db @@ -1078,8 +1107,8 @@ app.get("/:did/app.rocksky.artist/:rkey/shouts", async (c) => { EXISTS ( SELECT 1 FROM ${tables.shoutLikes} - WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.xata_id - AND ${tables.shoutLikes}.user_id = ${user.xata_id} + WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.id + AND ${tables.shoutLikes}.user_id = ${user.id} )`.as("liked"), } : { @@ -1103,7 +1132,7 @@ app.get("/:did/app.rocksky.artist/:rkey/shouts", async (c) => { .leftJoin(tables.artists, eq(tables.shouts.artistId, tables.artists.id)) .leftJoin( tables.shoutLikes, - eq(tables.shouts.id, tables.shoutLikes.shoutId), + eq(tables.shouts.id, tables.shoutLikes.shoutId) ) .where(eq(tables.artists.uri, `at://${did}/app.rocksky.artist/${rkey}`)) .groupBy( @@ -1116,7 +1145,7 @@ app.get("/:did/app.rocksky.artist/:rkey/shouts", async (c) => { tables.users.did, tables.users.handle, tables.users.displayName, - tables.users.avatar, + tables.users.avatar ) .orderBy(desc(tables.shouts.createdAt)) .execute(); @@ -1134,15 +1163,19 @@ app.get("/:did/app.rocksky.album/:rkey/shouts", async (c) => { const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); - let user; + let user: SelectUser | undefined; if (bearer && bearer !== "null") { const payload = jwt.verify(bearer, env.JWT_SECRET, { ignoreExpiration: true, }); - user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); } const shouts = await ctx.db @@ -1159,8 +1192,8 @@ app.get("/:did/app.rocksky.album/:rkey/shouts", async (c) => { EXISTS ( SELECT 1 FROM ${tables.shoutLikes} - WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.xata_id - AND ${tables.shoutLikes}.user_id = ${user.xata_id} + WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.id + AND ${tables.shoutLikes}.user_id = ${user.id} )`.as("liked"), } : { @@ -1184,7 +1217,7 @@ app.get("/:did/app.rocksky.album/:rkey/shouts", async (c) => { .leftJoin(tables.albums, eq(tables.shouts.albumId, tables.albums.id)) .leftJoin( tables.shoutLikes, - eq(tables.shouts.id, tables.shoutLikes.shoutId), + eq(tables.shouts.id, tables.shoutLikes.shoutId) ) .where(eq(tables.albums.uri, `at://${did}/app.rocksky.album/${rkey}`)) .groupBy( @@ -1197,7 +1230,7 @@ app.get("/:did/app.rocksky.album/:rkey/shouts", async (c) => { tables.users.did, tables.users.handle, tables.users.displayName, - tables.users.avatar, + tables.users.avatar ) .orderBy(desc(tables.shouts.createdAt)) .execute(); @@ -1215,15 +1248,19 @@ app.get("/:did/app.rocksky.song/:rkey/shouts", async (c) => { const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); - let user; + let user: SelectUser | undefined; if (bearer && bearer !== "null") { const payload = jwt.verify(bearer, env.JWT_SECRET, { ignoreExpiration: true, }); - user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); } const shouts = await ctx.db @@ -1240,8 +1277,8 @@ app.get("/:did/app.rocksky.song/:rkey/shouts", async (c) => { EXISTS ( SELECT 1 FROM ${tables.shoutLikes} - WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.xata_id - AND ${tables.shoutLikes}.user_id = ${user.xata_id} + WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.id + AND ${tables.shoutLikes}.user_id = ${user.id} )`.as("liked"), } : { @@ -1265,7 +1302,7 @@ app.get("/:did/app.rocksky.song/:rkey/shouts", async (c) => { .leftJoin(tables.tracks, eq(tables.shouts.trackId, tables.tracks.id)) .leftJoin( tables.shoutLikes, - eq(tables.shouts.id, tables.shoutLikes.shoutId), + eq(tables.shouts.id, tables.shoutLikes.shoutId) ) .where(eq(tables.tracks.uri, `at://${did}/app.rocksky.song/${rkey}`)) .groupBy( @@ -1278,7 +1315,7 @@ app.get("/:did/app.rocksky.song/:rkey/shouts", async (c) => { tables.users.did, tables.users.handle, tables.users.displayName, - tables.users.avatar, + tables.users.avatar ) .orderBy(desc(tables.shouts.createdAt)) .execute(); @@ -1296,15 +1333,19 @@ app.get("/:did/app.rocksky.scrobble/:rkey/shouts", async (c) => { const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); - let user; + let user: SelectUser | undefined; if (bearer && bearer !== "null") { const payload = jwt.verify(bearer, env.JWT_SECRET, { ignoreExpiration: true, }); - user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); } const shouts = await ctx.db @@ -1321,8 +1362,8 @@ app.get("/:did/app.rocksky.scrobble/:rkey/shouts", async (c) => { EXISTS ( SELECT 1 FROM ${tables.shoutLikes} - WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.xata_id - AND ${tables.shoutLikes}.user_id = ${user.xata_id} + WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.id + AND ${tables.shoutLikes}.user_id = ${user.id} )`.as("liked"), } : { @@ -1345,11 +1386,11 @@ app.get("/:did/app.rocksky.scrobble/:rkey/shouts", async (c) => { .leftJoin(tables.users, eq(tables.shouts.authorId, tables.users.id)) .leftJoin( tables.scrobbles, - eq(tables.shouts.scrobbleId, tables.scrobbles.id), + eq(tables.shouts.scrobbleId, tables.scrobbles.id) ) .leftJoin( tables.shoutLikes, - eq(tables.shouts.id, tables.shoutLikes.shoutId), + eq(tables.shouts.id, tables.shoutLikes.shoutId) ) .where(eq(tables.scrobbles.uri, `at://${did}/app.rocksky.scrobble/${rkey}`)) .groupBy( @@ -1362,7 +1403,7 @@ app.get("/:did/app.rocksky.scrobble/:rkey/shouts", async (c) => { tables.users.did, tables.users.handle, tables.users.displayName, - tables.users.avatar, + tables.users.avatar ) .orderBy(desc(tables.shouts.createdAt)) .execute(); @@ -1376,15 +1417,19 @@ app.get("/:did/shouts", async (c) => { const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); - let user; + let user: SelectUser | undefined; if (bearer && bearer !== "null") { const payload = jwt.verify(bearer, env.JWT_SECRET, { ignoreExpiration: true, }); - user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); } const shouts = await ctx.db @@ -1405,15 +1450,15 @@ app.get("/:did/shouts", async (c) => { EXISTS ( SELECT 1 FROM ${tables.shoutLikes} - WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.xata_id - AND ${tables.shoutLikes}.user_id = ${user.xata_id} + WHERE ${tables.shoutLikes}.shout_id = ${tables.shouts}.id + AND ${tables.shoutLikes}.user_id = ${user.id} )`.as("liked"), reported: sql` EXISTS ( SELECT 1 FROM ${tables.shoutReports} - WHERE ${tables.shoutReports}.shout_id = ${tables.shouts}.xata_id - AND ${tables.shoutReports}.user_id = ${user.xata_id} + WHERE ${tables.shoutReports}.shout_id = ${tables.shouts}.id + AND ${tables.shoutReports}.user_id = ${user.id} )`.as("reported"), } : { @@ -1437,12 +1482,12 @@ app.get("/:did/shouts", async (c) => { .leftJoin(tables.shouts, eq(tables.profileShouts.shoutId, tables.shouts.id)) .leftJoin( aliasedTable(tables.users, "authors"), - eq(tables.shouts.authorId, aliasedTable(tables.users, "authors").id), + eq(tables.shouts.authorId, aliasedTable(tables.users, "authors").id) ) .leftJoin(tables.users, eq(tables.profileShouts.userId, tables.users.id)) .leftJoin( tables.shoutLikes, - eq(tables.shouts.id, tables.shoutLikes.shoutId), + eq(tables.shouts.id, tables.shoutLikes.shoutId) ) .groupBy( tables.profileShouts.id, @@ -1461,7 +1506,7 @@ app.get("/:did/shouts", async (c) => { aliasedTable(tables.users, "authors").did, aliasedTable(tables.users, "authors").handle, aliasedTable(tables.users, "authors").displayName, - aliasedTable(tables.users, "authors").avatar, + aliasedTable(tables.users, "authors").avatar ) .orderBy(desc(tables.profileShouts.createdAt)) .execute(); @@ -1476,10 +1521,13 @@ app.get("/:did/app.rocksky.shout/:rkey/likes", async (c) => { }); const did = c.req.param("did"); const rkey = c.req.param("rkey"); - const likes = await ctx.client.db.shout_likes - .select(["user_id.*", "xata_createdat"]) - .filter("shout_id.uri", `at://${did}/app.rocksky.shout/${rkey}`) - .getAll(); + const likes = await ctx.db + .select() + .from(tables.shoutLikes) + .leftJoin(tables.users, eq(tables.shoutLikes.userId, tables.users.id)) + .leftJoin(tables.shouts, eq(tables.shoutLikes.shoutId, tables.shouts.id)) + .where(eq(tables.shouts.uri, `at://${did}/app.rocksky.shout/${rkey}`)) + .execute(); return c.json(likes); }); @@ -1490,11 +1538,13 @@ app.get("/:did/app.rocksky.shout/:rkey/replies", async (c) => { }); const did = c.req.param("did"); const rkey = c.req.param("rkey"); - const shouts = await ctx.client.db.shouts - .select(["author_id.*", "xata_createdat"]) - .filter("parent_id.uri", `at://${did}/app.rocksky.shout/${rkey}`) - .sort("xata_createdat", "asc") - .getAll(); + const shouts = await ctx.db + .select() + .from(tables.shouts) + .leftJoin(tables.users, eq(tables.shouts.authorId, tables.users.id)) + .where(eq(tables.shouts.parentId, `at://${did}/app.rocksky.shout/${rkey}`)) + .orderBy(asc(tables.shouts.createdAt)) + .execute(); return c.json(shouts); }); @@ -1533,13 +1583,21 @@ app.post("/:did/app.rocksky.shout/:rkey/report", async (c) => { const payload = jwt.verify(bearer, env.JWT_SECRET, { ignoreExpiration: true, }); - const shout = await ctx.client.db.shouts - .filter("uri", `at://${did}/app.rocksky.shout/${rkey}`) - .getFirst(); + const shout = await ctx.db + .select() + .from(tables.shouts) + .where(eq(tables.shouts.uri, `at://${did}/app.rocksky.shout/${rkey}`)) + .limit(1) + .execute() + .then((rows) => rows[0]); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!shout) { c.status(404); @@ -1551,21 +1609,32 @@ app.post("/:did/app.rocksky.shout/:rkey/report", async (c) => { return c.text("Unauthorized"); } - const existingReport = await ctx.client.db.shout_reports - .filter({ - user_id: user.xata_id, - shout_id: shout.xata_id, - }) - .getFirst(); + const existingReport = await ctx.db + .select() + .from(tables.shoutReports) + .where( + and( + eq(tables.shoutReports.userId, user.id), + eq(tables.shoutReports.shoutId, shout.id) + ) + ) + .limit(1) + .execute() + .then((rows) => rows[0]); if (existingReport) { return c.json(existingReport); } - const report = await ctx.client.db.shout_reports.create({ - user_id: user.xata_id, - shout_id: shout.xata_id, - }); + const report = await ctx.db + .insert(tables.shoutReports) + .values({ + userId: user.id, + shoutId: shout.id, + }) + .returning() + .execute() + .then((rows) => rows[0]); return c.json(report); }); @@ -1588,13 +1657,21 @@ app.delete("/:did/app.rocksky.shout/:rkey/report", async (c) => { const payload = jwt.verify(bearer, env.JWT_SECRET, { ignoreExpiration: true, }); - const shout = await ctx.client.db.shouts - .filter("uri", `at://${did}/app.rocksky.shout/${rkey}`) - .getFirst(); + const shout = await ctx.db + .select() + .from(tables.shouts) + .where(eq(tables.shouts.uri, `at://${did}/app.rocksky.shout/${rkey}`)) + .limit(1) + .execute() + .then((rows) => rows[0]); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!shout) { c.status(404); @@ -1606,25 +1683,33 @@ app.delete("/:did/app.rocksky.shout/:rkey/report", async (c) => { return c.text("Unauthorized"); } - const report = await ctx.client.db.shout_reports - .select(["user_id.*", "shout_id.*"]) - .filter({ - user_id: user.xata_id, - shout_id: shout.xata_id, - }) - .getFirst(); + const report = await ctx.db + .select() + .from(tables.shoutReports) + .where( + and( + eq(tables.shoutReports.userId, user.id), + eq(tables.shoutReports.shoutId, shout.id) + ) + ) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!report) { c.status(404); return c.text("Report not found"); } - if (report.user_id.xata_id !== user.xata_id) { + if (report.userId !== user.id) { c.status(403); return c.text("Forbidden"); } - await ctx.client.db.shout_reports.delete(report.xata_id); + await ctx.db + .delete(tables.shoutReports) + .where(eq(tables.shoutReports.id, report.id)) + .execute(); return c.json(report); }); @@ -1649,33 +1734,33 @@ app.delete("/:did/app.rocksky.shout/:rkey", async (c) => { }); const agent = await createAgent(ctx.oauthClient, payload.did); - const user = await ctx.client.db.users - .filter("did", equals(payload.did)) - .getFirst(); + const user = await ctx.db + .select() + .from(tables.users) + .where(eq(tables.users.did, payload.did)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!user) { c.status(401); return c.text("Unauthorized"); } - const shout = await ctx.client.db.shouts - .select([ - "author_id.*", - "uri", - "content", - "xata_id", - "xata_createdat", - "parent_id", - ]) - .filter("uri", `at://${did}/app.rocksky.shout/${rkey}`) - .getFirst(); + const shout = await ctx.db + .select() + .from(tables.shouts) + .where(eq(tables.shouts.uri, `at://${did}/app.rocksky.shout/${rkey}`)) + .limit(1) + .execute() + .then((rows) => rows[0]); if (!shout) { c.status(404); return c.text("Shout not found"); } - if (shout.author_id.xata_id !== user.xata_id) { + if (shout.authorId !== user.id) { c.status(403); return c.text("Forbidden"); } @@ -1687,7 +1772,7 @@ app.delete("/:did/app.rocksky.shout/:rkey", async (c) => { }, }) .from(tables.shouts) - .where(eq(tables.shouts.parentId, shout.xata_id)) + .where(eq(tables.shouts.parentId, shout.id)) .execute(); const replyIds = replies.map(({ replies: r }) => r.id); @@ -1705,7 +1790,7 @@ app.delete("/:did/app.rocksky.shout/:rkey", async (c) => { await ctx.db .delete(tables.profileShouts) - .where(eq(tables.profileShouts.shoutId, shout.xata_id)) + .where(eq(tables.profileShouts.shoutId, shout.id)) .execute(); await ctx.db @@ -1715,12 +1800,12 @@ app.delete("/:did/app.rocksky.shout/:rkey", async (c) => { await ctx.db .delete(tables.shoutLikes) - .where(eq(tables.shoutLikes.shoutId, shout.xata_id)) + .where(eq(tables.shoutLikes.shoutId, shout.id)) .execute(); await ctx.db .delete(tables.shoutReports) - .where(eq(tables.shoutReports.shoutId, shout.xata_id)) + .where(eq(tables.shoutReports.shoutId, shout.id)) .execute(); await ctx.db @@ -1730,7 +1815,7 @@ app.delete("/:did/app.rocksky.shout/:rkey", async (c) => { await ctx.db .delete(tables.shouts) - .where(eq(tables.shouts.id, shout.xata_id)) + .where(eq(tables.shouts.id, shout.id)) .execute(); await agent.com.atproto.repo.deleteRecord({ diff --git a/apps/api/src/webscrobbler/app.ts b/apps/api/src/webscrobbler/app.ts index a805cb78..cf07a221 100644 --- a/apps/api/src/webscrobbler/app.ts +++ b/apps/api/src/webscrobbler/app.ts @@ -1,5 +1,5 @@ import { ctx } from "context"; -import { eq } from "drizzle-orm"; +import { eq, or } from "drizzle-orm"; import { Hono } from "hono"; import jwt from "jsonwebtoken"; import { env } from "lib/env"; @@ -21,11 +21,13 @@ app.get("/", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users - .filter({ - $any: [{ did }, { handle: did }], - }) - .getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(or(eq(users.did, did), eq(users.handle, did))) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -35,15 +37,17 @@ app.get("/", async (c) => { .select() .from(webscrobblers) .leftJoin(users, eq(webscrobblers.userId, users.id)) - .where(eq(users.did, did)) - .execute(); + .where(eq(users.did, did)); if (records.length === 0) { - const record = await ctx.client.db.webscrobblers.create({ - uuid: uuid(), - user_id: user.xata_id, - name: "webscrobbler", - }); + const [record] = await ctx.db + .insert(webscrobblers) + .values({ + uuid: uuid(), + userId: user.id, + name: "webscrobbler", + }) + .returning(); return c.json(record); } @@ -63,11 +67,13 @@ app.put("/:id", async (c) => { ignoreExpiration: true, }); - const user = await ctx.client.db.users - .filter({ - $any: [{ did }, { handle: did }], - }) - .getFirst(); + const user = await ctx.db + .select() + .from(users) + .where(or(eq(users.did, did), eq(users.handle, did))) + .limit(1) + .then((rows) => rows[0]); + if (!user) { c.status(401); return c.text("Unauthorized"); @@ -80,20 +86,35 @@ app.put("/:id", async (c) => { return c.text("Invalid id"); } - const existing = await ctx.client.db.webscrobblers - .filter({ user_id: user.xata_id }) - .getFirst(); - - const record = await ctx.client.db.webscrobblers.createOrReplace( - existing?.xata_id, - { - uuid: id, - user_id: user.xata_id, - name: "webscrobbler", - }, - ); - - return c.json(record); + const existing = await ctx.db + .select() + .from(webscrobblers) + .where(eq(webscrobblers.userId, user.id)) + .limit(1) + .then((rows) => rows[0]); + + if (existing) { + const [record] = await ctx.db + .update(webscrobblers) + .set({ + uuid: id, + userId: user.id, + name: "webscrobbler", + }) + .where(eq(webscrobblers.id, existing.id)) + .returning(); + return c.json(record); + } else { + const [record] = await ctx.db + .insert(webscrobblers) + .values({ + uuid: id, + userId: user.id, + name: "webscrobbler", + }) + .returning(); + return c.json(record); + } }); export default app; diff --git a/apps/api/src/xata.ts b/apps/api/src/xata.ts deleted file mode 100644 index 82d97b74..00000000 --- a/apps/api/src/xata.ts +++ /dev/null @@ -1,5265 +0,0 @@ -// Generated by Xata Codegen 0.30.1. Please do not edit. -import { buildClient } from "@xata.io/client"; -import type { - BaseClientOptions, - SchemaInference, - XataRecord, -} from "@xata.io/client"; - -const tables = [ - { - name: "album_tags", - checkConstraints: { - album_tags_xata_id_length_xata_id: { - name: "album_tags_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - album_id_link: { - name: "album_id_link", - columns: ["album_id"], - referencedTable: "albums", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - tag_id_link: { - name: "tag_id_link", - columns: ["tag_id"], - referencedTable: "tags", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_album_tags_xata_id_key: { - name: "_pgroll_new_album_tags_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "album_id", - type: "link", - link: { table: "albums" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"albums"}', - }, - { - name: "tag_id", - type: "link", - link: { table: "tags" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tags"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "album_tracks", - checkConstraints: { - album_tracks_xata_id_length_xata_id: { - name: "album_tracks_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - album_id_link: { - name: "album_id_link", - columns: ["album_id"], - referencedTable: "albums", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_album_tracks_xata_id_key: { - name: "_pgroll_new_album_tracks_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "album_id", - type: "link", - link: { table: "albums" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"albums"}', - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "albums", - checkConstraints: { - albums_xata_id_length_xata_id: { - name: "albums_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_albums_xata_id_key: { - name: "_pgroll_new_albums_xata_id_key", - columns: ["xata_id"], - }, - albums__pgroll_new_sha256_key: { - name: "albums__pgroll_new_sha256_key", - columns: ["sha256"], - }, - albums__pgroll_new_uri_key: { - name: "albums__pgroll_new_uri_key", - columns: ["uri"], - }, - albums_apple_music_link_unique: { - name: "albums_apple_music_link_unique", - columns: ["apple_music_link"], - }, - albums_spotify_link_unique: { - name: "albums_spotify_link_unique", - columns: ["spotify_link"], - }, - }, - columns: [ - { - name: "album_art", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "apple_music_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "artist", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "artist_uri", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "lastfm_link", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "release_date", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "sha256", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "spotify_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "tidal_link", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "title", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - { - name: "year", - type: "int", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "youtube_link", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - ], - }, - { - name: "api_keys", - checkConstraints: { - api_keys_xata_id_length_xata_id: { - name: "api_keys_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_api_keys_xata_id_key: { - name: "_pgroll_new_api_keys_xata_id_key", - columns: ["xata_id"], - }, - api_keys__pgroll_new_api_key_key: { - name: "api_keys__pgroll_new_api_key_key", - columns: ["api_key"], - }, - api_keys__pgroll_new_shared_secret_key: { - name: "api_keys__pgroll_new_shared_secret_key", - columns: ["shared_secret"], - }, - }, - columns: [ - { - name: "api_key", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "description", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "enabled", - type: "bool", - notNull: true, - unique: false, - defaultValue: "true", - comment: "", - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "shared_secret", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "artist_albums", - checkConstraints: { - artist_albums_xata_id_length_xata_id: { - name: "artist_albums_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - album_id_link: { - name: "album_id_link", - columns: ["album_id"], - referencedTable: "albums", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - artist_id_link: { - name: "artist_id_link", - columns: ["artist_id"], - referencedTable: "artists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_artist_albums_xata_id_key: { - name: "_pgroll_new_artist_albums_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "album_id", - type: "link", - link: { table: "albums" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"albums"}', - }, - { - name: "artist_id", - type: "link", - link: { table: "artists" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"artists"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "artist_tags", - checkConstraints: { - artist_tags_xata_id_length_xata_id: { - name: "artist_tags_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - artist_id_link: { - name: "artist_id_link", - columns: ["artist_id"], - referencedTable: "artists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - tag_id_link: { - name: "tag_id_link", - columns: ["tag_id"], - referencedTable: "tags", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_artist_tags_xata_id_key: { - name: "_pgroll_new_artist_tags_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "artist_id", - type: "link", - link: { table: "artists" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"artists"}', - }, - { - name: "tag_id", - type: "link", - link: { table: "tags" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tags"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "artist_tracks", - checkConstraints: { - artist_tracks_xata_id_length_xata_id: { - name: "artist_tracks_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - artist_id_link: { - name: "artist_id_link", - columns: ["artist_id"], - referencedTable: "artists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_artist_tracks_xata_id_key: { - name: "_pgroll_new_artist_tracks_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "artist_id", - type: "link", - link: { table: "artists" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"artists"}', - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "artists", - checkConstraints: { - artists_xata_id_length_xata_id: { - name: "artists_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_artists_xata_id_key: { - name: "_pgroll_new_artists_xata_id_key", - columns: ["xata_id"], - }, - artists__pgroll_new_sha256_key: { - name: "artists__pgroll_new_sha256_key", - columns: ["sha256"], - }, - artists__pgroll_new_uri_key: { - name: "artists__pgroll_new_uri_key", - columns: ["uri"], - }, - artists_apple_music_link_unique: { - name: "artists_apple_music_link_unique", - columns: ["apple_music_link"], - }, - artists_spotify_link_unique: { - name: "artists_spotify_link_unique", - columns: ["spotify_link"], - }, - artists_tidal_link_unique: { - name: "artists_tidal_link_unique", - columns: ["tidal_link"], - }, - artists_youtube_link_unique: { - name: "artists_youtube_link_unique", - columns: ["youtube_link"], - }, - }, - columns: [ - { - name: "apple_music_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "biography", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "born", - type: "datetime", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "born_in", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "died", - type: "datetime", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "genres", - type: "multiple", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "lastfm_link", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "picture", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "sha256", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "spotify_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "tidal_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - { - name: "youtube_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - ], - }, - { - name: "builtin_storage_paths", - checkConstraints: { - builtin_storage_paths_xata_id_length_xata_id: { - name: "builtin_storage_paths_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_builtin_storage_paths_xata_id_key: { - name: "_pgroll_new_builtin_storage_paths_xata_id_key", - columns: ["xata_id"], - }, - builtin_storage_paths__pgroll_new_path_key: { - name: "builtin_storage_paths__pgroll_new_path_key", - columns: ["path"], - }, - }, - columns: [ - { - name: "path", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "dropbox", - checkConstraints: { - dropbox_xata_id_length_xata_id: { - name: "dropbox_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - dropbox_token_id_link: { - name: "dropbox_token_id_link", - columns: ["dropbox_token_id"], - referencedTable: "dropbox_tokens", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_dropbox_xata_id_key: { - name: "_pgroll_new_dropbox_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "dropbox_token_id", - type: "link", - link: { table: "dropbox_tokens" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"dropbox_tokens"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "dropbox_accounts", - checkConstraints: { - dropbox_accounts_xata_id_length_xata_id: { - name: "dropbox_accounts_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_dropbox_accounts_xata_id_key: { - name: "_pgroll_new_dropbox_accounts_xata_id_key", - columns: ["xata_id"], - }, - dropbox_accounts__pgroll_new_email_key: { - name: "dropbox_accounts__pgroll_new_email_key", - columns: ["email"], - }, - }, - columns: [ - { - name: "email", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "is_beta_user", - type: "bool", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "dropbox_directories", - checkConstraints: { - dropbox_directories_xata_id_length_xata_id: { - name: "dropbox_directories_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - dropbox_id_link: { - name: "dropbox_id_link", - columns: ["dropbox_id"], - referencedTable: "dropbox", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - parent_id_link: { - name: "parent_id_link", - columns: ["parent_id"], - referencedTable: "dropbox_directories", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_dropbox_directories_xata_id_key: { - name: "_pgroll_new_dropbox_directories_xata_id_key", - columns: ["xata_id"], - }, - dropbox_directories__pgroll_new_file_id_key: { - name: "dropbox_directories__pgroll_new_file_id_key", - columns: ["file_id"], - }, - }, - columns: [ - { - name: "dropbox_id", - type: "link", - link: { table: "dropbox" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"dropbox"}', - }, - { - name: "file_id", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "parent_id", - type: "link", - link: { table: "dropbox_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"dropbox_directories"}', - }, - { - name: "path", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "dropbox_paths", - checkConstraints: { - dropbox_paths_xata_id_length_xata_id: { - name: "dropbox_paths_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - directory_id_link: { - name: "directory_id_link", - columns: ["directory_id"], - referencedTable: "dropbox_directories", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - dropbox_id_link: { - name: "dropbox_id_link", - columns: ["dropbox_id"], - referencedTable: "dropbox", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_dropbox_paths_xata_id_key: { - name: "_pgroll_new_dropbox_paths_xata_id_key", - columns: ["xata_id"], - }, - dropbox_paths__pgroll_new_file_id_key: { - name: "dropbox_paths__pgroll_new_file_id_key", - columns: ["file_id"], - }, - }, - columns: [ - { - name: "directory_id", - type: "link", - link: { table: "dropbox_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"dropbox_directories"}', - }, - { - name: "dropbox_id", - type: "link", - link: { table: "dropbox" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"dropbox"}', - }, - { - name: "file_id", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "path", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "dropbox_tokens", - checkConstraints: { - dropbox_tokens_xata_id_length_xata_id: { - name: "dropbox_tokens_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_dropbox_tokens_xata_id_key: { - name: "_pgroll_new_dropbox_tokens_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "refresh_token", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "google_drive", - checkConstraints: { - google_drive_xata_id_length_xata_id: { - name: "google_drive_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - google_drive_token_id_link: { - name: "google_drive_token_id_link", - columns: ["google_drive_token_id"], - referencedTable: "google_drive_tokens", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_google_drive_xata_id_key: { - name: "_pgroll_new_google_drive_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "google_drive_token_id", - type: "link", - link: { table: "google_drive_tokens" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"google_drive_tokens"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "google_drive_accounts", - checkConstraints: { - google_drive_accounts_xata_id_length_xata_id: { - name: "google_drive_accounts_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_google_drive_accounts_xata_id_key: { - name: "_pgroll_new_google_drive_accounts_xata_id_key", - columns: ["xata_id"], - }, - google_drive_accounts__pgroll_new_email_key: { - name: "google_drive_accounts__pgroll_new_email_key", - columns: ["email"], - }, - }, - columns: [ - { - name: "email", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "is_beta_user", - type: "bool", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "google_drive_directories", - checkConstraints: { - google_drive_directories_xata_id_length_xata_id: { - name: "google_drive_directories_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - google_drive_id_link: { - name: "google_drive_id_link", - columns: ["google_drive_id"], - referencedTable: "google_drive", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - parent_id_link: { - name: "parent_id_link", - columns: ["parent_id"], - referencedTable: "google_drive_directories", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_google_drive_directories_xata_id_key: { - name: "_pgroll_new_google_drive_directories_xata_id_key", - columns: ["xata_id"], - }, - google_drive_directories__pgroll_new_file_id_key: { - name: "google_drive_directories__pgroll_new_file_id_key", - columns: ["file_id"], - }, - }, - columns: [ - { - name: "file_id", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "google_drive_id", - type: "link", - link: { table: "google_drive" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"google_drive"}', - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "parent_id", - type: "link", - link: { table: "google_drive_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"google_drive_directories"}', - }, - { - name: "path", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "google_drive_paths", - checkConstraints: { - google_drive_paths_xata_id_length_xata_id: { - name: "google_drive_paths_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - directory_id_link: { - name: "directory_id_link", - columns: ["directory_id"], - referencedTable: "google_drive_directories", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - google_drive_id_link: { - name: "google_drive_id_link", - columns: ["google_drive_id"], - referencedTable: "google_drive", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_google_drive_paths_xata_id_key: { - name: "_pgroll_new_google_drive_paths_xata_id_key", - columns: ["xata_id"], - }, - google_drive_paths__pgroll_new_google_drive_file_id_key: { - name: "google_drive_paths__pgroll_new_google_drive_file_id_key", - columns: ["file_id"], - }, - }, - columns: [ - { - name: "directory_id", - type: "link", - link: { table: "google_drive_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"google_drive_directories"}', - }, - { - name: "file_id", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "google_drive_id", - type: "link", - link: { table: "google_drive" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"google_drive"}', - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "google_drive_tokens", - checkConstraints: { - google_drive_tokens_xata_id_length_xata_id: { - name: "google_drive_tokens_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_google_drive_tokens_xata_id_key: { - name: "_pgroll_new_google_drive_tokens_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "refresh_token", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "loved_tracks", - checkConstraints: { - loved_tracks_xata_id_length_xata_id: { - name: "loved_tracks_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_loved_tracks_xata_id_key: { - name: "_pgroll_new_loved_tracks_xata_id_key", - columns: ["xata_id"], - }, - loved_tracks__pgroll_new_uri_key: { - name: "loved_tracks__pgroll_new_uri_key", - columns: ["uri"], - }, - }, - columns: [ - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "playback_state", - checkConstraints: { - playback_states_xata_id_length_xata_id: { - name: "playback_states_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_playback_states_xata_id_key: { - name: "_pgroll_new_playback_states_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "progress_ms", - type: "int", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "queue_position", - type: "int", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "playlist_tracks", - checkConstraints: { - playlist_tracks_xata_id_length_xata_id: { - name: "playlist_tracks_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - playlist_id_link: { - name: "playlist_id_link", - columns: ["playlist_id"], - referencedTable: "playlists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_playlist_tracks_xata_id_key: { - name: "_pgroll_new_playlist_tracks_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "playlist_id", - type: "link", - link: { table: "playlists" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"playlists"}', - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "playlists", - checkConstraints: { - playlists_xata_id_length_xata_id: { - name: "playlists_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - created_by_link: { - name: "created_by_link", - columns: ["created_by"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_playlists_xata_id_key: { - name: "_pgroll_new_playlists_xata_id_key", - columns: ["xata_id"], - }, - playlists__pgroll_new_uri_key: { - name: "playlists__pgroll_new_uri_key", - columns: ["uri"], - }, - playlists_apple_music_link_unique: { - name: "playlists_apple_music_link_unique", - columns: ["apple_music_link"], - }, - playlists_spotify_link_unique: { - name: "playlists_spotify_link_unique", - columns: ["spotify_link"], - }, - playlists_tidal_link_unique: { - name: "playlists_tidal_link_unique", - columns: ["tidal_link"], - }, - }, - columns: [ - { - name: "apple_music_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "created_by", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "description", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "picture", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "spotify_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "tidal_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "profile_shouts", - checkConstraints: { - profile_shouts_xata_id_length_xata_id: { - name: "profile_shouts_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - shout_id_link: { - name: "shout_id_link", - columns: ["shout_id"], - referencedTable: "shouts", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_profile_shouts_xata_id_key: { - name: "_pgroll_new_profile_shouts_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "shout_id", - type: "link", - link: { table: "shouts" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"shouts"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "queue_tracks", - checkConstraints: { - queue_tracks_xata_id_length_xata_id: { - name: "queue_tracks_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_queue_tracks_xata_id_key: { - name: "_pgroll_new_queue_tracks_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "file_uri", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "position", - type: "int", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "radios", - checkConstraints: { - radios_xata_id_length_xata_id: { - name: "radios_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_radios_xata_id_key: { - name: "_pgroll_new_radios_xata_id_key", - columns: ["xata_id"], - }, - radios__pgroll_new_uri_key: { - name: "radios__pgroll_new_uri_key", - columns: ["uri"], - }, - radios__pgroll_new_url_key: { - name: "radios__pgroll_new_url_key", - columns: ["url"], - }, - }, - columns: [ - { - name: "description", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "genre", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "logo", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "url", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "website", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "s3_bucket", - checkConstraints: { - s3_xata_id_length_xata_id: { - name: "s3_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - s3_token_id_link: { - name: "s3_token_id_link", - columns: ["s3_token_id"], - referencedTable: "s3_tokens", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_s3_xata_id_key: { - name: "_pgroll_new_s3_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "s3_token_id", - type: "link", - link: { table: "s3_tokens" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"s3_tokens"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "s3_directories", - checkConstraints: { - s3_directories_xata_id_length_xata_id: { - name: "s3_directories_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - parent_id_link: { - name: "parent_id_link", - columns: ["parent_id"], - referencedTable: "s3_directories", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - s3_bucket_id_link: { - name: "s3_bucket_id_link", - columns: ["s3_bucket_id"], - referencedTable: "s3_bucket", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_s3_directories_xata_id_key: { - name: "_pgroll_new_s3_directories_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "parent_id", - type: "link", - link: { table: "s3_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"s3_directories"}', - }, - { - name: "path", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "s3_bucket_id", - type: "link", - link: { table: "s3_bucket" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"s3_bucket"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "s3_paths", - checkConstraints: { - s3_paths_xata_id_length_xata_id: { - name: "s3_paths_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - directory_id_link: { - name: "directory_id_link", - columns: ["directory_id"], - referencedTable: "s3_directories", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - s3_bucket_id_link: { - name: "s3_bucket_id_link", - columns: ["s3_bucket_id"], - referencedTable: "s3_bucket", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_s3_paths_xata_id_key: { - name: "_pgroll_new_s3_paths_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "directory_id", - type: "link", - link: { table: "s3_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"s3_directories"}', - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "path", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "s3_bucket_id", - type: "link", - link: { table: "s3_bucket" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"s3_bucket"}', - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "s3_tokens", - checkConstraints: { - s3_tokens_xata_id_length_xata_id: { - name: "s3_tokens_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_s3_tokens_xata_id_key: { - name: "_pgroll_new_s3_tokens_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "client_access_key", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "secret_access_key", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "scrobbles", - checkConstraints: { - scrobbles_xata_id_length_xata_id: { - name: "scrobbles_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - album_id_link: { - name: "album_id_link", - columns: ["album_id"], - referencedTable: "albums", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - artist_id_link: { - name: "artist_id_link", - columns: ["artist_id"], - referencedTable: "artists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_scrobbles_xata_id_key: { - name: "_pgroll_new_scrobbles_xata_id_key", - columns: ["xata_id"], - }, - scrobbles_uri_unique: { name: "scrobbles_uri_unique", columns: ["uri"] }, - }, - columns: [ - { - name: "album_id", - type: "link", - link: { table: "albums" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"albums"}', - }, - { - name: "artist_id", - type: "link", - link: { table: "artists" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"artists"}', - }, - { - name: "timestamp", - type: "datetime", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "sftp", - checkConstraints: { - sftp_xata_id_length_xata_id: { - name: "sftp_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_sftp_xata_id_key: { - name: "_pgroll_new_sftp_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "sftp_access", - checkConstraints: { - sftp_access_xata_id_length_xata_id: { - name: "sftp_access_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_sftp_access_xata_id_key: { - name: "_pgroll_new_sftp_access_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "sftp_directories", - checkConstraints: { - ftp_directories_xata_id_length_xata_id: { - name: "ftp_directories_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - parent_id_link: { - name: "parent_id_link", - columns: ["parent_id"], - referencedTable: "sftp_directories", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - sftp_id_link: { - name: "sftp_id_link", - columns: ["sftp_id"], - referencedTable: "sftp", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_ftp_directories_xata_id_key: { - name: "_pgroll_new_ftp_directories_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "parent_id", - type: "link", - link: { table: "sftp_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"sftp_directories"}', - }, - { - name: "path", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "sftp_id", - type: "link", - link: { table: "sftp" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"sftp"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "sftp_path", - checkConstraints: { - sftp_path_xata_id_length_xata_id: { - name: "sftp_path_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - directory_id_link: { - name: "directory_id_link", - columns: ["directory_id"], - referencedTable: "sftp_directories", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - sftp_id_link: { - name: "sftp_id_link", - columns: ["sftp_id"], - referencedTable: "sftp", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_sftp_path_xata_id_key: { - name: "_pgroll_new_sftp_path_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "directory_id", - type: "link", - link: { table: "sftp_directories" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"sftp_directories"}', - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "path", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "sftp_id", - type: "link", - link: { table: "sftp" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"sftp"}', - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "shout_likes", - checkConstraints: { - shout_likes_xata_id_length_xata_id: { - name: "shout_likes_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - shout_id_link: { - name: "shout_id_link", - columns: ["shout_id"], - referencedTable: "shouts", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_shout_likes_xata_id_key: { - name: "_pgroll_new_shout_likes_xata_id_key", - columns: ["xata_id"], - }, - shout_likes__pgroll_new_uri_key: { - name: "shout_likes__pgroll_new_uri_key", - columns: ["uri"], - }, - }, - columns: [ - { - name: "shout_id", - type: "link", - link: { table: "shouts" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"shouts"}', - }, - { - name: "uri", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "shout_reports", - checkConstraints: { - shout_reports_xata_id_length_xata_id: { - name: "shout_reports_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - shout_id_link: { - name: "shout_id_link", - columns: ["shout_id"], - referencedTable: "shouts", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_shout_reports_xata_id_key: { - name: "_pgroll_new_shout_reports_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "shout_id", - type: "link", - link: { table: "shouts" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"shouts"}', - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "shouts", - checkConstraints: { - shouts_xata_id_length_xata_id: { - name: "shouts_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - album_id_link: { - name: "album_id_link", - columns: ["album_id"], - referencedTable: "albums", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - artist_id_link: { - name: "artist_id_link", - columns: ["artist_id"], - referencedTable: "artists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - parent_id_link: { - name: "parent_id_link", - columns: ["parent_id"], - referencedTable: "shouts", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - scrobble_id_link: { - name: "scrobble_id_link", - columns: ["scrobble_id"], - referencedTable: "scrobbles", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["author_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_shouts_xata_id_key: { - name: "_pgroll_new_shouts_xata_id_key", - columns: ["xata_id"], - }, - shouts__pgroll_new_uri_key: { - name: "shouts__pgroll_new_uri_key", - columns: ["uri"], - }, - }, - columns: [ - { - name: "album_id", - type: "link", - link: { table: "albums" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"albums"}', - }, - { - name: "artist_id", - type: "link", - link: { table: "artists" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"artists"}', - }, - { - name: "author_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "content", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "parent_id", - type: "link", - link: { table: "shouts" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"shouts"}', - }, - { - name: "scrobble_id", - type: "link", - link: { table: "scrobbles" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"scrobbles"}', - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: false, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "spotify_accounts", - checkConstraints: { - spotify_accounts_xata_id_length_xata_id: { - name: "spotify_accounts_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_spotify_accounts_xata_id_key: { - name: "_pgroll_new_spotify_accounts_xata_id_key", - columns: ["xata_id"], - }, - spotify_accounts__pgroll_new_email_key: { - name: "spotify_accounts__pgroll_new_email_key", - columns: ["email"], - }, - }, - columns: [ - { - name: "email", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "is_beta_user", - type: "bool", - notNull: true, - unique: false, - defaultValue: "false", - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "spotify_tokens", - checkConstraints: { - spotify_tokens_xata_id_length_xata_id: { - name: "spotify_tokens_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_spotify_tokens_xata_id_key: { - name: "_pgroll_new_spotify_tokens_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "access_token", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "refresh_token", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "tags", - checkConstraints: { - tags_xata_id_length_xata_id: { - name: "tags_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_tags_xata_id_key: { - name: "_pgroll_new_tags_xata_id_key", - columns: ["xata_id"], - }, - tags__pgroll_new_name_key: { - name: "tags__pgroll_new_name_key", - columns: ["name"], - }, - }, - columns: [ - { - name: "name", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "track_tags", - checkConstraints: { - track_tags_xata_id_length_xata_id: { - name: "track_tags_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - tag_id_link: { - name: "tag_id_link", - columns: ["tag_id"], - referencedTable: "tags", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_track_tags_xata_id_key: { - name: "_pgroll_new_track_tags_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "tag_id", - type: "link", - link: { table: "tags" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tags"}', - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "tracks", - checkConstraints: { - tracks_xata_id_length_xata_id: { - name: "tracks_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_tracks_xata_id_key: { - name: "_pgroll_new_tracks_xata_id_key", - columns: ["xata_id"], - }, - tracks__pgroll_new_mb_id_key: { - name: "tracks__pgroll_new_mb_id_key", - columns: ["mb_id"], - }, - tracks__pgroll_new_sha256_key: { - name: "tracks__pgroll_new_sha256_key", - columns: ["sha256"], - }, - tracks__pgroll_new_uri_key: { - name: "tracks__pgroll_new_uri_key", - columns: ["uri"], - }, - tracks_tidal_link_unique: { - name: "tracks_tidal_link_unique", - columns: ["tidal_link"], - }, - tracks_youtube_link_unique: { - name: "tracks_youtube_link_unique", - columns: ["youtube_link"], - }, - }, - columns: [ - { - name: "album", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "album_art", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "album_artist", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "album_uri", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "apple_music_link", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "artist", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "artist_uri", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "composer", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "copyright_message", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "disc_number", - type: "int", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "duration", - type: "int", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "genre", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "label", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "lastfm_link", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "lyrics", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "mb_id", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "sha256", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "spotify_link", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "tidal_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "title", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "track_number", - type: "int", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - { - name: "youtube_link", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - ], - }, - { - name: "user_albums", - checkConstraints: { - user_albums_xata_id_length_xata_id: { - name: "user_albums_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - album_id_link: { - name: "album_id_link", - columns: ["album_id"], - referencedTable: "albums", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_user_albums_xata_id_key: { - name: "_pgroll_new_user_albums_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "album_id", - type: "link", - link: { table: "albums" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"albums"}', - }, - { - name: "scrobbles", - type: "int", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "uri", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "user_artists", - checkConstraints: { - user_artists_xata_id_length_xata_id: { - name: "user_artists_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - artist_id_link: { - name: "artist_id_link", - columns: ["artist_id"], - referencedTable: "artists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_user_artists_xata_id_key: { - name: "_pgroll_new_user_artists_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "artist_id", - type: "link", - link: { table: "artists" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"artists"}', - }, - { - name: "scrobbles", - type: "int", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "uri", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "user_playlists", - checkConstraints: { - user_playlists_xata_id_length_xata_id: { - name: "user_playlists_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - playlist_id_link: { - name: "playlist_id_link", - columns: ["playlist_id"], - referencedTable: "playlists", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_user_playlists_xata_id_key: { - name: "_pgroll_new_user_playlists_xata_id_key", - columns: ["xata_id"], - }, - user_playlists__pgroll_new_uri_key: { - name: "user_playlists__pgroll_new_uri_key", - columns: ["uri"], - }, - }, - columns: [ - { - name: "playlist_id", - type: "link", - link: { table: "playlists" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"playlists"}', - }, - { - name: "uri", - type: "text", - notNull: false, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "user_tracks", - checkConstraints: { - user_tracks_xata_id_length_xata_id: { - name: "user_tracks_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - track_id_link: { - name: "track_id_link", - columns: ["track_id"], - referencedTable: "tracks", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "SET NULL", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_user_tracks_xata_id_key: { - name: "_pgroll_new_user_tracks_xata_id_key", - columns: ["xata_id"], - }, - }, - columns: [ - { - name: "scrobbles", - type: "int", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "track_id", - type: "link", - link: { table: "tracks" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"tracks"}', - }, - { - name: "uri", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "users", - checkConstraints: { - users_xata_id_length_xata_id: { - name: "users_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: {}, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_users_xata_id_key: { - name: "_pgroll_new_users_xata_id_key", - columns: ["xata_id"], - }, - users__pgroll_new_did_key: { - name: "users__pgroll_new_did_key", - columns: ["did"], - }, - users__pgroll_new_handle_key: { - name: "users__pgroll_new_handle_key", - columns: ["handle"], - }, - }, - columns: [ - { - name: "avatar", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "did", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "display_name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "handle", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, - { - name: "webscrobblers", - checkConstraints: { - webscrobblers_xata_id_length_xata_id: { - name: "webscrobblers_xata_id_length_xata_id", - columns: ["xata_id"], - definition: "CHECK ((length(xata_id) < 256))", - }, - }, - foreignKeys: { - user_id_link: { - name: "user_id_link", - columns: ["user_id"], - referencedTable: "users", - referencedColumns: ["xata_id"], - onDelete: "CASCADE", - }, - }, - primaryKey: [], - uniqueConstraints: { - _pgroll_new_webscrobblers_xata_id_key: { - name: "_pgroll_new_webscrobblers_xata_id_key", - columns: ["xata_id"], - }, - webscrobblers__pgroll_new_uuid_key: { - name: "webscrobblers__pgroll_new_uuid_key", - columns: ["uuid"], - }, - }, - columns: [ - { - name: "description", - type: "text", - notNull: false, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "enabled", - type: "bool", - notNull: false, - unique: false, - defaultValue: "true", - comment: "", - }, - { - name: "name", - type: "text", - notNull: true, - unique: false, - defaultValue: null, - comment: "", - }, - { - name: "user_id", - type: "link", - link: { table: "users" }, - notNull: true, - unique: false, - defaultValue: null, - comment: '{"xata.link":"users"}', - }, - { - name: "uuid", - type: "text", - notNull: true, - unique: true, - defaultValue: null, - comment: "", - }, - { - name: "xata_createdat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_id", - type: "text", - notNull: true, - unique: true, - defaultValue: "('rec_'::text || (xata_private.xid())::text)", - comment: "", - }, - { - name: "xata_updatedat", - type: "datetime", - notNull: true, - unique: false, - defaultValue: "now()", - comment: "", - }, - { - name: "xata_version", - type: "int", - notNull: true, - unique: false, - defaultValue: "0", - comment: "", - }, - ], - }, -] as const; - -export type SchemaTables = typeof tables; -export type InferredTypes = SchemaInference; - -export type AlbumTags = InferredTypes["album_tags"]; -export type AlbumTagsRecord = AlbumTags & XataRecord; - -export type AlbumTracks = InferredTypes["album_tracks"]; -export type AlbumTracksRecord = AlbumTracks & XataRecord; - -export type Albums = InferredTypes["albums"]; -export type AlbumsRecord = Albums & XataRecord; - -export type ApiKeys = InferredTypes["api_keys"]; -export type ApiKeysRecord = ApiKeys & XataRecord; - -export type ArtistAlbums = InferredTypes["artist_albums"]; -export type ArtistAlbumsRecord = ArtistAlbums & XataRecord; - -export type ArtistTags = InferredTypes["artist_tags"]; -export type ArtistTagsRecord = ArtistTags & XataRecord; - -export type ArtistTracks = InferredTypes["artist_tracks"]; -export type ArtistTracksRecord = ArtistTracks & XataRecord; - -export type Artists = InferredTypes["artists"]; -export type ArtistsRecord = Artists & XataRecord; - -export type BuiltinStoragePaths = InferredTypes["builtin_storage_paths"]; -export type BuiltinStoragePathsRecord = BuiltinStoragePaths & XataRecord; - -export type Dropbox = InferredTypes["dropbox"]; -export type DropboxRecord = Dropbox & XataRecord; - -export type DropboxAccounts = InferredTypes["dropbox_accounts"]; -export type DropboxAccountsRecord = DropboxAccounts & XataRecord; - -export type DropboxDirectories = InferredTypes["dropbox_directories"]; -export type DropboxDirectoriesRecord = DropboxDirectories & XataRecord; - -export type DropboxPaths = InferredTypes["dropbox_paths"]; -export type DropboxPathsRecord = DropboxPaths & XataRecord; - -export type DropboxTokens = InferredTypes["dropbox_tokens"]; -export type DropboxTokensRecord = DropboxTokens & XataRecord; - -export type GoogleDrive = InferredTypes["google_drive"]; -export type GoogleDriveRecord = GoogleDrive & XataRecord; - -export type GoogleDriveAccounts = InferredTypes["google_drive_accounts"]; -export type GoogleDriveAccountsRecord = GoogleDriveAccounts & XataRecord; - -export type GoogleDriveDirectories = InferredTypes["google_drive_directories"]; -export type GoogleDriveDirectoriesRecord = GoogleDriveDirectories & XataRecord; - -export type GoogleDrivePaths = InferredTypes["google_drive_paths"]; -export type GoogleDrivePathsRecord = GoogleDrivePaths & XataRecord; - -export type GoogleDriveTokens = InferredTypes["google_drive_tokens"]; -export type GoogleDriveTokensRecord = GoogleDriveTokens & XataRecord; - -export type LovedTracks = InferredTypes["loved_tracks"]; -export type LovedTracksRecord = LovedTracks & XataRecord; - -export type PlaybackState = InferredTypes["playback_state"]; -export type PlaybackStateRecord = PlaybackState & XataRecord; - -export type PlaylistTracks = InferredTypes["playlist_tracks"]; -export type PlaylistTracksRecord = PlaylistTracks & XataRecord; - -export type Playlists = InferredTypes["playlists"]; -export type PlaylistsRecord = Playlists & XataRecord; - -export type ProfileShouts = InferredTypes["profile_shouts"]; -export type ProfileShoutsRecord = ProfileShouts & XataRecord; - -export type QueueTracks = InferredTypes["queue_tracks"]; -export type QueueTracksRecord = QueueTracks & XataRecord; - -export type Radios = InferredTypes["radios"]; -export type RadiosRecord = Radios & XataRecord; - -export type S3Bucket = InferredTypes["s3_bucket"]; -export type S3BucketRecord = S3Bucket & XataRecord; - -export type S3Directories = InferredTypes["s3_directories"]; -export type S3DirectoriesRecord = S3Directories & XataRecord; - -export type S3Paths = InferredTypes["s3_paths"]; -export type S3PathsRecord = S3Paths & XataRecord; - -export type S3Tokens = InferredTypes["s3_tokens"]; -export type S3TokensRecord = S3Tokens & XataRecord; - -export type Scrobbles = InferredTypes["scrobbles"]; -export type ScrobblesRecord = Scrobbles & XataRecord; - -export type Sftp = InferredTypes["sftp"]; -export type SftpRecord = Sftp & XataRecord; - -export type SftpAccess = InferredTypes["sftp_access"]; -export type SftpAccessRecord = SftpAccess & XataRecord; - -export type SftpDirectories = InferredTypes["sftp_directories"]; -export type SftpDirectoriesRecord = SftpDirectories & XataRecord; - -export type SftpPath = InferredTypes["sftp_path"]; -export type SftpPathRecord = SftpPath & XataRecord; - -export type ShoutLikes = InferredTypes["shout_likes"]; -export type ShoutLikesRecord = ShoutLikes & XataRecord; - -export type ShoutReports = InferredTypes["shout_reports"]; -export type ShoutReportsRecord = ShoutReports & XataRecord; - -export type Shouts = InferredTypes["shouts"]; -export type ShoutsRecord = Shouts & XataRecord; - -export type SpotifyAccounts = InferredTypes["spotify_accounts"]; -export type SpotifyAccountsRecord = SpotifyAccounts & XataRecord; - -export type SpotifyTokens = InferredTypes["spotify_tokens"]; -export type SpotifyTokensRecord = SpotifyTokens & XataRecord; - -export type Tags = InferredTypes["tags"]; -export type TagsRecord = Tags & XataRecord; - -export type TrackTags = InferredTypes["track_tags"]; -export type TrackTagsRecord = TrackTags & XataRecord; - -export type Tracks = InferredTypes["tracks"]; -export type TracksRecord = Tracks & XataRecord; - -export type UserAlbums = InferredTypes["user_albums"]; -export type UserAlbumsRecord = UserAlbums & XataRecord; - -export type UserArtists = InferredTypes["user_artists"]; -export type UserArtistsRecord = UserArtists & XataRecord; - -export type UserPlaylists = InferredTypes["user_playlists"]; -export type UserPlaylistsRecord = UserPlaylists & XataRecord; - -export type UserTracks = InferredTypes["user_tracks"]; -export type UserTracksRecord = UserTracks & XataRecord; - -export type Users = InferredTypes["users"]; -export type UsersRecord = Users & XataRecord; - -export type Webscrobblers = InferredTypes["webscrobblers"]; -export type WebscrobblersRecord = Webscrobblers & XataRecord; - -export type DatabaseSchema = { - album_tags: AlbumTagsRecord; - album_tracks: AlbumTracksRecord; - albums: AlbumsRecord; - api_keys: ApiKeysRecord; - artist_albums: ArtistAlbumsRecord; - artist_tags: ArtistTagsRecord; - artist_tracks: ArtistTracksRecord; - artists: ArtistsRecord; - builtin_storage_paths: BuiltinStoragePathsRecord; - dropbox: DropboxRecord; - dropbox_accounts: DropboxAccountsRecord; - dropbox_directories: DropboxDirectoriesRecord; - dropbox_paths: DropboxPathsRecord; - dropbox_tokens: DropboxTokensRecord; - google_drive: GoogleDriveRecord; - google_drive_accounts: GoogleDriveAccountsRecord; - google_drive_directories: GoogleDriveDirectoriesRecord; - google_drive_paths: GoogleDrivePathsRecord; - google_drive_tokens: GoogleDriveTokensRecord; - loved_tracks: LovedTracksRecord; - playback_state: PlaybackStateRecord; - playlist_tracks: PlaylistTracksRecord; - playlists: PlaylistsRecord; - profile_shouts: ProfileShoutsRecord; - queue_tracks: QueueTracksRecord; - radios: RadiosRecord; - s3_bucket: S3BucketRecord; - s3_directories: S3DirectoriesRecord; - s3_paths: S3PathsRecord; - s3_tokens: S3TokensRecord; - scrobbles: ScrobblesRecord; - sftp: SftpRecord; - sftp_access: SftpAccessRecord; - sftp_directories: SftpDirectoriesRecord; - sftp_path: SftpPathRecord; - shout_likes: ShoutLikesRecord; - shout_reports: ShoutReportsRecord; - shouts: ShoutsRecord; - spotify_accounts: SpotifyAccountsRecord; - spotify_tokens: SpotifyTokensRecord; - tags: TagsRecord; - track_tags: TrackTagsRecord; - tracks: TracksRecord; - user_albums: UserAlbumsRecord; - user_artists: UserArtistsRecord; - user_playlists: UserPlaylistsRecord; - user_tracks: UserTracksRecord; - users: UsersRecord; - webscrobblers: WebscrobblersRecord; -}; - -const DatabaseClient = buildClient(); - -const defaultOptions = { - databaseURL: - "https://Tsiry-Sandratraina-s-workspace-b1ficn.us-east-1.xata.sh/db/rocksky", -}; - -export class XataClient extends DatabaseClient { - constructor(options?: BaseClientOptions) { - super({ ...defaultOptions, ...options }, tables); - } -} - -let instance: XataClient | undefined; - -export const getXataClient = () => { - if (instance) return instance; - - instance = new XataClient(); - return instance; -}; diff --git a/bun.lock b/bun.lock index 2261cefc..ec58ce7b 100644 --- a/bun.lock +++ b/bun.lock @@ -33,7 +33,6 @@ "@opentelemetry/sdk-node": "^0.200.0", "@opentelemetry/semantic-conventions": "^1.32.0", "@pyroscope/nodejs": "^0.4.5", - "@xata.io/client": "^0.0.0-next.va121e4207b94bfe0a3c025fc00b247b923880930", "assert": "^2.1.0", "axios": "^1.7.9", "better-sqlite3": "^11.8.1", @@ -328,9 +327,6 @@ "wrangler": "^3.107.3", }, }, - "crates": { - "name": "@rocksky/crates", - }, }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.3", "", {}, "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA=="], @@ -943,8 +939,6 @@ "@rocksky/cli": ["@rocksky/cli@workspace:apps/cli"], - "@rocksky/crates": ["@rocksky/crates@workspace:crates"], - "@rocksky/doc": ["@rocksky/doc@workspace:apps/doc"], "@rocksky/spotify-proxy": ["@rocksky/spotify-proxy@workspace:apps/spotify-proxy"], -- 2.51.2