mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(books): join metadata table when resolving query-based book ids
Query-based book selections failed with a 500 error when filtering by metadata fields because findIdsByWhere queried the bare books table without joining book_metadata. Left join book_metadata so WHERE clause predicates referencing metadata fields resolve correctly. Closes #732
This commit is contained in:
@@ -312,6 +312,82 @@ describe('Book API contract (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk query selections', () => {
|
||||
it('resolves metadata filters and search without regressing subquery or explicit selections', async () => {
|
||||
const metadataCollectionResponse = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/collections',
|
||||
headers: authHeader(limitedUser.accessToken),
|
||||
payload: { name: `Metadata selection ${randomUUID()}`, icon: 'FolderOpen' },
|
||||
});
|
||||
expect(metadataCollectionResponse.statusCode).toBe(201);
|
||||
const metadataCollection = metadataCollectionResponse.json() as { id: number };
|
||||
|
||||
const metadataAdd = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/v1/collections/${metadataCollection.id}/books`,
|
||||
headers: authHeader(limitedUser.accessToken),
|
||||
payload: {
|
||||
query: {
|
||||
libraryId: visibleLibrary.libraryId,
|
||||
filter: {
|
||||
type: 'group',
|
||||
join: 'AND',
|
||||
rules: [{ type: 'rule', field: 'title', operator: 'contains', value: 'Contract' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(metadataAdd.statusCode).toBe(201);
|
||||
expect(metadataAdd.json()).toMatchObject({ id: metadataCollection.id, bookCount: 3 });
|
||||
|
||||
const subqueryRemove = await ctx.app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/v1/collections/${metadataCollection.id}/books`,
|
||||
headers: authHeader(limitedUser.accessToken),
|
||||
payload: {
|
||||
query: {
|
||||
libraryId: visibleLibrary.libraryId,
|
||||
filter: {
|
||||
type: 'group',
|
||||
join: 'AND',
|
||||
rules: [{ type: 'rule', field: 'format', operator: 'includesAny', value: ['pdf'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(subqueryRemove.statusCode).toBe(200);
|
||||
expect(subqueryRemove.json()).toMatchObject({ id: metadataCollection.id, bookCount: 2 });
|
||||
|
||||
const explicitAdd = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/v1/collections/${metadataCollection.id}/books`,
|
||||
headers: authHeader(limitedUser.accessToken),
|
||||
payload: { bookIds: [visiblePdf.bookId] },
|
||||
});
|
||||
expect(explicitAdd.statusCode).toBe(201);
|
||||
expect(explicitAdd.json()).toMatchObject({ id: metadataCollection.id, bookCount: 3 });
|
||||
|
||||
const searchCollectionResponse = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/collections',
|
||||
headers: authHeader(limitedUser.accessToken),
|
||||
payload: { name: `Search selection ${randomUUID()}`, icon: 'FolderSearch' },
|
||||
});
|
||||
expect(searchCollectionResponse.statusCode).toBe(201);
|
||||
const searchCollection = searchCollectionResponse.json() as { id: number };
|
||||
|
||||
const scopedSearchAdd = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/v1/collections/${searchCollection.id}/books`,
|
||||
headers: authHeader(limitedUser.accessToken),
|
||||
payload: { query: { q: 'EPUB' } },
|
||||
});
|
||||
expect(scopedSearchAdd.statusCode).toBe(201);
|
||||
expect(scopedSearchAdd.json()).toMatchObject({ id: searchCollection.id, bookCount: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('detail and reader state', () => {
|
||||
it('returns detail payload fields and keeps progress and status isolated per user', async () => {
|
||||
const saveProgress = await ctx.app.inject({
|
||||
|
||||
Reference in New Issue
Block a user