Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions apps/backend/controllers/library.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type AlbumQueryParams = {
code_letters?: string;
code_artist_number?: string;
code_number?: number;
genre_name?: string;
n?: number;
page?: number;
};
Expand All @@ -103,9 +104,19 @@ export const searchForAlbum: RequestHandler = async (
'Missing query parameter. Query must include: artist_name, album_title, or code_letters, code_artist_number, and code_number'
);
} else if (query.code_letters !== undefined && query.code_artist_number !== undefined) {
//quickly look up albums by that artist
res.status(501);
res.send('TODO: Library Code Lookup');
try {
const response = await libraryService.lookupByLibraryCode(
query.code_letters,
parseInt(query.code_artist_number),
query.code_number !== undefined ? Number(query.code_number) : undefined,
query.genre_name
);
res.status(200).json(response);
} catch (e) {
console.error('Error: Library code lookup failed');
console.error(e);
next(e);
}
} else {
try {
const response = await libraryService.fuzzySearchLibrary(query.artist_name, query.album_title, query.n);
Expand Down Expand Up @@ -283,3 +294,29 @@ export const getAlbum: RequestHandler<object, unknown, unknown, { album_id: stri
}
}
};

type UpdateAlbumBody = {
album_id?: number;
label?: string;
album_title?: string;
};

export const updateAlbum: RequestHandler = async (req: Request<object, object, UpdateAlbumBody>, res, next) => {
const { body } = req;
if (body.album_id === undefined) {
res.status(400).send('Bad Request, Missing Parameter: album_id');
} else {
try {
const fields: { label?: string; album_title?: string } = {};
if (body.label !== undefined) fields.label = body.label;
if (body.album_title !== undefined) fields.album_title = body.album_title;

const updated = await libraryService.updateAlbumFields(body.album_id, fields);
res.status(200).json(updated);
} catch (e) {
console.error('Error updating album');
console.error(e);
next(e);
}
}
};
44 changes: 44 additions & 0 deletions apps/backend/controllers/review.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Request, RequestHandler } from 'express';
import * as reviewService from '../services/review.service.js';

export const getReview: RequestHandler = async (
req: Request<object, object, object, { album_id?: string }>,
res,
next
) => {
const { query } = req;
if (query.album_id === undefined) {
res.status(400).send('Bad Request, Missing Parameter: album_id');
} else {
try {
const review = await reviewService.getReviewByAlbumId(parseInt(query.album_id));
res.status(200).json(review ?? null);
} catch (e) {
console.error('Error retrieving review');
console.error(e);
next(e);
}
}
};

type UpsertReviewBody = {
album_id?: number;
review?: string;
author?: string;
};

export const upsertReview: RequestHandler = async (req: Request<object, object, UpsertReviewBody>, res, next) => {
const { body } = req;
if (body.album_id === undefined || body.review === undefined) {
res.status(400).send('Bad Request, Missing Parameters: album_id and review are required');
} else {
try {
const result = await reviewService.upsertReview(body.album_id, body.review, body.author);
res.status(200).json(result);
} catch (e) {
console.error('Error upserting review');
console.error(e);
next(e);
}
}
};
7 changes: 7 additions & 0 deletions apps/backend/routes/library.route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { requirePermissions } from '@wxyc/authentication';
import { Router } from 'express';
import * as libraryController from '../controllers/library.controller.js';
import * as reviewController from '../controllers/review.controller.js';
import * as requestLineController from '../controllers/requestLine.controller.js';
import { requireAnonymousAuth } from '../middleware/anonymousAuth.js';

