feat(metadata): add full publication date support#598
Conversation
- Add a persisted publication date while continuing to derive publication years. - Thread full dates through metadata import, editing, filters, sorting, write-back, and sync payloads. - Cover provider parsing, DTO validation, jump buckets, and date formatting with tests. Closes bookorbit#596
📝 WalkthroughWalkthroughThis PR adds full published-date support ( ChangesPublished Date Feature
Estimated code review effort: 4 (Complex) | ~75 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant User
participant EditMetadataTab
participant useMetadataDiff
participant BookService
User->>EditMetadataTab: enters Published Date
EditMetadataTab->>EditMetadataTab: setPublishedDateField(derives publishedYear)
EditMetadataTab->>useMetadataDiff: buildPatch()
useMetadataDiff-->>EditMetadataTab: formPatch.publishedDate
EditMetadataTab->>BookService: PATCH publishedDate/publishedYear
BookService->>BookService: normalize & derive publishedYear from date
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
server/src/modules/book/book.service.ts (1)
3063-3081: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMirror the MOBI year fallback here
parseMobiFile()returns the raw EXTH date string, so year-only values are dropped in this branch becausepublishedYearis only derived afternormalizePublishedDate(...)succeeds. Fall back toparsePublishedYear(parsed.publishedDate)here, as the dedicated MOBI extractor already does.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/book/book.service.ts` around lines 3063 - 3081, The MOBI/AZW3/AZW metadata branch in book.service.ts is losing year-only EXTH dates because publishedYear is only set from normalizePublishedDate(...) when that parsing succeeds. Update the case handling around parseMobiFile and the publishedDate/publishedYear assignment to mirror the dedicated MOBI extractor by falling back to parsePublishedYear(parsed.publishedDate) when normalizePublishedDate(...) returns nothing.client/src/features/book-dock/components/BookDockFileSheet.vue (1)
315-338: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
openFetchedDiffis missingpublishedDatefrom fetched metadata.The constructed
MetadataCandidateincludespublishedYearbut omitspublishedDate. SincefetchedMetadata(f) is aBookDockMetadatathat now carriespublishedDate, the diff view against fetched metadata won't display the full publication date.🐛 Proposed fix
selectedCandidate.value = { provider: 'auto' as MetadataProviderKey, providerId: '', title: f.title ?? '', subtitle: f.subtitle, authors: f.authors, description: f.description, publisher: f.publisher, + publishedDate: f.publishedDate, publishedYear: f.publishedYear, language: f.language, pageCount: f.pageCount, isbn10: f.isbn10, isbn13: f.isbn13, seriesName: f.seriesName, seriesIndex: f.seriesIndex, genres: f.genres, coverUrl: f.coverUrl, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/features/book-dock/components/BookDockFileSheet.vue` around lines 315 - 338, openFetchedDiff is not passing through publishedDate from fetched metadata, so the diff view loses the full publication date. Update the MetadataCandidate construction inside openFetchedDiff in BookDockFileSheet to copy f.publishedDate alongside the existing publication fields, keeping the fetchedMetadata-to-selectedCandidate mapping complete.
🧹 Nitpick comments (7)
server/src/modules/book-dock/dto/index.ts (1)
60-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding a semantic date validation message to
@Matches.The regex
^\d{4}-\d{2}-\d{2}$validates format but not semantic validity (e.g.,2023-13-45passes). While downstream normalization likely catches invalid dates, adding a validation message improves error clarity for API consumers.💡 Suggested improvement
- `@IsOptional`() `@IsString`() `@Matches`(/^\d{4}-\d{2}-\d{2}$/) publishedDate?: string; + `@IsOptional`() `@IsString`() `@Matches`(/^\d{4}-\d{2}-\d{2}$/, { message: 'publishedDate must be a valid YYYY-MM-DD date string' }) publishedDate?: string;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/book-dock/dto/index.ts` at line 60, The publishedDate validation in the DTO only checks the YYYY-MM-DD shape via `@Matches`, so add a custom validation message there to make invalid date input clearer for API consumers. Update the publishedDate field in the book-dock DTO so the `@Matches` decorator includes a semantic-friendly message, and keep the existing `@IsOptional` and `@IsString` checks intact. Use the publishedDate property as the target for the change so it is easy to locate even if lines shift.client/src/features/book/lib/published-date.ts (1)
1-9: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueRegex doesn't validate month/day value ranges.
The
DATE_KEY_REregex only checks the digit format (\d{4}-\d{2}-\d{2}), not valid ranges. Invalid dates like"2007-13-45"would match and produce rollover dates via theDateconstructor (e.g., Feb 14, 2008). Since data should be validated upstream, this is low risk, but a brief comment noting the assumption would help future maintainers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/features/book/lib/published-date.ts` around lines 1 - 9, Add a brief inline comment near DATE_KEY_RE or in formatPublishedDate explaining that the regex only enforces the YYYY-MM-DD shape and intentionally assumes upstream validation for month/day ranges. Keep the existing parsing logic in formatPublishedDate unchanged, but document the assumption so future maintainers understand why invalid values like 2007-13-45 are not rejected here.server/src/modules/book/dto/update-book-metadata.dto.ts (1)
49-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding a validation error message to
@Matches.The regex
^\d{4}-\d{2}-\d{2}$validates the format but not semantic validity (e.g.,2020-13-45would pass). Adding amessageto the@Matchesdecorator would improve API error clarity for clients sending invalid date strings.✨ Suggested improvement
- `@IsOptional`() `@IsString`() `@Matches`(/^\d{4}-\d{2}-\d{2}$/) publishedDate?: string | null; + `@IsOptional`() `@IsString`() `@Matches`(/^\d{4}-\d{2}-\d{2}$/, { message: 'publishedDate must be in YYYY-MM-DD format' }) publishedDate?: string | null;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/book/dto/update-book-metadata.dto.ts` at line 49, The `publishedDate` validator in `UpdateBookMetadataDto` uses `@Matches` without an explicit message, so add a clear validation `message` there to improve API feedback for invalid date strings. Update the `publishedDate` decorator chain in `UpdateBookMetadataDto` so the `@Matches(/^\d{4}-\d{2}-\d{2}$/)` rule reports a user-friendly error when the value is not in the expected format.server/src/common/utils/published-date.utils.test.ts (1)
40-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for year-only input to
normalizePublishedDate.No test pins down what
normalizePublishedDate('1965')returns. This matters becausebook.service.ts's MOBI/AZW branch relies on this function to decide whether to derivepublishedYear, and if it returns null/undefined for year-only strings, the year is silently dropped (see companion comment inbook.service.ts).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/common/utils/published-date.utils.test.ts` around lines 40 - 46, Add a test in published-date.utils.test for normalizePublishedDate with a year-only string like '1965' so the expected return value is explicitly pinned down. Update the normalizePublishedDate behavior if needed so year-only input is handled consistently, and make sure the new assertion covers the path used by book.service.ts in the MOBI/AZW branch when it reads publishedYear from normalizePublishedDate.server/src/modules/metadata/lib/fb2-parser.ts (1)
70-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract shared candidate-extraction helper to eliminate duplication.
parseFb2DateNode(lines 70–87) andparseFb2YearNode(lines 49–68) share identical candidate-extraction logic — buildingcandidatesfrom direct text and@_valueattributes. Both are called on the sameyearRawvalue (lines 158–159), so the extraction runs twice.♻️ Proposed refactor
+function extractFb2DateCandidates(val: unknown): string[] { + const candidates: string[] = []; + const direct = text(val); + if (direct) candidates.push(direct); + if (typeof val === 'object' && val !== null) { + const objectNode = val as Record<string, unknown>; + const valueAttr = text(objectNode['`@_value`']); + if (valueAttr) candidates.push(valueAttr); + } + return candidates; +} + function parseFb2YearNode(val: unknown): number | null { - const candidates: string[] = []; - const direct = text(val); - if (direct) candidates.push(direct); - - if (typeof val === 'object' && val !== null) { - const objectNode = val as Record<string, unknown>; - const valueAttr = text(objectNode['`@_value`']); - if (valueAttr) candidates.push(valueAttr); - } - + const candidates = extractFb2DateCandidates(val); for (const candidate of candidates) { const match = candidate.match(/(\d{4})/); if (!match) continue; const year = parseInt(match[1], 10); if (!isNaN(year) && year > 1000 && year < 2200) return year; } return null; } function parseFb2DateNode(val: unknown): string | null { - const candidates: string[] = []; - const direct = text(val); - if (direct) candidates.push(direct); - - if (typeof val === 'object' && val !== null) { - const objectNode = val as Record<string, unknown>; - const valueAttr = text(objectNode['`@_value`']); - if (valueAttr) candidates.push(valueAttr); - } - + const candidates = extractFb2DateCandidates(val); for (const candidate of candidates) { const publishedDate = parsePublishedDateKey(candidate); if (publishedDate) return publishedDate; } return null; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/metadata/lib/fb2-parser.ts` around lines 70 - 87, Extract the duplicated candidate-building logic shared by parseFb2DateNode and parseFb2YearNode into a single helper that gathers direct text and the `@_value` attribute from the same input. Update both parseFb2DateNode and parseFb2YearNode to call the shared helper, and reuse it at the yearRaw handling site so the same value is not processed twice.server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.ts (1)
123-124: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCache
resolvePublishedDateto avoid duplicate resolution.
resolvePublishedDateis called once directly on line 123 and again insideresolvePublishedYearon line 124. For the samebook/releasepair, this duplicates the parsing work.♻️ Proposed refactor
return { provider: MetadataProviderKey.RANOBEDB, providerId: String(book.id), title: resolveTitle(book), subtitle: resolveSubtitle(book), authors: resolveAuthors(book), publisher: resolvePublisher(book), - publishedDate: resolvePublishedDate(book, selectedRelease), - publishedYear: resolvePublishedYear(book, selectedRelease), + publishedDate: (publishedDate => publishedDate ? publishedYearFromDateKey(publishedDate) : undefined)(resolvePublishedDate(book, selectedRelease)),Or more readably, extract a local:
const selectedRelease = selectEnglishRelease(book.releases); const communityRating = normalizeCommunityRating(book); + const publishedDate = resolvePublishedDate(book, selectedRelease); return { provider: MetadataProviderKey.RANOBEDB, providerId: String(book.id), title: resolveTitle(book), subtitle: resolveSubtitle(book), authors: resolveAuthors(book), publisher: resolvePublisher(book), - publishedDate: resolvePublishedDate(book, selectedRelease), - publishedYear: resolvePublishedYear(book, selectedRelease), + publishedDate, + publishedYear: publishedDate ? publishedYearFromDateKey(publishedDate) : undefined,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.ts` around lines 123 - 124, Cache the published date resolution in the ranobedb mapper to avoid doing the same work twice for the same book/release pair. In the mapper where `resolvePublishedDate` and `resolvePublishedYear` are used, compute the date once into a local value and pass that value through so `resolvePublishedYear` doesn’t re-run the same parsing logic. Use the existing `resolvePublishedDate`, `resolvePublishedYear`, and mapper code around `publishedDate`/`publishedYear` to place the shared result.server/src/modules/metadata/metadata.service.ts (1)
609-619: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract duplicated published date/year handling into a shared helper.
The published date/year scalar assignment logic at lines 609-619 (
persistAudioMetadata) and lines 690-700 (persistBookMetadata) is identical. Extracting it into a private helper would eliminate the duplication and ensure both paths stay in sync — particularly important given the lock-sensitivity of this logic.♻️ Proposed helper extraction
+ private applyPublishedDateFields( + scalarFields: Partial<typeof schema.bookMetadata.$inferInsert>, + filteredPublishedDate: string | null | undefined, + filteredPublishedYear: number | null | undefined, + ): void { + const publishedDate = normalizePublishedDate(filteredPublishedDate); + if (publishedDate !== undefined) { + scalarFields.publishedDate = publishedDate; + if (publishedDate !== null) scalarFields.publishedYear = publishedYearFromDateKey(publishedDate); + } + if (filteredPublishedYear !== undefined && publishedDate === undefined) { + scalarFields.publishedYear = normalizePublishedYear(filteredPublishedYear); + } else if (filteredPublishedYear !== undefined && publishedDate === null) { + scalarFields.publishedYear = normalizePublishedYear(filteredPublishedYear); + } + } + // In persistAudioMetadata, replace lines 609-619 with: - const publishedDate = normalizePublishedDate(filtered.publishedDate); - if (publishedDate !== undefined) { - scalarFields.publishedDate = publishedDate; - if (publishedDate !== null) scalarFields.publishedYear = publishedYearFromDateKey(publishedDate); - } - if (filtered.publishedYear !== undefined && publishedDate === undefined) { - scalarFields.publishedDate = null; - scalarFields.publishedYear = normalizePublishedYear(filtered.publishedYear); - } else if (filtered.publishedYear !== undefined && publishedDate === null) { - scalarFields.publishedYear = normalizePublishedYear(filtered.publishedYear); - } + this.applyPublishedDateFields(scalarFields, filtered.publishedDate, filtered.publishedYear);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/metadata/metadata.service.ts` around lines 609 - 619, The published date/year assignment logic in persistAudioMetadata and persistBookMetadata is duplicated and should be moved into a shared private helper. Extract the identical scalar-field handling into a helper with a clear name, then call it from both metadata persistence paths so the publishedDate/publishedYear behavior stays consistent and lock-sensitive logic remains in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/src/features/book/components/detail/tabs/EditMetadataTab.vue`:
- Around line 411-420: `applyPublishedPatch` currently updates `publishedDate`
without syncing `publishedYear` when only a date is provided. Update this helper
so that when `formPatch.publishedDate` exists but `publishedYear` is absent, it
derives and assigns `publishedYear` from the date before returning. Keep the
existing lock check for `publishedYear` and preserve explicit `publishedYear`
values when they are present.
In `@server/src/common/utils/published-date.utils.ts`:
- Around line 61-63: The published-date conversion has a timezone shift in the
natural-language and Date-object paths, which can produce the previous day after
toISOString is applied. Update published-date handling in
published-date.utils.ts so the MONTH_NAME_RE branch does not rely on new
Date(trimmed) plus UTC serialization; instead parse the month-name string as a
UTC date explicitly, and keep the value instanceof Date path using the
appropriate date components so dateToPublishedDateKey returns the intended
calendar day. Ensure the fix is applied in the published date parsing flow
around parsePublishedDateFromEpochMillis/dateToPublishedDateKey and the
MONTH_NAME_RE logic.
In `@server/src/db/migrations/0044_add_published_date.sql`:
- Around line 1-4: The migration for book_metadata is using blocking DDL on the
default transactional path. Update 0044_add_published_date.sql so the index
creation in bm_published_date_idx and bm_published_date_sort_idx uses an online
approach like CREATE INDEX CONCURRENTLY, and make the
book_metadata_published_date_range_chk constraint a NOT VALID check followed by
a separate VALIDATE step; if that is not possible in this migration flow, move
the work to an off-peak online migration.
In `@server/src/modules/file-write/formats/cbx/comic-info-builder.ts`:
- Around line 112-131: `setComicInfoPublicationDate` is using a broad
`shouldWrite` check and then writing whichever date data is available, which can
emit `Year`, `Month`, or `Day` even when only one publication field was
selected. Update the branching so `publishedDate` writes only when `fieldMask`
contains `publishedDate`, and `publishedYear` writes only when `fieldMask`
contains `publishedYear`; if the selected field is absent, remove the
corresponding ComicInfo keys instead of falling through. Keep the logic
localized in `setComicInfoPublicationDate` and align it with the field-mask
behavior used by `setComicInfoField`.
---
Outside diff comments:
In `@client/src/features/book-dock/components/BookDockFileSheet.vue`:
- Around line 315-338: openFetchedDiff is not passing through publishedDate from
fetched metadata, so the diff view loses the full publication date. Update the
MetadataCandidate construction inside openFetchedDiff in BookDockFileSheet to
copy f.publishedDate alongside the existing publication fields, keeping the
fetchedMetadata-to-selectedCandidate mapping complete.
In `@server/src/modules/book/book.service.ts`:
- Around line 3063-3081: The MOBI/AZW3/AZW metadata branch in book.service.ts is
losing year-only EXTH dates because publishedYear is only set from
normalizePublishedDate(...) when that parsing succeeds. Update the case handling
around parseMobiFile and the publishedDate/publishedYear assignment to mirror
the dedicated MOBI extractor by falling back to
parsePublishedYear(parsed.publishedDate) when normalizePublishedDate(...)
returns nothing.
---
Nitpick comments:
In `@client/src/features/book/lib/published-date.ts`:
- Around line 1-9: Add a brief inline comment near DATE_KEY_RE or in
formatPublishedDate explaining that the regex only enforces the YYYY-MM-DD shape
and intentionally assumes upstream validation for month/day ranges. Keep the
existing parsing logic in formatPublishedDate unchanged, but document the
assumption so future maintainers understand why invalid values like 2007-13-45
are not rejected here.
In `@server/src/common/utils/published-date.utils.test.ts`:
- Around line 40-46: Add a test in published-date.utils.test for
normalizePublishedDate with a year-only string like '1965' so the expected
return value is explicitly pinned down. Update the normalizePublishedDate
behavior if needed so year-only input is handled consistently, and make sure the
new assertion covers the path used by book.service.ts in the MOBI/AZW branch
when it reads publishedYear from normalizePublishedDate.
In `@server/src/modules/book-dock/dto/index.ts`:
- Line 60: The publishedDate validation in the DTO only checks the YYYY-MM-DD
shape via `@Matches`, so add a custom validation message there to make invalid
date input clearer for API consumers. Update the publishedDate field in the
book-dock DTO so the `@Matches` decorator includes a semantic-friendly message,
and keep the existing `@IsOptional` and `@IsString` checks intact. Use the
publishedDate property as the target for the change so it is easy to locate even
if lines shift.
In `@server/src/modules/book/dto/update-book-metadata.dto.ts`:
- Line 49: The `publishedDate` validator in `UpdateBookMetadataDto` uses
`@Matches` without an explicit message, so add a clear validation `message`
there to improve API feedback for invalid date strings. Update the
`publishedDate` decorator chain in `UpdateBookMetadataDto` so the
`@Matches(/^\d{4}-\d{2}-\d{2}$/)` rule reports a user-friendly error when the
value is not in the expected format.
In `@server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.ts`:
- Around line 123-124: Cache the published date resolution in the ranobedb
mapper to avoid doing the same work twice for the same book/release pair. In the
mapper where `resolvePublishedDate` and `resolvePublishedYear` are used, compute
the date once into a local value and pass that value through so
`resolvePublishedYear` doesn’t re-run the same parsing logic. Use the existing
`resolvePublishedDate`, `resolvePublishedYear`, and mapper code around
`publishedDate`/`publishedYear` to place the shared result.
In `@server/src/modules/metadata/lib/fb2-parser.ts`:
- Around line 70-87: Extract the duplicated candidate-building logic shared by
parseFb2DateNode and parseFb2YearNode into a single helper that gathers direct
text and the `@_value` attribute from the same input. Update both parseFb2DateNode
and parseFb2YearNode to call the shared helper, and reuse it at the yearRaw
handling site so the same value is not processed twice.
In `@server/src/modules/metadata/metadata.service.ts`:
- Around line 609-619: The published date/year assignment logic in
persistAudioMetadata and persistBookMetadata is duplicated and should be moved
into a shared private helper. Extract the identical scalar-field handling into a
helper with a clear name, then call it from both metadata persistence paths so
the publishedDate/publishedYear behavior stays consistent and lock-sensitive
logic remains in one place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e2b5cf81-6c2e-458c-b316-8ea682af81ae
📒 Files selected for processing (138)
client/src/components/__tests__/AppHeader.spec.tsclient/src/features/book-dock/components/BookDockFileSheet.vueclient/src/features/book/components/BookCoverCard.test.tsclient/src/features/book/components/BookFilterBuilder.vueclient/src/features/book/components/BookListRow.vueclient/src/features/book/components/BookQuickView.vueclient/src/features/book/components/VirtualBookGrid.spec.tsclient/src/features/book/components/__tests__/BookCoverCard.spec.tsclient/src/features/book/components/__tests__/BookListRow.spec.tsclient/src/features/book/components/__tests__/BookQuickView.spec.tsclient/src/features/book/components/__tests__/BookTableCells.spec.tsclient/src/features/book/components/__tests__/CollapsedSeriesCard.spec.tsclient/src/features/book/components/__tests__/DetailsTab.spec.tsclient/src/features/book/components/detail/tabs/DetailsTab.vueclient/src/features/book/components/detail/tabs/EditMetadataTab.vueclient/src/features/book/components/detail/tabs/MetadataDiffPanel.vueclient/src/features/book/components/detail/tabs/MetadataResultCard.vueclient/src/features/book/components/detail/tabs/MetadataSearchDrawer.vueclient/src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.tsclient/src/features/book/components/detail/tabs/__tests__/ReadingLogHero.spec.tsclient/src/features/book/components/detail/tabs/__tests__/ReadingLogTab.spec.tsclient/src/features/book/components/table/BookTableDateCell.vueclient/src/features/book/components/table/__tests__/BookTableCellDispatcher.spec.tsclient/src/features/book/components/table/__tests__/BookTableCollapsedSeriesCell.spec.tsclient/src/features/book/composables/__tests__/tableColumnSchema.spec.tsclient/src/features/book/composables/__tests__/useBookBulkActions.spec.tsclient/src/features/book/composables/__tests__/useBookNavigation.spec.tsclient/src/features/book/composables/__tests__/useBookTableShell.spec.tsclient/src/features/book/composables/__tests__/useBookViewContext.spec.tsclient/src/features/book/composables/__tests__/useBookViewWindow.spec.tsclient/src/features/book/composables/__tests__/useBulkEditMetadata.test.tsclient/src/features/book/composables/__tests__/useMetadataDiff.spec.tsclient/src/features/book/composables/__tests__/useMetadataEditor.spec.tsclient/src/features/book/composables/__tests__/useMetadataLocks.spec.tsclient/src/features/book/composables/__tests__/usePersonalNote.spec.tsclient/src/features/book/composables/__tests__/useTableCellEditor.spec.tsclient/src/features/book/composables/__tests__/useTableCellHelpers.spec.tsclient/src/features/book/composables/__tests__/useTableCoverDialog.spec.tsclient/src/features/book/composables/tableColumnSchema.tsclient/src/features/book/composables/useBookBulkActions.tsclient/src/features/book/composables/useBulkEditMetadata.tsclient/src/features/book/composables/useFileMetadata.tsclient/src/features/book/composables/useGlobalSearch.test.tsclient/src/features/book/composables/useMetadataDiff.tsclient/src/features/book/composables/useMetadataEditor.tsclient/src/features/book/composables/useTableCellEditor.tsclient/src/features/book/composables/useTablePresets.tsclient/src/features/book/composables/useTableQuickFilters.tsclient/src/features/book/lib/__tests__/metadata-refresh-feedback.spec.tsclient/src/features/book/lib/book-card-mapper.test.tsclient/src/features/book/lib/book-card-mapper.tsclient/src/features/book/lib/file-metadata-patch.tsclient/src/features/book/lib/filter-labels.tsclient/src/features/book/lib/published-date.tsclient/src/features/book/lib/table-row-state-sync.spec.tsclient/src/features/dashboard/components/DashboardScroller.spec.tsclient/src/features/series/composables/useSeriesBookMediaGroups.test.tsclient/src/features/series/views/SeriesDetailView.spec.tsclient/src/views/__tests__/BookDetailView.spec.tspackages/types/src/__tests__/jump-buckets.spec.tspackages/types/src/book-dock.tspackages/types/src/book.tspackages/types/src/file-write.tspackages/types/src/jump-buckets.tspackages/types/src/koreader.tspackages/types/src/metadata-fetch.tspackages/types/src/query.tsserver/src/common/utils/published-date.utils.test.tsserver/src/common/utils/published-date.utils.tsserver/src/db/migrations/0044_add_published_date.sqlserver/src/db/migrations/meta/0044_snapshot.jsonserver/src/db/migrations/meta/_journal.jsonserver/src/db/schema/metadata.tsserver/src/modules/book-dock/book-dock-finalize.service.tsserver/src/modules/book-dock/book-dock-metadata.service.test.tsserver/src/modules/book-dock/book-dock-metadata.service.tsserver/src/modules/book-dock/dto/index.tsserver/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.tsserver/src/modules/book-metadata-lock/book-metadata-lock.service.test.tsserver/src/modules/book-metadata-lock/book-metadata-lock.service.tsserver/src/modules/book/book-query-builder.service.test.tsserver/src/modules/book/book-query-builder.service.tsserver/src/modules/book/book-sort-builder.service.test.tsserver/src/modules/book/book-sort-builder.service.tsserver/src/modules/book/book.repository.tsserver/src/modules/book/book.service.tsserver/src/modules/book/dto/book-detail.dto.tsserver/src/modules/book/dto/book-dto-validation.test.tsserver/src/modules/book/dto/update-book-metadata.dto.tsserver/src/modules/book/jump-bucket-expr.test.tsserver/src/modules/book/jump-bucket-expr.tsserver/src/modules/book/utils/assemble-book-cards.test.tsserver/src/modules/book/utils/assemble-book-cards.tsserver/src/modules/file-write/file-write.repository.tsserver/src/modules/file-write/formats/audio/audio-format-writer.tsserver/src/modules/file-write/formats/cbx/comic-info-builder.tsserver/src/modules/file-write/formats/epub/epub-opf-builder.tsserver/src/modules/file-write/formats/pdf/pdf-format-writer.test.tsserver/src/modules/file-write/formats/pdf/pdf-write-core.tsserver/src/modules/file-write/formats/pdf/pdf-xmp-builder.tsserver/src/modules/file-write/interfaces/book-write-payload.interface.tsserver/src/modules/kobo/services/kobo-sync.service.tsserver/src/modules/koreader/koreader-catalog.service.tsserver/src/modules/metadata-fetch/metadata-fetch-pipeline.tsserver/src/modules/metadata-fetch/providers/aladin/aladin.mapper.tsserver/src/modules/metadata-fetch/providers/amazon/amazon.provider.tsserver/src/modules/metadata-fetch/providers/amazon/amazon.scraper.tsserver/src/modules/metadata-fetch/providers/audible/audible.mapper.tsserver/src/modules/metadata-fetch/providers/audnexus/audnexus.mapper.tsserver/src/modules/metadata-fetch/providers/comicvine/comicvine.mapper.tsserver/src/modules/metadata-fetch/providers/goodreads/goodreads.mapper.test.tsserver/src/modules/metadata-fetch/providers/goodreads/goodreads.mapper.tsserver/src/modules/metadata-fetch/providers/google/google.mapper.test.tsserver/src/modules/metadata-fetch/providers/google/google.mapper.tsserver/src/modules/metadata-fetch/providers/hardcover/hardcover.mapper.test.tsserver/src/modules/metadata-fetch/providers/hardcover/hardcover.mapper.tsserver/src/modules/metadata-fetch/providers/itunes/itunes.mapper.tsserver/src/modules/metadata-fetch/providers/kobo/kobo.provider.tsserver/src/modules/metadata-fetch/providers/kobo/kobo.scraper.test.tsserver/src/modules/metadata-fetch/providers/kobo/kobo.scraper.tsserver/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.tsserver/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.tsserver/src/modules/metadata-fetch/providers/open-library/open-library.mapper.tsserver/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.tsserver/src/modules/metadata/extractors/audio-format.extractor.tsserver/src/modules/metadata/extractors/audio.extractor.tsserver/src/modules/metadata/extractors/comic-format.extractor.tsserver/src/modules/metadata/extractors/format-extractor.interface.tsserver/src/modules/metadata/extractors/mobi-format.extractor.tsserver/src/modules/metadata/extractors/opf-metadata.mapper.tsserver/src/modules/metadata/extractors/pdf-format.extractor.tsserver/src/modules/metadata/lib/cbz-metadata.tsserver/src/modules/metadata/lib/fb2-parser.tsserver/src/modules/metadata/lib/opf-parser.tsserver/src/modules/metadata/lib/pdf-parser.tsserver/src/modules/metadata/lib/pdf-xmp-reader.tsserver/src/modules/metadata/metadata.service.tsserver/src/modules/scanner/scanner.repository.ts
| function applyPublishedPatch(formPatch: MetadataPatch, skippedFields: BookMetadataLockField[]): number { | ||
| if (formPatch.publishedDate === undefined && formPatch.publishedYear === undefined) return 0 | ||
| if (isLocked('publishedYear')) { | ||
| trackLockedField('publishedYear', skippedFields) | ||
| return 0 | ||
| } | ||
| if (formPatch.publishedDate !== undefined) form.publishedDate = formPatch.publishedDate | ||
| if (formPatch.publishedYear !== undefined) form.publishedYear = formPatch.publishedYear | ||
| return 1 | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if buildFileMetadataPatch always provides both publishedDate and publishedYear
rg -n -A5 'publishedDate\|publishedYear' client/src/features/book/lib/file-metadata-patch.tsRepository: bookorbit/bookorbit
Length of output: 157
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- locate file-metadata-patch.ts ---'
fd -a 'file-metadata-patch.ts' client/src/features/book || true
echo '--- search for buildFileMetadataPatch / published fields ---'
rg -n 'buildFileMetadataPatch|publishedDate|publishedYear' client/src/features/book -S || trueRepository: bookorbit/bookorbit
Length of output: 24095
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show likely files in the book feature area
git ls-files 'client/src/features/book/**' | sed -n '1,200p'
echo '---'
# Search for the patch builder and any published field handling
rg -n 'buildFileMetadataPatch|applyPublishedPatch|publishedDate|publishedYear' client/src/features/book -SRepository: bookorbit/bookorbit
Length of output: 37530
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the actual patch-builder implementation and its call sites.
rg -n 'buildFileMetadataPatch|buildPreviewPatch|applyPublishedPatch' . -S --glob '!**/node_modules/**'Repository: bookorbit/bookorbit
Length of output: 1847
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- file-metadata-patch.ts ---'
cat -n client/src/features/book/lib/file-metadata-patch.ts | sed -n '1,220p'
echo '--- file-metadata-patch.spec.ts ---'
cat -n client/src/features/book/lib/__tests__/file-metadata-patch.spec.ts | sed -n '1,220p'Repository: bookorbit/bookorbit
Length of output: 4647
Derive publishedYear for date-only patches (EditMetadataTab.vue:411-420)
buildFileMetadataPatch forwards whichever fields exist, so applyPublishedPatch can receive publishedDate without publishedYear. In that case the date updates while the year stays stale; derive the year from the date unless an explicit publishedYear is present.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/src/features/book/components/detail/tabs/EditMetadataTab.vue` around
lines 411 - 420, `applyPublishedPatch` currently updates `publishedDate` without
syncing `publishedYear` when only a date is provided. Update this helper so that
when `formPatch.publishedDate` exists but `publishedYear` is absent, it derives
and assigns `publishedYear` from the date before returning. Keep the existing
lock check for `publishedYear` and preserve explicit `publishedYear` values when
they are present.
| if (MONTH_NAME_RE.test(trimmed) && /\b\d{1,2}\b/.test(trimmed) && /\b\d{4}\b/.test(trimmed)) { | ||
| const parsed = new Date(trimmed); | ||
| return dateToPublishedDateKey(parsed); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Timezone bug: natural-language date parsing can shift the date by one day.
new Date("May 15, 2023") parses as local midnight. dateToPublishedDateKey then calls toISOString() which converts to UTC — in UTC+ timezones this yields the previous day (e.g., "2023-05-14" in UTC+9). The same issue affects the value instanceof Date path (line 42–43) if a locally-constructed Date is passed.
The parsePublishedDateFromEpochMillis path is unaffected because epoch millis represent an exact UTC moment.
🐛 Proposed fix: use UTC components for natural-language dates
function dateToPublishedDateKey(value: Date): string | undefined {
if (Number.isNaN(value.getTime())) return undefined;
- const key = value.toISOString().slice(0, 10);
+ const key = `${value.getUTCFullYear()}-${String(value.getUTCMonth() + 1).padStart(2, '0')}-${String(value.getUTCDate()).padStart(2, '0')}`;
return isPublishedDateKey(key) ? key : undefined;
}Important: this fix alone is not sufficient for the MONTH_NAME_RE path because new Date("May 15, 2023") still creates a local-midnight Date. For that path, parse the string as UTC explicitly:
if (MONTH_NAME_RE.test(trimmed) && /\b\d{1,2}\b/.test(trimmed) && /\b\d{4}\b/.test(trimmed)) {
- const parsed = new Date(trimmed);
- return dateToPublishedDateKey(parsed);
+ const parsed = new Date(trimmed);
+ if (Number.isNaN(parsed.getTime())) return undefined;
+ // Use local components since the string was parsed as local time
+ const key = `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}-${String(parsed.getDate()).padStart(2, '0')}`;
+ return isPublishedDateKey(key) ? key : undefined;
}This uses local getFullYear/getMonth/getDate (matching the local-time parse semantics of new Date(nonISOString)) while the Date-object and epoch-millis paths continue using UTC components.
Also applies to: 96-100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/src/common/utils/published-date.utils.ts` around lines 61 - 63, The
published-date conversion has a timezone shift in the natural-language and
Date-object paths, which can produce the previous day after toISOString is
applied. Update published-date handling in published-date.utils.ts so the
MONTH_NAME_RE branch does not rely on new Date(trimmed) plus UTC serialization;
instead parse the month-name string as a UTC date explicitly, and keep the value
instanceof Date path using the appropriate date components so
dateToPublishedDateKey returns the intended calendar day. Ensure the fix is
applied in the published date parsing flow around
parsePublishedDateFromEpochMillis/dateToPublishedDateKey and the MONTH_NAME_RE
logic.
| ALTER TABLE "book_metadata" ADD COLUMN "published_date" date;--> statement-breakpoint | ||
| CREATE INDEX "bm_published_date_idx" ON "book_metadata" USING btree ("published_date");--> statement-breakpoint | ||
| CREATE INDEX "bm_published_date_sort_idx" ON "book_metadata" USING btree (coalesce("published_date", make_date("published_year", 1, 1)));--> statement-breakpoint | ||
| ALTER TABLE "book_metadata" ADD CONSTRAINT "book_metadata_published_date_range_chk" CHECK ("book_metadata"."published_date" is null or (extract(year from "book_metadata"."published_date") >= 1000 and extract(year from "book_metadata"."published_date") <= 2200)); No newline at end of file |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the migration and migration runner files.
git ls-files | rg 'server/src/db/migrations/0044_add_published_date\.sql|server/src/db|drizzle|migrat|migration'
# Show nearby migration files to infer pattern.
fd -a '0044_add_published_date.sql|migration.*|migrations' server/src/db 2>/dev/null || trueRepository: bookorbit/bookorbit
Length of output: 12007
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the migration file and the migration runner/config that executes SQL.
wc -l server/src/db/migrations/0044_add_published_date.sql
cat -n server/src/db/migrations/0044_add_published_date.sql
printf '\n--- search for transaction handling and statement-breakpoint semantics ---\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' \
'statement-breakpoint|BEGIN|COMMIT|transaction|wrap.*transaction|migrate' server/src/db server 2>/dev/nullRepository: bookorbit/bookorbit
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the migration and discover the migration runner path precisely.
git ls-files 'server/src/db/**' | sed -n '1,200p'Repository: bookorbit/bookorbit
Length of output: 6566
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find files that define migration execution or database client initialization.
git ls-files | rg 'server/src/db/|drizzle|migrat|db/index|database|schema'
# Search for explicit transactional migration settings.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' \
'transaction|single-statement|statement-breakpoint|migrate.*transaction|use.*transaction|sql migrations' \
server/src . 2>/dev/nullRepository: bookorbit/bookorbit
Length of output: 50376
🌐 Web query:
Drizzle ORM SQL migrations transaction default statement-breakpoint create index transaction postgres migrator
💡 Result:
In Drizzle ORM, migration behavior regarding transactions and statement breakpoints is defined as follows: Transactions: Drizzle migrations are designed to run within transactions by default where the database dialect supports it [1][2]. For PostgreSQL, migrations are executed inside a transaction, which ensures atomicity for the applied operations [1][3][2]. If you are running multiple pending migration files, Drizzle Kit typically applies them as part of the same transaction process [4]. Statement Breakpoints: The --> statement-breakpoint comment is a mechanism used by Drizzle Kit to split migration files into multiple sequential execution blocks [3]. 1. Purpose: It enables the sequential execution of individual SQL statements within a single migration file [3]. This is critical for databases like MySQL, SQLite, and SingleStore, which do not support multi-statement transactions for certain DDL commands [3][5]. 2. PostgreSQL Usage: While PostgreSQL supports multi-statement transactions, Drizzle still uses these breakpoints in generated migration files to maintain consistency and order across different dialects [3]. 3. Custom Migrations: If you are writing custom SQL migration files, you may need to manually include --> statement-breakpoint between DDL or DML statements to ensure they are executed correctly and as expected, especially if you encounter issues where subsequent commands are not executing [6]. Index Creation: When generating migrations for indexes (e.g., CREATE INDEX), Drizzle Kit automatically manages the necessary SQL syntax and includes the required --> statement-breakpoint to ensure the DDL statements are processed appropriately by the database [7]. If you are defining these in your schema, Drizzle handles the translation to the required SQL format automatically [7]. In summary, the --> statement-breakpoint acts as a delimiter that Drizzle uses to safely manage execution order and transaction constraints across different SQL dialects [3][5][6].
Citations:
- 1: https://drizzle-team-drizzle-orm.mintlify.app/core/migrations
- 2: https://drizzle-team-drizzle-orm.mintlify.app/api/core/migrations
- 3: https://drizzle-team-drizzle-orm.mintlify.app/api/kit/migrations
- 4: [BUG]: Drizzle kit applies multiple migration files in the same transaction drizzle-team/drizzle-orm#3249
- 5: https://geekbetrieb.de/ramblings/drizzle-orm-and-statement-breakpoint/
- 6: https://www.answeroverflow.com/m/1193324770092134430
- 7: [DOCS]: Index creation docs unclear drizzle-team/drizzle-orm#3851
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the migration runner and Drizzle config for transaction settings.
wc -l server/src/scripts/migrate.ts server/drizzle.config.ts server/src/db/db.module.ts server/src/db/index.ts
cat -n server/src/scripts/migrate.ts
printf '\n--- drizzle.config.ts ---\n'
cat -n server/drizzle.config.ts
printf '\n--- db.module.ts ---\n'
sed -n '1,220p' server/src/db/db.module.ts
printf '\n--- db/index.ts ---\n'
sed -n '1,220p' server/src/db/index.tsRepository: bookorbit/bookorbit
Length of output: 3238
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the generated snapshot for the table/index/constraint definitions to understand intent.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' \
'published_date|bm_published_date|book_metadata_published_date_range_chk' \
server/src/db/migrations/meta/0044_snapshot.json server/src/db/schema/metadata.ts server/src/db/schema/books.tsRepository: bookorbit/bookorbit
Length of output: 1867
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read only the relevant snapshot slices.
python3 - <<'PY'
from pathlib import Path
paths = [
Path("server/src/db/migrations/meta/0044_snapshot.json"),
Path("server/src/db/schema/metadata.ts"),
]
for p in paths:
print(f"\n--- {p} ---")
if p.suffix == ".json":
text = p.read_text()
for needle in ["published_date", "bm_published_date", "book_metadata_published_date_range_chk"]:
idx = text.find(needle)
if idx != -1:
start = max(0, idx - 600)
end = min(len(text), idx + 1600)
print(text[start:end])
break
else:
# print line numbers around published_date if present
for i, line in enumerate(p.read_text().splitlines(), 1):
if "published_date" in line or "published_year" in line:
print(f"{i}: {line}")
PYRepository: bookorbit/bookorbit
Length of output: 3022
Avoid blocking writes here. This migration runs through the default transactional path, so CREATE INDEX and the new CHECK can hold write locks on book_metadata while the table is scanned. Split this into an online migration (CREATE INDEX CONCURRENTLY, NOT VALID/VALIDATE) or run it off-peak.
🧰 Tools
🪛 Squawk (2.59.0)
[warning] 2-2: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
[warning] 3-3: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
[warning] 4-4: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.
(constraint-missing-not-valid)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/src/db/migrations/0044_add_published_date.sql` around lines 1 - 4, The
migration for book_metadata is using blocking DDL on the default transactional
path. Update 0044_add_published_date.sql so the index creation in
bm_published_date_idx and bm_published_date_sort_idx uses an online approach
like CREATE INDEX CONCURRENTLY, and make the
book_metadata_published_date_range_chk constraint a NOT VALID check followed by
a separate VALIDATE step; if that is not possible in this migration flow, move
the work to an off-peak online migration.
Source: Linters/SAST tools
| function setComicInfoPublicationDate(info: ComicInfoObject, fieldMask: Set<BookWritePayloadKey>, payload: BookWritePayload): void { | ||
| const shouldWrite = fieldMask.has('publishedDate') || fieldMask.has('publishedYear'); | ||
| if (!shouldWrite) return; | ||
| if (payload.publishedDate) { | ||
| info['Year'] = Number(payload.publishedDate.slice(0, 4)); | ||
| info['Month'] = Number(payload.publishedDate.slice(5, 7)); | ||
| info['Day'] = Number(payload.publishedDate.slice(8, 10)); | ||
| return; | ||
| } | ||
| if (payload.publishedYear != null) { | ||
| info['Year'] = payload.publishedYear; | ||
| delete info['Month']; | ||
| delete info['Day']; | ||
| return; | ||
| } | ||
| delete info['Year']; | ||
| delete info['Month']; | ||
| delete info['Day']; | ||
| } | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Field mask not respected granularly — unwanted ComicInfo fields may be written to user files.
The shouldWrite gate checks if either publishedDate or publishedYear is in the mask, but the body doesn't distinguish which one was selected. Two violation scenarios:
- Only
publishedYearin mask,payload.publishedDateis set: The code writesMonthandDayfrompublishedDateeven though the user only selected year for write-back. - Only
publishedDatein mask,payload.publishedDateis null: The code falls through to writepayload.publishedYeareven though the user didn't select year.
Both cases write metadata to users' comic files that they didn't explicitly choose, violating the field-mask contract that setComicInfoField enforces for every other field.
🔧 Proposed fix: gate each branch by its own field-mask entry
function setComicInfoPublicationDate(info: ComicInfoObject, fieldMask: Set<BookWritePayloadKey>, payload: BookWritePayload): void {
- const shouldWrite = fieldMask.has('publishedDate') || fieldMask.has('publishedYear');
- if (!shouldWrite) return;
- if (payload.publishedDate) {
+ const hasDate = fieldMask.has('publishedDate');
+ const hasYear = fieldMask.has('publishedYear');
+ if (!hasDate && !hasYear) return;
+ if (hasDate && payload.publishedDate) {
info['Year'] = Number(payload.publishedDate.slice(0, 4));
info['Month'] = Number(payload.publishedDate.slice(5, 7));
info['Day'] = Number(payload.publishedDate.slice(8, 10));
return;
}
- if (payload.publishedYear != null) {
+ if (hasYear && payload.publishedYear != null) {
info['Year'] = payload.publishedYear;
delete info['Month'];
delete info['Day'];
return;
}
delete info['Year'];
delete info['Month'];
delete info['Day'];
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function setComicInfoPublicationDate(info: ComicInfoObject, fieldMask: Set<BookWritePayloadKey>, payload: BookWritePayload): void { | |
| const shouldWrite = fieldMask.has('publishedDate') || fieldMask.has('publishedYear'); | |
| if (!shouldWrite) return; | |
| if (payload.publishedDate) { | |
| info['Year'] = Number(payload.publishedDate.slice(0, 4)); | |
| info['Month'] = Number(payload.publishedDate.slice(5, 7)); | |
| info['Day'] = Number(payload.publishedDate.slice(8, 10)); | |
| return; | |
| } | |
| if (payload.publishedYear != null) { | |
| info['Year'] = payload.publishedYear; | |
| delete info['Month']; | |
| delete info['Day']; | |
| return; | |
| } | |
| delete info['Year']; | |
| delete info['Month']; | |
| delete info['Day']; | |
| } | |
| function setComicInfoPublicationDate(info: ComicInfoObject, fieldMask: Set<BookWritePayloadKey>, payload: BookWritePayload): void { | |
| const hasDate = fieldMask.has('publishedDate'); | |
| const hasYear = fieldMask.has('publishedYear'); | |
| if (!hasDate && !hasYear) return; | |
| if (hasDate && payload.publishedDate) { | |
| info['Year'] = Number(payload.publishedDate.slice(0, 4)); | |
| info['Month'] = Number(payload.publishedDate.slice(5, 7)); | |
| info['Day'] = Number(payload.publishedDate.slice(8, 10)); | |
| return; | |
| } | |
| if (hasYear && payload.publishedYear != null) { | |
| info['Year'] = payload.publishedYear; | |
| delete info['Month']; | |
| delete info['Day']; | |
| return; | |
| } | |
| delete info['Year']; | |
| delete info['Month']; | |
| delete info['Day']; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/src/modules/file-write/formats/cbx/comic-info-builder.ts` around lines
112 - 131, `setComicInfoPublicationDate` is using a broad `shouldWrite` check
and then writing whichever date data is available, which can emit `Year`,
`Month`, or `Day` even when only one publication field was selected. Update the
branching so `publishedDate` writes only when `fieldMask` contains
`publishedDate`, and `publishedYear` writes only when `fieldMask` contains
`publishedYear`; if the selected field is absent, remove the corresponding
ComicInfo keys instead of falling through. Keep the logic localized in
`setComicInfoPublicationDate` and align it with the field-mask behavior used by
`setComicInfoField`.
|
🎉 This PR is included in version 2.2.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Before you submit
What does this PR do?
Adds support for storing and using full publication dates while keeping year-only metadata compatible with existing library views and integrations.
This PR includes:
published_datemetadata column and sharedpublishedDatecontract fieldspublishedYearfrom full dates so existing sorting, locks, exports, and clients continue to workCloses #596
How did you test this?
pnpm run verify:fastpnpm --filter server test -- src/common/utils/published-date.utils.test.ts src/modules/book/book-query-builder.service.test.ts src/modules/book/book-sort-builder.service.test.ts src/modules/book-metadata-lock/book-metadata-lock.service.test.ts src/modules/book/dto/book-dto-validation.test.ts src/modules/metadata-fetch/providers/google/google.mapper.test.ts src/modules/metadata-fetch/providers/hardcover/hardcover.mapper.test.ts src/modules/metadata-fetch/providers/kobo/kobo.scraper.test.tspnpm --filter client test:unit --run src/features/book/composables/__tests__/useMetadataDiff.spec.ts src/features/book/composables/__tests__/tableColumnSchema.spec.ts src/features/book/lib/book-card-mapper.test.ts src/features/book/components/__tests__/BookListRow.spec.ts src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.ts src/features/book/components/table/__tests__/BookTableCellDispatcher.spec.tsScreenshots
UI changes are limited to existing metadata display and editing surfaces for publication dates. Screenshots can be added on GitHub with a book that has a full publication date.
Anything non-obvious in the diff?
The feature keeps both
publishedDateandpublishedYear. Full dates are saved as full dates, while year-only metadata remains year-only and still drives existing year-based behavior.AI tools used: OpenAI Codex
Extent: assisted with porting the existing branch as a code-only change, drafting this PR description, and running verification. The patch identity was checked against the source branch and local lint, typecheck, and tests were run.
Checklist
.env, personal configs)Summary by CodeRabbit
New Features
Bug Fixes