From 5774be1d226b150c5b1d1f2e13c23aeb52e1fac4 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 29 Apr 2026 15:50:10 +0000 Subject: [PATCH] Implement api response transform to old structure --- .../common/database/drizzle/drizzleTypes.ts | 1 + .../common/database/drizzle/entityUtils.ts | 18 +++++++++++++++--- .../drizzle/repositories/BaseRepository.ts | 5 +++++ .../drizzle/repositories/PlayRepository.ts | 12 ++++-------- .../scrobblers/AbstractScrobbleClient.ts | 4 +--- src/backend/server/api.ts | 12 +++++++++--- src/backend/utils/ErrorUtils.ts | 2 +- 7 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/backend/common/database/drizzle/drizzleTypes.ts b/src/backend/common/database/drizzle/drizzleTypes.ts index e4c55903..7a7e3b0d 100644 --- a/src/backend/common/database/drizzle/drizzleTypes.ts +++ b/src/backend/common/database/drizzle/drizzleTypes.ts @@ -13,6 +13,7 @@ export type PlayInputNew = typeof playInputs.$inferInsert; export type PlayInputSelect = typeof playInputs.$inferSelect; export type PlaySelect = typeof plays.$inferSelect; +export type PlaySelectRel = ModelWithRelations; export type PlayNew = typeof plays.$inferInsert; diff --git a/src/backend/common/database/drizzle/entityUtils.ts b/src/backend/common/database/drizzle/entityUtils.ts index c7ac974d..2b131baa 100644 --- a/src/backend/common/database/drizzle/entityUtils.ts +++ b/src/backend/common/database/drizzle/entityUtils.ts @@ -1,14 +1,15 @@ import assert from "node:assert"; -import { PlayNew, PlaySelect } from "./drizzleTypes.js"; +import { PlayNew, PlaySelect, PlaySelectRel } from "./drizzleTypes.js"; import { PlayInputNew } from "./drizzleTypes.js"; import { QueueStateNew } from "./drizzleTypes.js"; import { ComponentNew } from "./drizzleTypes.js"; -import { MarkOptional } from "ts-essentials"; -import { ErrorLike, PlayObject } from "../../../../core/Atomic.js"; +import { MarkOptional, MarkRequired } from "ts-essentials"; +import { CLIENT_DEAD_QUEUE, DeadLetterScrobble, ErrorLike, PlayObject } from "../../../../core/Atomic.js"; import dayjs, { Dayjs } from "dayjs"; import { asPlay } from "../../../../core/PlayMarshalUtils.js"; import { playContentBasicInvariantTransform, playMbidIdentifier } from "../../../utils/PlayComparisonUtils.js"; import { hashObject } from "../../../utils/StringUtils.js"; +import { messageWithCauses } from "../../../utils/ErrorUtils.js"; export const generateComponentEntity = (data: MarkOptional): ComponentNew => { assert(data.name !== undefined, 'Must provide name'); @@ -68,6 +69,17 @@ export const hydratePlaySelect = (select: PlaySelect, opts: PlayHydateOptions[]) return res; } +export const playSelectToDeadScrobble = (select: MarkRequired): DeadLetterScrobble => { + const deadQueue = select.queueStates.find(x => x.queueName === CLIENT_DEAD_QUEUE); + return { + play: select.play, + id: select.uid, + source: select.play.meta.source, + retries: deadQueue.retries, + error: messageWithCauses(deadQueue.error as Error) + } +} + export const generateInputEntity = (data: PlayInputNew): PlayInputNew => { return data; } diff --git a/src/backend/common/database/drizzle/repositories/BaseRepository.ts b/src/backend/common/database/drizzle/repositories/BaseRepository.ts index a3f42795..ac93b706 100644 --- a/src/backend/common/database/drizzle/repositories/BaseRepository.ts +++ b/src/backend/common/database/drizzle/repositories/BaseRepository.ts @@ -26,6 +26,11 @@ export interface PaginatedQueryResponse { offset: number } +export interface PaginatedResponse { + data: T[] + meta: PaginatedQueryResponse +} + export interface ComponentConstrainedRepoOpts { componentId?: number } diff --git a/src/backend/common/database/drizzle/repositories/PlayRepository.ts b/src/backend/common/database/drizzle/repositories/PlayRepository.ts index 31da5217..f48cd002 100644 --- a/src/backend/common/database/drizzle/repositories/PlayRepository.ts +++ b/src/backend/common/database/drizzle/repositories/PlayRepository.ts @@ -4,14 +4,14 @@ import { loggerNoop } from "../../../MaybeLogger.js"; import { ErrorLike, PlayObject } from "../../../../../core/Atomic.js"; import { generateInputEntity, generatePlayEntity, PlayEntityOpts, hydratePlaySelect, PlayHydateOptions } from "../entityUtils.js"; import { playInputs, plays, queueStates, relations } from "../schema/schema.js"; -import { PlayNew, PlaySelect, PlayInputNew, FindWhere, FindMany, CompareOpKey, QueueStateSelect, PlayInputSelect } from "../drizzleTypes.js";; +import { PlayNew, PlaySelect, PlayInputNew, FindWhere, FindMany, CompareOpKey, QueueStateSelect, PlayInputSelect, PlaySelectRel } from "../drizzleTypes.js";; import { MarkOptional, MarkRequired, PathValue } from "ts-essentials"; import { genGroupIdStrFromPlay, removeEmptyArrays, removeUndefinedKeys } from "../../../../utils.js"; import dayjs, { Dayjs } from "dayjs"; import { RelationsFieldFilter, eq, inArray, ne, notInArray, desc, asc, and } from "drizzle-orm"; import { CompactableProperty, RetentionOptions, retentionPlayTypes } from "../../../infrastructure/config/database.js"; import { shortTodayAwareFormat } from "../../../../../core/TimeUtils.js"; -import { buildDateCompare, CompareDateOp, ComponentConstrainedRepoOpts, DrizzleBaseRepository, DrizzleRepositoryOpts, PaginatedQueryResponse } from "./BaseRepository.js"; +import { buildDateCompare, CompareDateOp, ComponentConstrainedRepoOpts, DrizzleBaseRepository, DrizzleRepositoryOpts, PaginatedQueryResponse, PaginatedResponse } from "./BaseRepository.js"; import { asPlay } from "../../../../../core/PlayMarshalUtils.js"; import assert from "node:assert"; import { hashObject, parseArrayFromMaybeString } from "../../../../utils/StringUtils.js"; @@ -171,18 +171,14 @@ export class DrizzlePlayRepository extends DrizzleBaseRepository<'plays'> { return results; } - findPlaysPaginated = async (args: QueryPlaysOpts, opts: HydrateOpts & ComponentConstrainedRepoOpts = {}): Promise<{data: (PlaySelect & { - queueStates?: QueueStateSelect[] - input?: PlayInputSelect - parent?: PlaySelect - })[], meta: {offset: number, limit: number}}> => { + findPlaysPaginated = async (args: QueryPlaysOpts, opts: HydrateOpts & ComponentConstrainedRepoOpts = {}): Promise> => { const { limit, offset = 0, ...rest } = args; const clampedLimit = Math.min(limit, 100); - const res = await this.findPlays({limit: clampedLimit, offset, ...rest}, opts); + const res = await this.findPlays({limit: clampedLimit, offset, ...rest}, opts) as T[]; return {data: res, meta: {limit: clampedLimit, offset}}; } diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 3e0ea151..3b1112fa 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -571,8 +571,6 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i this.tracksScrobbled++; } - getScrobbledPlays = () => this.scrobbledPlayObjs.data.map(x => x.play) - findExistingSubmittedPlayObj = async (playObjPre: PlayObject): Promise<([undefined, undefined] | [ScrobbledPlayObject, ScrobbledPlayObject[]])> => { const playObj = await this.transformPlay(playObjPre, TRANSFORM_HOOK.candidate); @@ -1331,7 +1329,7 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i return this.playRepo.getQueued(queueName, {offset}); } - public getPlays = (args: QueryPlaysOpts) => { + public getPlaysPaginated = (args: QueryPlaysOpts) => { const { limit, offset, diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index e1c4d90a..8bbd8f1b 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -36,6 +36,7 @@ import ScrobbleClients from "../scrobblers/ScrobbleClients.js"; import prom from 'prom-client'; import { SimpleError } from "../common/errors/MSErrors.js"; import { QueryPlaysOpts } from "../common/database/drizzle/repositories/PlayRepository.js"; +import { playSelectToDeadScrobble } from "../common/database/drizzle/entityUtils.js"; const maxBufferSize = 300; const output: Record> = {}; @@ -329,7 +330,7 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro } = req; // @ts-ignore - const result: DeadLetterScrobble[] = (await (client as AbstractScrobbleClient).getPlays(query as Partial)).data.map(x => ({play: x.play, source: x.play.meta.source, retries: x.queueStates.find(x => x.queueName === CLIENT_DEAD_QUEUE)?.retries ?? 0 })); + const result: DeadLetterScrobble[] = (await (client as AbstractScrobbleClient).getPlaysPaginated(query as Partial)).data.map(x => playSelectToDeadScrobble); return res.json(result); }); @@ -373,7 +374,7 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro return res.status(200).send(); } - return res.json(dead); + return res.json(playSelectToDeadScrobble(dead)); }); app.delete('/api/dead', clientMiddleFunc(true), async (req, res, next) => { @@ -417,11 +418,16 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro const { // @ts-expect-error scrobbleClient not part of req scrobbleClient: client, + query } = req; let result: PlayObject[] = []; if (client !== undefined) { - result = [...(client as AbstractScrobbleClient).getScrobbledPlays()].sort(sortByNewestPlayDate); + const q: Partial = { + ...query as Partial, + state: ['scrobbled'] + } + result = [...(await (client as AbstractScrobbleClient).getPlaysPaginated(q)).data.map(x => x.play)].sort(sortByNewestPlayDate); } return res.json(result); diff --git a/src/backend/utils/ErrorUtils.ts b/src/backend/utils/ErrorUtils.ts index 2e7bee38..f4fbfa14 100644 --- a/src/backend/utils/ErrorUtils.ts +++ b/src/backend/utils/ErrorUtils.ts @@ -86,7 +86,7 @@ export const MessageTransformerDefault = (val: string) => val; /** * Adapted from https://github.com/voxpelli/pony-cause * */ -const _messageWithCauses = (err: Error, seen = new Set(), msgTransform: MessageTransformer = MessageTransformerDefault) => { +const _messageWithCauses = (err: Error, seen = new Set(), msgTransform: MessageTransformer = MessageTransformerDefault): string => { if (!(err instanceof Error)) return ''; const message = err.message; -- 2.51.2