Expand Down Expand Up @@ -31,3 +32,9 @@ library_route.get('/genres', requirePermissions({ catalog: ['read'] }), libraryC
library_route.post('/genres', requirePermissions({ catalog: ['write'] }), libraryController.addGenre);

library_route.get('/info', requirePermissions({ catalog: ['read'] }), libraryController.getAlbum);

library_route.get('/reviews', requirePermissions({ catalog: ['read'] }), reviewController.getReview);

library_route.put('/reviews', requirePermissions({ catalog: ['write'] }), reviewController.upsertReview);

library_route.patch('/', requirePermissions({ catalog: ['write'] }), libraryController.updateAlbum);
41 changes: 40 additions & 1 deletion apps/backend/services/library.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { desc, eq, sql } from 'drizzle-orm';
import { and, desc, eq, sql } from 'drizzle-orm';
import { RotationAddRequest } from '../controllers/library.controller.js';
import { db } from '@wxyc/database';
import {
Expand Down Expand Up @@ -224,6 +224,45 @@ export const isISODate = (date: string): boolean => {
return date.match(regex) !== null;
};

export const lookupByLibraryCode = async (
code_letters: string,
code_artist_number: number,
code_number?: number,
genre_name?: string
): Promise<LibraryArtistViewEntry[]> => {
const conditions = [
eq(library_artist_view.code_letters, code_letters),
eq(library_artist_view.code_artist_number, code_artist_number),
];

if (code_number !== undefined) {
conditions.push(eq(library_artist_view.code_number, code_number));
}

if (genre_name !== undefined) {
conditions.push(eq(library_artist_view.genre_name, genre_name));
}

const result = await db
.select()
.from(library_artist_view)
.where(and(...conditions));

return result as LibraryArtistViewEntry[];
};

export const updateAlbumFields = async (albumId: number, fields: { label?: string; album_title?: string }) => {
const result = await db
.update(library)
.set({
...fields,
last_modified: sql`now()`,
})
.where(eq(library.id, albumId))
.returning();
return result[0];
};

// =============================================================================
// Request Line Enhanced Search Functions
// =============================================================================
Expand Down
28 changes: 28 additions & 0 deletions apps/backend/services/review.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { eq, sql } from 'drizzle-orm';
import { db } from '@wxyc/database';
import { reviews } from '@wxyc/database';

export const getReviewByAlbumId = async (albumId: number) => {
const result = await db.select().from(reviews).where(eq(reviews.album_id, albumId)).limit(1);
return result[0];
};

export const upsertReview = async (albumId: number, review: string, author?: string) => {
const result = await db
.insert(reviews)
.values({
album_id: albumId,
review,
author: author ?? null,
})
.onConflictDoUpdate({
target: reviews.album_id,
set: {
review,
author: author ?? null,
last_modified: sql`now()`,
},
})
.returning();
return result[0];
};
34 changes: 31 additions & 3 deletions tests/integration/library.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,38 @@ describe('Library Catalog', () => {
expect(res.body.length).toBe(0);
});

test('code lookup returns 501 (not implemented)', async () => {
const res = await auth.get('/library').query({ code_letters: 'BUI', code_artist_number: '1' }).expect(501);
test('code lookup returns albums matching library code', async () => {
const res = await auth.get('/library').query({ code_letters: 'BU', code_artist_number: '60' }).expect(200);

expectErrorContains(res, 'TODO');
expectArray(res);
expect(res.body.length).toBeGreaterThan(0);
res.body.forEach((album) => {
expectFields(album, 'id', 'code_letters', 'code_artist_number', 'code_number', 'artist_name', 'album_title');
expect(album.code_letters).toBe('BU');
expect(album.code_artist_number).toBe(60);
});
});

test('code lookup with code_number returns specific album', async () => {
const res = await auth
.get('/library')
.query({ code_letters: 'BU', code_artist_number: '60', code_number: 1 })
.expect(200);

expectArray(res);
expect(res.body.length).toBeLessThanOrEqual(1);
if (res.body.length > 0) {
expect(res.body[0].code_letters).toBe('BU');
expect(res.body[0].code_artist_number).toBe(60);
expect(res.body[0].code_number).toBe(1);
}
});

test('code lookup returns empty array for non-existent code', async () => {
const res = await auth.get('/library').query({ code_letters: 'ZZZ', code_artist_number: '999' }).expect(200);

expectArray(res);
expect(res.body.length).toBe(0);
});
});

Expand Down
3 changes: 3 additions & 0 deletions tests/mocks/database.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export const genres = {};
export const format = {};
export const rotation = {};
export const library_artist_view = {};
export const reviews = {
album_id: 'album_id',
};
export const flowsheet = {
id: 'id',
show_id: 'show_id',
Expand Down
Loading
Loading