fix: align Book Dock ready filter with filing readiness

Use the destination-complete predicate for the Ready chip and select-all operations so counts and filtered results remain consistent.

Fixes #1215
This commit is contained in:
neon
2026-08-30 17:21:35 +00:00
parent 9391555859
commit cc1b767264
22 changed files with 301 additions and 33 deletions
@@ -473,9 +473,10 @@ describe('BookDockFinalizeService', () => {
);
vi.spyOn(service as never, 'emitChange').mockReturnValue(undefined as never);
const result = await service.finalize(7, true, true, [], true, [], 5, 9, [], 'ready', 'foo');
const result = await service.finalize(7, true, true, [], true, [], 5, 9, [], undefined, 'foo', undefined, true);
expect(repo.findSelectionBatch).toHaveBeenCalledTimes(2);
expect(repo.findSelectionBatch).toHaveBeenCalledWith(expect.objectContaining({ readyToFile: true }));
expect(result.total).toBe(2);
expect(result.failed).toBe(0);
});
@@ -184,6 +184,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status?: string,
search?: string,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<BookDockFinalizeResult> {
const ids = selectAll ? [] : dedupeIds(fileIds ?? []);
const overrideMap = new Map((overrides ?? []).map((o) => [o.fileId, o]));
@@ -202,6 +203,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
});
@@ -338,6 +340,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status?: string,
search?: string,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<BookDockFinalizePreviewResult> {
const summary = createFinalizePreviewSummary();
const overrideMap = new Map((overrides ?? []).map((o) => [o.fileId, o]));
@@ -351,6 +354,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status,
search,
needsReview,
readyToFile,
async (rows, missingIds) => {
const prepared = await this.prepareFinalizeBatch(rows, defaultLibraryId, defaultFolderId, overrideMap, userId, isSuperuser);
for (const candidate of prepared.analyses) {
@@ -384,6 +388,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status?: string,
search?: string,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<BookDockDiscardDuplicatesResult> {
const startedAt = Date.now();
this.logger.log(`[book_dock.discard_duplicates] [start] userId=${userId} selectAll=${selectAll === true} - duplicate discard started`);
@@ -403,6 +408,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status,
search,
needsReview,
readyToFile,
async (rows, missingIds) => {
total += rows.length + missingIds.length;
const prepared = await this.prepareFinalizeBatch(rows, defaultLibraryId, defaultFolderId, overrideMap, userId, isSuperuser);
@@ -587,6 +593,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status: string | undefined,
search: string | undefined,
needsReview: boolean | undefined,
readyToFile: boolean | undefined,
processBatch: (rows: BookDockFileRow[], missingIds: number[]) => Promise<void>,
): Promise<void> {
if (selectAll) {
@@ -599,6 +606,7 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
});
@@ -721,8 +729,9 @@ export class BookDockFinalizeService implements OnModuleInit, OnApplicationBoots
status?: string,
search?: string,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<{ fileId: number; fileName: string; newName: string }[]> {
const ids = selectAll ? await this.repo.findAllIds(excludedIds, status, search, userId, canManageAll, needsReview) : (fileIds ?? []);
const ids = selectAll ? await this.repo.findAllIds(excludedIds, status, search, userId, canManageAll, needsReview, readyToFile) : (fileIds ?? []);
if (!ids.length) return [];
const rows = await this.repo.findByIds(ids, userId, canManageAll);
@@ -67,14 +67,16 @@ describe('BookDockController', () => {
vi.clearAllMocks();
});
it('listFiles applies defaults before delegating', async () => {
it('listFiles applies defaults and propagates the ready-to-file predicate', async () => {
const { controller, service } = makeController();
service.listFiles.mockResolvedValue({ items: [], total: 0, page: 1, size: 20 });
await controller.listFiles(MOCK_USER, {});
await controller.listFiles(MOCK_USER, { readyToFile: true });
expect(service.listFiles).toHaveBeenCalledWith({
status: undefined,
needsReview: undefined,
readyToFile: true,
page: 1,
limit: 20,
sort: 'createdAt',
@@ -143,9 +145,9 @@ describe('BookDockController', () => {
it('bulk and finalize endpoints delegate payload fields as expected', async () => {
const { controller, service, finalizeService, watcherService } = makeController();
await controller.bulkDiscard(MOCK_USER, { fileIds: [1], selectAll: false, excludedIds: [2], status: 'error', search: 'x' });
await controller.applyFetched(MOCK_USER, { fileIds: [1], selectAll: true, excludedIds: [2], status: 'ready', search: 'x' });
await controller.retryFetch(MOCK_USER, { fileIds: [3], selectAll: false, excludedIds: [4], status: 'error', search: 'y' });
await controller.bulkDiscard(MOCK_USER, { fileIds: [1], selectAll: false, excludedIds: [2], status: 'error', search: 'x', readyToFile: true });
await controller.applyFetched(MOCK_USER, { fileIds: [1], selectAll: true, excludedIds: [2], status: 'ready', search: 'x', readyToFile: true });
await controller.retryFetch(MOCK_USER, { fileIds: [3], selectAll: false, excludedIds: [4], status: 'error', search: 'y', readyToFile: true });
await controller.refetchMetadata(MOCK_USER, 11);
await controller.setTarget(MOCK_USER, {
fileIds: [5],
@@ -153,8 +155,9 @@ describe('BookDockController', () => {
excludedIds: [6],
targetLibraryId: undefined,
targetFolderId: undefined,
readyToFile: true,
});
await controller.selectionSummary(MOCK_USER, { fileIds: [7], selectAll: false, excludedIds: [8] });
await controller.selectionSummary(MOCK_USER, { fileIds: [7], selectAll: false, excludedIds: [8], readyToFile: true });
await controller.bulkEdit(MOCK_USER, {
fileIds: [9],
selectAll: false,
@@ -162,8 +165,9 @@ describe('BookDockController', () => {
fields: { title: 'Edited' },
enabledFields: ['title'],
mergeArrays: false,
readyToFile: true,
} as any);
await controller.previewNames(MOCK_USER, { fileIds: [10], selectAll: false, excludedIds: [], defaultLibraryId: 2 } as any);
await controller.previewNames(MOCK_USER, { fileIds: [10], selectAll: false, excludedIds: [], defaultLibraryId: 2, readyToFile: true } as any);
await controller.previewFinalize(MOCK_USER, {
fileIds: [10],
selectAll: false,
@@ -171,6 +175,7 @@ describe('BookDockController', () => {
defaultLibraryId: 2,
defaultFolderId: 3,
overrides: [],
readyToFile: true,
} as any);
await controller.discardFinalizeDuplicates(MOCK_USER, {
fileIds: [10],
@@ -179,19 +184,38 @@ describe('BookDockController', () => {
defaultLibraryId: 2,
defaultFolderId: 3,
overrides: [],
readyToFile: true,
} as any);
await controller.finalize(
{ id: 99, isSuperuser: true, permissions: [] } as any,
{ fileIds: [1], defaultLibraryId: 2, defaultFolderId: 3, selectAll: false, excludedIds: [], overrides: [] } as any,
{ fileIds: [1], defaultLibraryId: 2, defaultFolderId: 3, selectAll: false, excludedIds: [], overrides: [], readyToFile: true } as any,
);
await controller.rescan();
await controller.pause();
await controller.resume();
expect(service.bulkSetTarget).toHaveBeenCalledWith([5], false, [6], null, null, undefined, undefined, MOCK_USER.id, false, undefined);
expect(service.bulkDiscard).toHaveBeenCalledWith([1], false, [2], 'error', 'x', MOCK_USER.id, false, undefined, true);
expect(service.bulkApplyFetched).toHaveBeenCalledWith([1], true, [2], 'ready', 'x', MOCK_USER.id, false, undefined, true);
expect(service.bulkRetryFetch).toHaveBeenCalledWith([3], false, [4], 'error', 'y', MOCK_USER.id, false, undefined, true);
expect(service.bulkSetTarget).toHaveBeenCalledWith([5], false, [6], null, null, undefined, undefined, MOCK_USER.id, false, undefined, true);
expect(service.selectionSummary).toHaveBeenCalledWith([7], false, [8], undefined, undefined, MOCK_USER.id, false, undefined, true);
expect(service.bulkEdit).toHaveBeenCalledWith(
[9],
false,
[],
{ title: 'Edited' },
['title'],
false,
undefined,
undefined,
MOCK_USER.id,
false,
undefined,
true,
);
expect(service.refetchMetadata).toHaveBeenCalledWith(11, MOCK_USER.id, false);
expect(finalizeService.previewNames).toHaveBeenCalledWith([10], false, [], 2, MOCK_USER.id, false, undefined, undefined, undefined);
expect(finalizeService.previewFinalize).toHaveBeenCalledWith(1, false, false, [10], false, [], 2, 3, [], undefined, undefined, undefined);
expect(finalizeService.previewNames).toHaveBeenCalledWith([10], false, [], 2, MOCK_USER.id, false, undefined, undefined, undefined, true);
expect(finalizeService.previewFinalize).toHaveBeenCalledWith(1, false, false, [10], false, [], 2, 3, [], undefined, undefined, undefined, true);
expect(finalizeService.discardDuplicateCandidates).toHaveBeenCalledWith(
1,
false,
@@ -205,8 +229,9 @@ describe('BookDockController', () => {
undefined,
undefined,
undefined,
true,
);
expect(finalizeService.finalize).toHaveBeenCalledWith(99, true, true, [1], false, [], 2, 3, [], undefined, undefined, undefined);
expect(finalizeService.finalize).toHaveBeenCalledWith(99, true, true, [1], false, [], 2, 3, [], undefined, undefined, undefined, true);
expect(watcherService.rescan).toHaveBeenCalled();
expect(service.pauseProcessing).toHaveBeenCalledTimes(1);
expect(service.resumeProcessing).toHaveBeenCalledTimes(1);
@@ -66,6 +66,7 @@ export class BookDockController {
return this.service.listFiles({
status: query.status,
needsReview: query.needsReview,
readyToFile: query.readyToFile,
page: query.page ?? 1,
limit: query.limit ?? 20,
sort: query.sort ?? 'createdAt',
@@ -172,6 +173,7 @@ export class BookDockController {
user.id,
this.canManageAll(user),
dto.needsReview,
dto.readyToFile,
);
}
@@ -186,6 +188,7 @@ export class BookDockController {
user.id,
this.canManageAll(user),
dto.needsReview,
dto.readyToFile,
);
}
@@ -200,6 +203,7 @@ export class BookDockController {
user.id,
this.canManageAll(user),
dto.needsReview,
dto.readyToFile,
);
}
@@ -222,6 +226,7 @@ export class BookDockController {
user.id,
this.canManageAll(user),
dto.needsReview,
dto.readyToFile,
);
}
@@ -236,6 +241,7 @@ export class BookDockController {
user.id,
this.canManageAll(user),
dto.needsReview,
dto.readyToFile,
);
}
@@ -254,6 +260,7 @@ export class BookDockController {
user.id,
this.canManageAll(user),
dto.needsReview,
dto.readyToFile,
);
}
@@ -269,6 +276,7 @@ export class BookDockController {
dto.status,
dto.search,
dto.needsReview,
dto.readyToFile,
);
}
@@ -289,6 +297,7 @@ export class BookDockController {
dto.status,
dto.search,
dto.needsReview,
dto.readyToFile,
);
}
@@ -309,6 +318,7 @@ export class BookDockController {
dto.status,
dto.search,
dto.needsReview,
dto.readyToFile,
);
}
@@ -339,6 +349,7 @@ export class BookDockController {
dto.status,
dto.search,
dto.needsReview,
dto.readyToFile,
);
}
@@ -21,10 +21,12 @@ describe('BookDock DTO validation', () => {
sort: 'fileName',
order: 'asc',
search: 'dune',
readyToFile: '1',
});
expect(dto.page).toBe(2);
expect(dto.limit).toBe(30);
expect(dto.readyToFile).toBe(true);
expect((await validate(dto)).length).toBe(0);
expect((await errorsFor(ListBookDockFilesDto, { status: 'unknown' })).length).toBeGreaterThan(0);
expect((await errorsFor(ListBookDockFilesDto, { limit: 200 })).length).toBeGreaterThan(0);
@@ -115,6 +117,7 @@ describe('BookDock DTO validation', () => {
selectAll: true,
status: 'error',
search: 'abc',
readyToFile: true,
targetLibraryId: null,
targetFolderId: null,
})
@@ -78,6 +78,20 @@ function makeDb() {
};
}
function hasReadyToFileCondition(value: unknown): boolean {
if (!value || typeof value !== 'object') return false;
const node = value as { op?: string; clauses?: unknown[]; right?: unknown };
const clauses = node.clauses ?? [];
if (
node.op === 'and' &&
clauses.some((clause) => (clause as { op?: string; right?: unknown }).op === 'eq' && (clause as { right?: unknown }).right === 'ready') &&
clauses.filter((clause) => (clause as { op?: string }).op === 'isNotNull').length === 2
) {
return true;
}
return clauses.some(hasReadyToFileCondition);
}
describe('BookDockRepository', () => {
it('findExistingBooksByAbsolutePaths short-circuits an empty path list', async () => {
const { db } = makeDb();
@@ -173,6 +187,36 @@ describe('BookDockRepository', () => {
expect(selectBuilder.limit).toHaveBeenCalledWith(2);
});
it('uses the same ready-to-file predicate for paged results and select-all batches', async () => {
const paged = makeDb();
paged.selectBuilder.where.mockReturnValueOnce(paged.selectBuilder).mockResolvedValueOnce([{ total: 1 }]);
paged.selectBuilder.offset.mockResolvedValueOnce([{ id: 1 }]);
const pagedRepo = new BookDockRepository(paged.db as never);
await pagedRepo.findAll({
readyToFile: true,
page: 1,
limit: 20,
sort: 'createdAt',
order: 'desc',
userId: 1,
canManageAll: true,
});
expect(paged.selectBuilder.where.mock.calls).toHaveLength(2);
for (const [where] of paged.selectBuilder.where.mock.calls) {
expect(hasReadyToFileCondition(where)).toBe(true);
}
const batched = makeDb();
batched.selectBuilder.limit.mockResolvedValueOnce([{ id: 1 }]);
const batchedRepo = new BookDockRepository(batched.db as never);
await batchedRepo.findSelectionBatch({ limit: 20, readyToFile: true, userId: 1, canManageAll: true });
expect(hasReadyToFileCondition(batched.selectBuilder.where.mock.calls[0]?.[0])).toBe(true);
});
it('findAll returns paged rows and scalar total using status/search filters', async () => {
const { db, selectBuilder } = makeDb();
selectBuilder.where.mockReturnValueOnce(selectBuilder).mockResolvedValueOnce([{ total: 7 }]);
@@ -40,6 +40,7 @@ function needsReviewCondition(): SQL {
export interface ListOptions {
status?: string;
needsReview?: boolean;
readyToFile?: boolean;
page: number;
limit: number;
sort: string;
@@ -55,6 +56,7 @@ export interface SelectionBatchOptions {
excludedIds?: number[];
status?: string;
needsReview?: boolean;
readyToFile?: boolean;
search?: string;
userId: number;
canManageAll: boolean;
@@ -65,7 +67,7 @@ export class BookDockRepository {
constructor(@Inject(DB) private readonly db: Db) {}
async findAll(opts: ListOptions): Promise<{ items: BookDockFileRow[]; total: number }> {
const conditions = this.buildSelectionConditions(opts.status, opts.search, opts.userId, opts.canManageAll, opts.needsReview);
const conditions = this.buildSelectionConditions(opts.status, opts.search, opts.userId, opts.canManageAll, opts.needsReview, opts.readyToFile);
const where = conditions.length ? and(...conditions) : undefined;
@@ -143,8 +145,9 @@ export class BookDockRepository {
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<number[]> {
const conditions = this.buildSelectionConditions(status, search, userId, canManageAll ?? true, needsReview);
const conditions = this.buildSelectionConditions(status, search, userId, canManageAll ?? true, needsReview, readyToFile);
if (excludedIds?.length) conditions.push(notInArray(bookDockFiles.id, excludedIds));
const where = conditions.length ? and(...conditions) : undefined;
const rows = await this.db.select({ id: bookDockFiles.id }).from(bookDockFiles).where(where);
@@ -179,7 +182,14 @@ export class BookDockRepository {
}
async findSelectionBatch(options: SelectionBatchOptions): Promise<BookDockFileRow[]> {
const conditions = this.buildSelectionConditions(options.status, options.search, options.userId, options.canManageAll, options.needsReview);
const conditions = this.buildSelectionConditions(
options.status,
options.search,
options.userId,
options.canManageAll,
options.needsReview,
options.readyToFile,
);
if (options.excludedIds?.length) conditions.push(notInArray(bookDockFiles.id, options.excludedIds));
if (options.afterId !== undefined) conditions.push(gt(bookDockFiles.id, options.afterId));
const where = conditions.length ? and(...conditions) : undefined;
@@ -265,7 +275,14 @@ export class BookDockRepository {
return eq(bookDockFiles.uploadedBy, userId);
}
private buildSelectionConditions(status?: string, search?: string, userId?: number, canManageAll?: boolean, needsReview?: boolean): SQL[] {
private buildSelectionConditions(
status?: string,
search?: string,
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): SQL[] {
const conditions: SQL[] = [];
if (status === 'pending') {
conditions.push(inArray(bookDockFiles.status, ['pending', 'extracting', 'fetching']));
@@ -273,6 +290,7 @@ export class BookDockRepository {
conditions.push(eq(bookDockFiles.status, status));
}
if (needsReview) conditions.push(needsReviewCondition());
if (readyToFile) conditions.push(readyToFileCondition());
if (search) conditions.push(accentInsensitiveIlike(bookDockFiles.fileName, `%${search}%`));
if (userId !== undefined && !canManageAll) {
conditions.push(eq(bookDockFiles.uploadedBy, userId));
@@ -329,13 +329,14 @@ describe('BookDockService', () => {
.mockResolvedValueOnce([]);
await service.discardFile(1, 1, false);
await service.bulkDiscard([], true);
await service.bulkDiscard([], true, [], undefined, undefined, 1, true, undefined, true);
expect(vi.mocked(unlink)).toHaveBeenCalledWith('/bucket/book.epub');
expect(vi.mocked(unlink)).toHaveBeenCalledWith('/covers/1.png');
expect(vi.mocked(unlink)).toHaveBeenCalledWith('/covers/1_thumb.jpg');
expect(repo.deleteById).toHaveBeenCalledWith(1);
expect(repo.deleteByIds).toHaveBeenCalledWith([2]);
expect(repo.findSelectionBatch).toHaveBeenCalledWith(expect.objectContaining({ readyToFile: true }));
});
it('proxies summary and statistics repository queries', async () => {
@@ -93,6 +93,7 @@ export class BookDockService {
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<void> {
await this.processSelectionRows(
{
@@ -102,6 +103,7 @@ export class BookDockService {
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
},
@@ -126,6 +128,7 @@ export class BookDockService {
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<{ total: number; updated: number; failed: number }> {
let updated = 0;
let failed = 0;
@@ -137,6 +140,7 @@ export class BookDockService {
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
},
@@ -178,6 +182,7 @@ export class BookDockService {
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<{ total: number; applied: number; skipped: number; skippedEdited: number }> {
let applied = 0;
let skipped = 0;
@@ -190,6 +195,7 @@ export class BookDockService {
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
},
@@ -224,6 +230,7 @@ export class BookDockService {
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<{ total: number; queued: number }> {
let queued = 0;
const total = await this.processSelectionRows(
@@ -234,6 +241,7 @@ export class BookDockService {
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
},
@@ -266,6 +274,7 @@ export class BookDockService {
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<{ total: number; updated: number; failed: number }> {
await this.assertValidTarget(targetLibraryId, targetFolderId);
let updated = 0;
@@ -277,6 +286,7 @@ export class BookDockService {
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
},
@@ -301,6 +311,7 @@ export class BookDockService {
userId?: number,
canManageAll?: boolean,
needsReview?: boolean,
readyToFile?: boolean,
): Promise<{ total: number; withDestination: number; withoutDestination: number }> {
const destinationPairCounts = new Map<string, number>();
const folderIdSet = new Set<number>();
@@ -312,6 +323,7 @@ export class BookDockService {
status,
search,
needsReview,
readyToFile,
userId,
canManageAll,
},
@@ -479,6 +491,7 @@ export class BookDockService {
status?: string;
search?: string;
needsReview?: boolean;
readyToFile?: boolean;
userId?: number;
canManageAll?: boolean;
},
@@ -498,6 +511,7 @@ export class BookDockService {
status: options.status,
search: options.search,
needsReview: options.needsReview,
readyToFile: options.readyToFile,
userId,
canManageAll,
});
+32
View File
@@ -191,6 +191,10 @@ export class FinalizeBookDockDto {
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
@IsOptional()
@ValidateIf((_, value) => value !== undefined)
@IsInt()
@@ -234,6 +238,10 @@ export class BulkDiscardDto {
@IsOptional()
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
}
export class PreviewNamesDto {
@@ -263,6 +271,10 @@ export class PreviewNamesDto {
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
@IsOptional()
@ValidateIf((_, value) => value !== undefined)
@IsInt()
@@ -295,6 +307,10 @@ export class BulkRetryFetchDto {
@IsOptional()
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
}
export class BulkApplyFetchedDto {
@@ -323,6 +339,10 @@ export class BulkApplyFetchedDto {
@IsOptional()
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
}
export class BulkSetTargetDto {
@@ -352,6 +372,10 @@ export class BulkSetTargetDto {
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
@IsOptional()
@ValidateIf((_, value) => value !== null)
@IsInt()
@@ -389,6 +413,10 @@ export class SelectionSummaryDto {
@IsOptional()
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
}
export class BulkEditBookDockDto {
@@ -418,6 +446,10 @@ export class BulkEditBookDockDto {
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@IsBoolean()
readyToFile?: boolean;
@ValidateNested()
@IsObject()
@Type(() => BookDockMetadataFieldsDto)
@@ -15,6 +15,11 @@ export class ListBookDockFilesDto {
@IsBoolean()
needsReview?: boolean;
@IsOptional()
@Transform(({ value }) => value === true || value === 'true' || value === '1')
@IsBoolean()
readyToFile?: boolean;
@IsOptional()
@Transform(({ value }) => parseInt(value as string, 10))
@IsInt()