Skip to content
Merged
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
108 changes: 0 additions & 108 deletions .github/prompts/review.prompt.md

This file was deleted.

127 changes: 127 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# AGENTS.md — mod-quick-marc

Spring Boot 4 / Java 21 backend module that provides a REST API for editing MARC bibliographic, holdings, and authority records stored in Source Record Storage (SRS).

---

## Build, test, and lint

```bash
# Full build (unit + integration tests)
mvn clean install

# Unit tests only (JUnit tag: unit)
mvn test

# Integration tests only (JUnit tag: integration)
mvn verify -DskipTests

# Single unit test class
mvn test -Dtest=MarcUtilsTest

# Single unit test method
mvn test -Dtest=MarcUtilsTest#normalizeFixedLengthString_ShouldPreserveLeadingBlanks

# Single integration test class
mvn failsafe:integration-test -Dit.test=RecordsEditorIT

# Checkstyle
mvn checkstyle:check

# Run locally with dev profile (auto-starts Docker infra)
mvn spring-boot:run -Dspring-boot.run.profiles=dev
```

---

## Architecture overview

```
REST controllers (org.folio.qm.controller)
└── Service layer (org.folio.qm.service)
├── change/ – create, update record orchestration
├── fetch/ – read record
├── links/ – authority link suggestions
├── validation/ – validate-only path
└── storage/ – thin wrappers around Feign clients
├── source/ – mod-source-record-storage
├── folio/ – mod-inventory-storage, mod-entities-links
├── specification/ – mod-record-specifications
└── tenant/ – user, config lookups

Conversion pipeline (org.folio.qm.convertion)
├── converter/ – Spring Converter<A,B> beans wired into ConversionService
│ – SourceRecord → QuickMarcView (MARC→QM direction)
│ – QuickMarcView → marc4j Record (QM→MARC direction)
├── field/dto/ – Tag006/007/008 converters: marc4j ControlField → QM FieldItem
├── field/qm/ – Tag006/007/008 converters: QM FieldItem → marc4j ControlField
└── elements/ – ControlFieldItem enums and Tag008Configuration

External clients (org.folio.qm.client) – Feign, generated from OpenAPI specs

Domain DTOs (org.folio.qm.domain.dto) – Generated by openapi-generator; never edit directly
```

**Request flow (update):** controller → `UpdateRecordService` → `QuickMarcRecordConverter` (QM→marc4j) → `SourceStorageClient` (PUT to SRS) → `InventoryStorageClient` (entity sync). Response: 202 (synchronous, no Kafka queue).

**Kafka consumer:** `SpecificationEventListener` listens to `{ENV}.*.specification-storage.specification.updated` and refreshes the `specifications` Caffeine cache.

---

## Key conventions

