Skip to content

components: ensure unique cdsrn - #569

Merged
kpsherva merged 7 commits into
CERNDocumentServer:masterfrom
sakshamarora1:feature/unique_alt_id
Oct 24, 2025
Merged

kpsherva merged 7 commits into
CERNDocumentServer:masterfrom
sakshamarora1:feature/unique_alt_id

Conversation

@sakshamarora1

@sakshamarora1 sakshamarora1 commented Sep 4, 2025 •

Copy link
Copy Markdown
Contributor

closes: #370

  1. Renamed cds_ref -> cdsrn
  2. Added component to mint cdsrn pid_type

Steps:

  1. Merge this PR
  2. Run the following scripts (It will automatically mint pids for the existing ones as it uses the service layer.
# Script to update cds_ref to cdsrn and mint cdsrn
from invenio_access.permissions import system_identity
from invenio_pidstore.errors import PIDAlreadyExists
from invenio_db import db
from invenio_drafts_resources.services.records.errors import DraftNotCreatedError
from invenio_rdm_records.proxies import current_rdm_records_service as records_service

from cds_rdm.components import MintAlternateIdentifierComponent

alternate_identifier_component = MintAlternateIdentifierComponent(records_service)

# Drafts (also, works for new versions)
hits = records_service._search(
    identity=system_identity,
    action="scan",
    params={"q": "metadata.identifiers.scheme:cds_ref", "allversions": True, "include_deleted": True},
    search_preference=None,
    record_cls=records_service.draft_cls,
    search_opts=records_service.config.search_drafts,
    permission_action="read_draft",
).scan()
for hit in hits:
    draft = records_service.read_draft(system_identity, hit['id'])
    for identifier in draft.data["metadata"].get("identifiers", []):
        if identifier.get("scheme") == "cds_ref":
            identifier["scheme"] = "cdsrn"
    updated_draft = records_service.update_draft(system_identity, draft.id, draft.data)
    print(f"INFO: Draft <{updated_draft.id}> updated with cds_ref to cdsrn.")

# Records, run after deploying changes: 1. cds_ref to cdsrn, 2. service component to mint cdsrn
hits = records_service.scan(system_identity, params={"q": "metadata.identifiers.scheme:cds_ref", "allversions": True, "include_deleted": True})
for hit in hits:
    record = records_service.read(
        system_identity, hit["id"], include_deleted=True
    )
    try:
        draft = records_service.read_draft(system_identity, record.id)
        # Step 1: Update cds_ref to cdsrn in the record via low-level API
        print(f"INFO: Record <{record.id}> has an existing draft <{draft.id}>! Updating record via low-level API.")
        # Update the record directly without affecting the draft
        for identifier in record._record["metadata"].get("identifiers", []):
            if identifier.get("scheme") == "cds_ref":
                identifier["scheme"] = "cdsrn"
        # Save the record changes and reindex
        record._record.commit()
        db.session.commit()
        records_service.indexer.index(record._record)
        # Run service component to mint cdsrn
        alternate_identifier_component.publish(system_identity, draft=draft, record=record)
        print(f"INFO: Record <{record.id}> updated with cds_ref to cdsrn and cdsrn minted.")

        # Step 2: Update cds_ref to cdsrn in the draft and save draft
        draft = records_service.edit(system_identity, draft.id)
        draft._record.fork_version_id = record._record.revision_id
        for identifier in draft.data["metadata"].get("identifiers", []):
            if identifier.get("scheme") == "cds_ref":
                identifier["scheme"] = "cdsrn"
        updated_draft = records_service.update_draft(
            system_identity, draft.id, draft.data
        )
        print(f"INFO: Draft <{updated_draft.id}> updated with cds_ref to cdsrn.")
    except DraftNotCreatedError:
        # If the draft didn't exist, we simply edit and publish the record
        draft = records_service.edit(system_identity, record.id)
        for identifier in draft.data["metadata"].get("identifiers", []):
            if identifier.get("scheme") == "cds_ref":
                identifier["scheme"] = "cdsrn"
        updated_draft = records_service.update_draft(
            system_identity, draft.id, draft.data
        )
        record = records_service.publish(system_identity, updated_draft.id)
        print(f"INFO: Record <{record.id}> updated with cds_ref to cdsrn and cdsrn minted.")

@yashlamba
yashlamba self-requested a review September 5, 2025 12:00
Comment thread site/cds_rdm/components.py Outdated
Comment thread site/cds_rdm/components.py Outdated
Comment thread site/cds_rdm/minters.py Outdated
Comment thread site/cds_rdm/components.py Outdated
Comment thread site/cds_rdm/components.py Outdated
return
else:
# Update the alternate identifiers with the new PIDs, but don't mint
record.metadata["identifiers"] = data["metadata"].get("identifiers", [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this will cover the case:

  1. user adds report number + another identifier, let's say legacy recid
  2. report number has a duplicate -> throws a validation error.

legacy recid is not remembered in the form, is it? all the alt identifiers will be wiped out if I am reading this correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the code, so this should not happen anymore, added a test case too.

Comment thread site/cds_rdm/minters.py Outdated
Comment thread site/cds_rdm/minters.py Outdated
Comment thread site/cds_rdm/minters.py Outdated
Comment thread site/tests/test_components.py
Comment thread site/cds_rdm/minters.py Outdated
Comment thread site/cds_rdm/minters.py Outdated
Comment thread site/cds_rdm/minters.py Outdated
)


def alternate_identifier_minter(scheme, value, uuid):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this utitlity ? can we use the .create directly
@ntarocco side question: in this case service (via component) access the data layer directly. Are we OK with that or should we find another way to manage it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that there are much alternatives, because we don't have a data layer exposing all the needed APIs. Most of our data layer objects are DB models that allows direct access, they are not wrapped exposing rich APIs.
Do you have other ideas?

Comment thread site/tests/test_components.py Outdated
# This should raise PIDAlreadyExists error
record9 = RDMRecord.publish(draft9._record)
with pytest.raises(PIDAlreadyExists):
component.publish(administrator.identity, draft=draft9._record, record=record9)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding all these tests, super good.
Can you please only change the identity, and always use the normal user and not system or admin whenever possible?

@kpsherva

Copy link
Copy Markdown
Contributor

check how long the scan should be

@kpsherva

Copy link
Copy Markdown
Contributor

on the draft lets mint with "reserved" status

@palkerecsenyi palkerecsenyi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I appreciate the nicely commented tests :)

Comment thread invenio.cfg
Comment on lines +141 to +149
alternate_identifiers_map = {}
for id in data["metadata"].get("identifiers", []):
if id["scheme"] in alt_id_schemes:
alternate_identifiers_map.setdefault(id["scheme"], []).append(
id["identifier"]
)
if not alternate_identifiers_map:
# If no mintable identifiers, return early
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could extract this into a shared method to reduce code duplication? But not really a huge deal since it's only used twice.

Comment thread site/tests/test_components.py Outdated
Comment thread site/cds_rdm/components.py
Comment thread site/cds_rdm/components.py
Comment thread invenio.cfg
CDS_CERN_SCIENTIFIC_COMMUNITY_ID = "CHANGE_ME"
"""The id of the CERN Scientific community."""

CDS_CERN_MINT_ALTERNATE_IDS = {"cdsrn": "CDS Reference"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't we say to have CDS report number as a name in the end? @ntarocco

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say so, what was the outcome to this question in your interviews? (pinging @zzacharo too)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From LHCb I remember that in their system the column name was "CDS reportnumber" but in their team they were calling it actually CDS reference number...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be scheduled for next deployment

Comment on lines +196 to +197
# Only delete the PIDs not in draft anymore, i.e. targetting this record
db.session.delete(pid)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for the fix above, in the next PR, can you please add something like (pseudocode):

assert pid.pid_type not in [recid, doi, oai]

It is just a safety check!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

@kpsherva
kpsherva merged commit fe9efa2 into CERNDocumentServer:master Oct 24, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CDS alternative identifier should be unique

6 participants