Migrated read APIs to Nitro

This commit is contained in:
lucast committed 2026-09-29 00:34:50 +02:00
1 parent eb4e9f0700
commit ee5809a489
26 files changed
+558

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
import { getRouterParam } from "h3"
import { Album } from "../../models/album"
import { requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getRouterParam(event, "id"))
if ("error" in parsedId) return validationResponse(event, parsedId.error)
return Album.findOne({ _id: parsedId.value })
.populate("artists")
.populate("genre")
.exec()
})
+24
View File
@@ -0,0 +1,24 @@
import { getQuery, getRouterParam } from "h3"
import { Track } from "../../../models/track"
import { requireAuthenticatedUser } from "../../../utils/auth"
import { parseMongoId, parseNumber, validationResponse } from "../../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getRouterParam(event, "id"))
if ("error" in parsedId) return validationResponse(event, parsedId.error)
const query = getQuery(event)
const limit = parseNumber(query.limit, 9, 1)
if ("error" in limit) return validationResponse(event, limit.error)
const skip = parseNumber(query.skip, 0, 0)
if ("error" in skip) return validationResponse(event, skip.error)
return Track.find({ album: parsedId.value })
.limit(limit.value || 9)
.skip(skip.value || 0)
.populate("artists")
.populate({ path: "album", select: "-artists -genre" })
.exec()
})
+16
View File
@@ -0,0 +1,16 @@
import { getQuery } from "h3"
import { Album } from "../../models/album"
import { requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoIds, validationResponse } from "../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedIds = parseMongoIds(getQuery(event).ids)
if ("error" in parsedIds) return validationResponse(event, parsedIds.error)
return Album.find({ _id: { $in: parsedIds.value } })
.populate("artists")
.populate("genre")
.exec()
})
+8
View File
@@ -0,0 +1,8 @@
import { Album } from "../../models/album"
import { requireAuthenticatedUser } from "../../utils/auth"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
return Album.find({}).populate("artists").populate("genre")
})
+8
View File
@@ -0,0 +1,8 @@
import { Artist } from "../../models/artist"
import { requireAuthenticatedUser } from "../../utils/auth"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
return Artist.find({})
})
+8
View File
@@ -0,0 +1,8 @@
import { Genre } from "../../models/genre"
import { requireAuthenticatedUser } from "../../utils/auth"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
return Genre.find({})
})
+14
View File
@@ -0,0 +1,14 @@
import { Track } from "../../models/track"
import { requireAuthenticatedUser } from "../../utils/auth"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
return Track.find({})
.populate({
path: "album",
select: "-artists",
populate: { path: "genre" },
})
.populate("artists")
})
+13
View File
@@ -0,0 +1,13 @@
import { getRouterParam } from "h3"
import { Artist } from "../../models/artist"
import { requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getRouterParam(event, "id"))
if ("error" in parsedId) return validationResponse(event, parsedId.error)
return Artist.findOne({ _id: parsedId.value }).exec()
})
+24
View File
@@ -0,0 +1,24 @@
import { getQuery, getRouterParam } from "h3"
import { Album } from "../../../models/album"
import { requireAuthenticatedUser } from "../../../utils/auth"
import { parseMongoId, parseNumber, validationResponse } from "../../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getRouterParam(event, "id"))
if ("error" in parsedId) return validationResponse(event, parsedId.error)
const query = getQuery(event)
const limit = parseNumber(query.limit, 9, 1)
if ("error" in limit) return validationResponse(event, limit.error)
const skip = parseNumber(query.skip, 0, 0)
if ("error" in skip) return validationResponse(event, skip.error)
return Album.find({ artists: parsedId.value })
.limit(limit.value || 9)
.skip(skip.value || 0)
.populate("genre")
.populate("artists")
.exec()
})
+24
View File
@@ -0,0 +1,24 @@
import { getQuery, getRouterParam } from "h3"
import { Track } from "../../../models/track"
import { requireAuthenticatedUser } from "../../../utils/auth"
import { parseMongoId, parseNumber, validationResponse } from "../../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getRouterParam(event, "id"))
if ("error" in parsedId) return validationResponse(event, parsedId.error)
const query = getQuery(event)
const limit = parseNumber(query.limit, 9, 1)
if ("error" in limit) return validationResponse(event, limit.error)
const skip = parseNumber(query.skip, 0, 0)
if ("error" in skip) return validationResponse(event, skip.error)
return Track.find({ artists: parsedId.value })
.limit(limit.value || 9)
.skip(skip.value || 0)
.populate({ path: "album", select: "-artists" })
.populate("artists")
.exec()
})
+13
View File
@@ -0,0 +1,13 @@
import { getQuery } from "h3"
import { Artist } from "../../models/artist"
import { requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoIds, validationResponse } from "../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedIds = parseMongoIds(getQuery(event).ids)
if ("error" in parsedIds) return validationResponse(event, parsedIds.error)
return Artist.find({ _id: { $in: parsedIds.value } }).exec()
})
+31
View File
@@ -0,0 +1,31 @@
import mongoose, { type PipelineStage } from "mongoose"
import { Album } from "../../../models/album"
import { parseMongoId, parseNumber, validationResponse } from "../../../utils/api-validation"
import { requireAuthenticatedUser } from "../../../utils/auth"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const id = parseMongoId(getRouterParam(event, "id"))
if ("error" in id) return validationResponse(event, id.error)
const { limit: rawLimit } = getQuery(event)
const limit = parseNumber(rawLimit, 10, 1)
if ("error" in limit) return validationResponse(event, limit.error)
const pipeline: PipelineStage[] = [
{ $match: { genre: new mongoose.Types.ObjectId(id.value) } },
{ $sample: { size: limit.value || 10 } },
{
$lookup: {
from: "artists",
localField: "artists",
foreignField: "_id",
as: "artists",
},
},
{ $unset: "genre" },
]
return Album.aggregate(pipeline)
})
+14
View File
@@ -0,0 +1,14 @@
import { Genre } from "../../models/genre"
import { parseNumber, validationResponse } from "../../utils/api-validation"
import { requireAuthenticatedUser } from "../../utils/auth"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const { limit: rawLimit } = getQuery(event)
const limit = parseNumber(rawLimit, 10, 1)
if ("error" in limit) return validationResponse(event, limit.error)
const genres = await Genre.aggregate([{ $sample: { size: limit.value || 10 } }])
return genres
})
+40
View File
@@ -0,0 +1,40 @@
import mongoose, { type PipelineStage } from "mongoose"
import { Track } from "../../../models/track"
import { parseMongoId, parseNumber, validationResponse } from "../../../utils/api-validation"
import { requireAuthenticatedUser } from "../../../utils/auth"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const id = parseMongoId(getRouterParam(event, "id"))
if ("error" in id) return validationResponse(event, id.error)
const { limit: rawLimit } = getQuery(event)
const limit = parseNumber(rawLimit, 10, 1)
if ("error" in limit) return validationResponse(event, limit.error)
const pipeline: PipelineStage[] = [
{
$lookup: {
from: "albums",
localField: "album",
foreignField: "_id",
as: "album",
},
},
{ $unwind: "$album" },
{
$lookup: {
from: "artists",
localField: "artists",
foreignField: "_id",
as: "artists",
},
},
{ $match: { "album.genre": new mongoose.Types.ObjectId(id.value) } },
{ $sample: { size: limit.value || 10 } },
{ $unset: "album.artists" },
]
return Track.aggregate(pipeline)
})
+95
View File
@@ -0,0 +1,95 @@
import Fuse from "fuse.js"
import { Album } from "../models/album"
import { Artist } from "../models/artist"
import { Track } from "../models/track"
import {
parseNumber,
parseQueryString,
validationResponse,
} from "../utils/api-validation"
import { requireAuthenticatedUser } from "../utils/auth"
const searchTypes = ["track", "album", "artist", "default"] as const
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const query = getQuery(event)
const q = parseQueryString(query.q)
if ("error" in q) return validationResponse(event, q.error)
let type = "default"
if (query.type !== undefined) {
const parsedType = parseQueryString(query.type)
if ("error" in parsedType) return validationResponse(event, parsedType.error)
if (!searchTypes.includes(parsedType.value as (typeof searchTypes)[number])) {
return validationResponse(
event,
`Invalid enum value. Expected 'track' | 'album' | 'artist' | 'default', received '${parsedType.value}'`,
)
}
type = parsedType.value
}
const parsedLimit = parseNumber(query.limit, 9, 1)
if ("error" in parsedLimit) return validationResponse(event, parsedLimit.error)
const limit = parsedLimit.value
let results: { tracks: any[]; albums: any[]; artists: any[] } = {
tracks: [],
albums: [],
artists: [],
}
if (type === "track" || type === "default") {
const rawTracks = await Track.find({})
.populate(["album", "artists"])
.lean()
const fuse = new Fuse(rawTracks, {
keys: ["name", "lyrics.text"],
threshold: 0.3,
distance: 100,
ignoreLocation: true,
})
results.tracks = fuse.search(q.value).slice(0, limit).map((result) => result.item)
}
if (type === "album" || type === "default") {
const rawAlbums = await Album.find({})
.populate(["artists", "genre"])
.lean()
const fuse = new Fuse(rawAlbums, {
keys: ["name"],
threshold: 0.3,
distance: 100,
ignoreLocation: true,
})
results.albums = fuse.search(q.value).slice(0, limit).map((result) => result.item)
}
if (type === "artist" || type === "default") {
const rawArtists = await Artist.find({}).lean()
const fuse = new Fuse(rawArtists, {
keys: ["name"],
threshold: 0.3,
distance: 100,
ignoreLocation: true,
})
results.artists = fuse.search(q.value).slice(0, limit).map((result) => result.item)
}
// Keep the original API's split allocation, including when type selects one category.
results = {
tracks: results.tracks.slice(0, Math.ceil(limit / 3)),
albums: results.albums.slice(0, Math.floor(limit / 3)),
artists: results.artists.slice(0, Math.floor(limit / 3)),
}
return results
})
+20
View File
@@ -0,0 +1,20 @@
import { getQuery } from "h3"
import { Track } from "../models/track"
import { requireAuthenticatedUser } from "../utils/auth"
import { parseMongoIds, validationResponse } from "../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedIds = parseMongoIds(getQuery(event).ids)
if ("error" in parsedIds) return validationResponse(event, parsedIds.error)
return Track.find({ _id: { $in: parsedIds.value } })
.populate({
path: "album",
select: "-artists",
populate: { path: "genre" },
})
.populate("artists")
.exec()
})
+20
View File
@@ -0,0 +1,20 @@
import { getRouterParam } from "h3"
import { Track } from "../../models/track"
import { requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getRouterParam(event, "id"))
if ("error" in parsedId) return validationResponse(event, parsedId.error)
return Track.findOne({ _id: parsedId.value })
.populate({
path: "album",
select: "-artists",
populate: { path: "genre" },
})
.populate("artists")
.exec()
})
+16
View File
@@ -0,0 +1,16 @@
import { getRouterParam } from "h3"
import { Track } from "../../../models/track"
import { requireAuthenticatedUser } from "../../../utils/auth"
import { parseMongoId, validationResponse } from "../../../utils/api-validation"
export default defineEventHandler(async (event) => {
await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getRouterParam(event, "id"))
if ("error" in parsedId) return validationResponse(event, parsedId.error)
const track = await Track.findOne({ _id: parsedId.value }).select("lyrics").exec()
if (!track) return {}
return track.lyrics
})
+5
View File
@@ -0,0 +1,5 @@
import { requireAuthenticatedUser } from "../../utils/auth"
export default defineEventHandler(async (event) => {
return await requireAuthenticatedUser(event)
})
+15
View File
@@ -0,0 +1,15 @@
import { User } from "../../models/user"
import { requireAuthenticatedUser } from "../../utils/auth"
import { setResponseStatus } from "h3"
export default defineEventHandler(async (event) => {
const authenticatedUser = await requireAuthenticatedUser(event)
const user = await User.findById(authenticatedUser._id).populate("savedTracks")
if (!user) {
setResponseStatus(event, 404)
return { message: "User not found" }
}
return user.savedTracks
})
+26
View File
@@ -0,0 +1,26 @@
import { getQuery, setResponseStatus } from "h3"
import mongoose, { type ObjectId } from "mongoose"
import { User } from "../../models/user"
import { requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation"
export default defineEventHandler(async (event) => {
const authenticatedUser = await requireAuthenticatedUser(event)
const parsedId = parseMongoId(getQuery(event).id)
if ("error" in parsedId) return validationResponse(event, parsedId.error)
const user = await User.findById(authenticatedUser._id)
if (!user) {
setResponseStatus(event, 404)
return { message: "User not found" }
}
// Preserve the legacy comparison behavior and empty-object response.
if ((user.savedTracks as ObjectId[]).includes(parsedId.value as unknown as ObjectId)) {
return {}
}
user.savedTracks.push(new mongoose.Types.ObjectId(parsedId.value))
await user.save()
return {}
})