### MARC blank masking
MARC blanks (spaces) are represented as `\` (backslash) in the QM JSON model. The canonical constant is `Constants.BLANK_REPLACEMENT = "\\"`.

- `MarcUtils.masqueradeBlanks(s)` — replaces spaces with `\` (MARC→QM direction)
- `MarcUtils.restoreBlanks(s)` — replaces `\` with spaces (QM→MARC direction)
- `MarcUtils.normalizeFixedLengthString(s, length)` — pads/trims to expected length, then calls `masqueradeBlanks`. Uses `StringUtils.defaultString` (not `trimToEmpty`) so leading blanks are preserved as positional data.

### DTOs are generated — do not edit them
`src/main/java/org/folio/qm/domain/dto/` is produced by `openapi-generator-maven-plugin` from the YAML specs in `src/main/resources/swagger.api/`. Edit the YAML specs, not the generated Java. The generation runs during `generate-sources` phase.

### Field converter pattern
Each special control field tag (006, 007, 008) has two converters:
- `convertion/field/dto/Tag00X…Converter` — MARC→QM (marc4j `ControlField` → `FieldItem`)
- `convertion/field/qm/Tag00X…FieldItemConverter` — QM→MARC (`FieldItem` → marc4j `VariableField`)

Both implement a `canProcess(field, marcFormat)` method for dispatch. New tag converters follow the same pattern and must be annotated `@Component`.

### Test tagging
- `@UnitTest` (`org.folio.spring.testing.type.UnitTest`) — picked up by `maven-surefire-plugin` (tag `unit`)
- `@IntegrationTest` (`org.folio.spring.testing.type.IntegrationTest`) — picked up by `maven-failsafe-plugin` (tag `integration`)

Integration tests all extend `org.folio.it.BaseIT`, which spins up real PostgreSQL, Kafka, and a WireMock Okapi via Testcontainers (`@EnablePostgres`, `@EnableKafka`, `@EnableOkapi`). WireMock stubs live in `src/test/resources/mappings/` and fixtures in `src/test/resources/__files/`.

### Test data enums
Parametrized converter tests use enum-backed test data:
- `Tag006FieldTestData`, `Tag007FieldTestData`, `Tag008FieldTestData`

Each enum constant carries `dtoData` (raw MARC string), `leader`, and `qmContent` (expected QM map). Use `@EnumSource` to cover all cases. Cases where the reverse (QM→MARC) conversion intentionally auto-modifies a value (e.g., `Date Entered` auto-fill for blank/invalid dates) must be excluded from round-trip tests via the `names`/`mode` filter on `@EnumSource`.

### Caches
Four Caffeine caches are declared in `application.yaml`:
- `linking-rules-results` — authority linking rules
- `specifications` — MARC specifications (custom TTL/size via `folio.cache.spec.specifications.*`)
- `authorities-extended-mapping-cache`
- `consortium-central-tenant-cache`

The `specifications` cache uses custom config keys (`folio.cache.spec.specifications.ttl=24h`, `folio.cache.spec.specifications.maximum-size=500`), not the global Caffeine spec.

### Commit and branch conventions
- Branch names follow Jira issue IDs: `MODQM-NNN`
- Commits follow [Conventional Commits](https://folio-org.atlassian.net/wiki/spaces/FOLIJET/pages/1400654/Conventional+Commits+Guideline): `fix(scope): message`, `feat(scope): message`, `docs: message`, etc.
Use a **scope** only when the change is narrowly tied to a single feature ID from [`docs/features/`](docs/features/):
`get-record` · `create-record` · `update-record` · `validate-record` · `links-suggestions` · `specification-refresh`
Omit the scope when the change spans multiple features or does not belong to any of them.
Examples: `fix(get-record): preserve leading blanks in MARC 008 Date Entered` / `chore: upgrade Spring Boot`
- `NEWS.md` must be updated for every bug fix or feature under the current version section.
- PRs must follow the template in [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md): fill in **Purpose**, **Approach**, and tick all items in the **Changes Checklist** (API changes, schema changes, interface versions, permissions, logging, unit/integration/manual testing, NEWS).

### Sonar exclusions
`entity`, `client`, and `repository` packages are excluded from SonarQube analysis (see `pom.xml` `sonar.exclusions`). Do not add logic to these packages.

---

## Feature documentation
Behavioral feature docs live in `docs/features/`. Each file uses fixed sections in this order: What it does, Why it exists, Entry point(s), Business rules, Error behavior, Caching, Configuration, Dependencies. Feature names describe observable behavior, not implementation mechanisms.
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## v8.0.1 2026-05-12
### Bug fixes
* Ignore `statusUpdatedDate` Instance field that is not mapped from MARC ([MODQM-514](https://folio-org.atlassian.net/browse/MODQM-514))
* Fix MARC 008 fields shifting left when positions 00-05 contain blank characters ([MODQM-515](https://folio-org.atlassian.net/browse/MODQM-515))

## v8.0.0 2026-04-17
### Breaking changes
* Update QuickMARC to use generation field from SRS for optimistic locking ([MODQM-478](https://folio-org.atlassian.net/browse/MODQM-478))
Expand Down
36 changes: 25 additions & 11 deletions docs/features/create-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,46 @@ updated: 2026-04-01
# Create Record

## What it does
Creates a brand-new MARC record together with its corresponding FOLIO inventory entity (Instance, Holdings, or Authority). The endpoint validates the submitted record, derives the inventory entity from the MARC content, persists both the inventory record and the SRS source record, and returns the full `quickMarcView` of the newly created record including the assigned external ID and HRID.
Creates a brand-new MARC record together with its corresponding FOLIO inventory entity (Instance, Holdings, or Authority). The endpoint validates the submitted record, derives the inventory entity from the MARC content, persists both the inventory record and the SRS source record, and returns the full `quickMarcView` of the newly created record including the assigned external ID and HRID. The `201 Created` response contains the full `quickMarcView` of the newly created record, including the assigned `parsedRecordId`, `parsedRecordDtoId`, `externalId`, and `externalHrid`.

## Why it exists
Cataloguers need the ability to create original MARC records without importing from an external source. This endpoint provides a single, consistent entry point for creating all three MARC types within the quickMARC editor.

## Entry point(s)
| Method | Path | Description |
|--------|------|-------------|
| POST | /records-editor/records | Creates a new MARC record and its linked FOLIO entity |
| POST | /records-editor/records | Creates a new MARC record and its linked FOLIO entity; returns `201 Created` with the full `quickMarcView` |

## Business rules and constraints
- The MARC record is validated before creation. For Bibliographic and Authority formats, validation is run against the MARC specification with the `001 MISSING_FIELD` rule skipped (the 001 field is added automatically after the FOLIO entity is created). Holdings records skip specification-based validation.
- The inventory entity is created first; the resulting external UUID and HRID are stored in the new MARC record.
- Required MARC fields are added automatically after inventory creation:

#### Sequencing
- **Validation before creation:** The MARC record is validated before creation. For Bibliographic and Authority formats, validation is run against the MARC specification with the `001 MISSING_FIELD` rule skipped (the 001 field is added automatically after the FOLIO entity is created). Holdings records skip specification-based validation.
- **Inventory first:** The inventory entity is created first; the resulting external UUID and HRID are stored in the new MARC record.
- **SRS snapshot first:** Before creating the SRS source record, a snapshot is created in mod-source-record-storage; the source record is written within that snapshot.

#### Field generation
- **Auto-added fields:** Required MARC fields are added automatically after inventory creation:
- A `999 $i` field containing the external entity UUID is added for all record types.
- A `001` field containing the HRID is added for Instance and Holdings records.
- For Instance (Bibliographic) records, `003` fields are removed and `035` fields are normalised before the SRS record is created.
- The SRS record is created with `generation = 0`, `state = ACTUAL`, and `deleted = false`.
- A `001` field containing the HRID is added for **Instance and Holdings** records only (not Authority).
- **Bibliographic (Instance) normalisation:** `003` fields are removed and `035` fields are normalised before SRS creation. If leader position 05 (`LDR/05`) equals `d` (deleted), `staffSuppress` and `discoverySuppress` are set to `true` on the Instance.
- **Holdings normalisation:** `003` fields are removed before SRS creation.
- **Authority:** no field normalisation is applied; `001` is not added (Authority records use `authorityHrid` carried in `999 $i`).

#### Persistence
- **Initial SRS state:** The SRS record is created with `generation = 0`, `state = ACTUAL`, and `deleted = false`.

## Error behavior
- **400 Bad Request** – malformed or unreadable request body.
- **422 Unprocessable Content** – MARC validation failed before creation; response includes a `validationResult` payload. Nothing is persisted.
- **500 Internal Server Error** – unexpected server-side failure.

## Dependencies and interactions
- **mod-inventory-storage** (instance-storage, holdings-storage) – creates the Instance or Holdings inventory entity and returns the assigned ID and HRID.
- **mod-entities-links** (authority-storage) – creates the Authority inventory entity and returns the assigned ID.
- **mod-source-record-storage** – creates the new SRS record containing the raw and parsed MARC content.
- **mod-record-specifications** – MARC field specifications used for validation.
- **mod-source-record-storage** – creates a snapshot then writes the new SRS record containing the raw and parsed MARC content.
- **mod-record-specifications** – MARC field specifications used for validation (Bibliographic and Authority only; Holdings skip specification-based validation).

### Internal feature dependencies
- [Validate Record](validate-record.md) – validation logic is invoked for Bibliographic and Authority records before the inventory and SRS records are created; a validation failure results in `422` and nothing is persisted.
- [Specification Cache Refresh](specification-cache-refresh.md) – the MARC specification used during validation is served from the cache that this feature keeps current.
- [Specification Cache Refresh](specification-refresh.md) – the MARC specification used during validation is served from the cache that this feature keeps current.

Loading
Loading