Skip to content
Open
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
30 changes: 30 additions & 0 deletions assertion-service-2/scripts/mongo-scripts-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# MONGO DB Scripts Setup Guide

## Run scripts via helper

Use `run-script.sh` from your local machine — no manual SSH or docker exec needed.

```bash
# Interactive shell inside the container
./run-script.sh --username <user> --server <host> --assertion-docker <container>

# Run a script
./run-script.sh --username <user> --server <host> --assertion-docker <container> --script test_connection.py
./run-script.sh --username <user> --server <host> --assertion-docker <container> --script query-fixes/backfill_orcid_record.py
```

## Clean test data

Use one of the options below to remove all affiliations shown in Affiliation Manager for a single member.

```bash
# Option 1: use Salesforce member ID
./run-script.sh --username <user> --server <host> --assertion-docker <container> --script "test-data/delete_documents.py --database assertionservice --collections assertion --member-salesforce-id <salesforce_id>"

# Option 2: use internal member ID (memberservice.member._id)
./run-script.sh --username <user> --server <host> --assertion-docker <container> --script "test-data/delete_documents.py --database assertionservice --collections assertion --member-id <member_id>"
```

Notes:
- Use exactly one selector: `--member-salesforce-id` or `--member-id`.
- The script will prompt for confirmation before deleting data.
15 changes: 0 additions & 15 deletions assertion-service-2/scripts/setup-scripts.md

This file was deleted.

162 changes: 117 additions & 45 deletions assertion-service-2/scripts/test-data/delete_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
"""
MongoDB Data Cleanup Script

This script removes documents from specified MongoDB collections using
filters defined in a JSON file. It is intended for targeted documents of
invalid, obsolete, or incorrect data.
This script removes affiliations for a target member from MongoDB.

Related ticket:
https://app.clickup.com/t/9014437828/PD-4941
Expand All @@ -17,34 +15,35 @@
--collections
One or more collection names to clean up.

--file_name
Path to a JSON file containing the document filters to delete.
--member-id
Internal member ID. When provided, the script deletes all affiliations
listed for that member in Affiliation Manager (assertion.member_id).

--member-salesforce-id
Salesforce member ID. The script resolves it via memberservice.member,
then deletes all affiliations listed in Affiliation Manager for the
resolved member ID.

Behavior
--------
For each specified collection, the script reads the filters from the
provided JSON file and deletes documents that match those filters.

Example JSON format:
[
{ "_id": "603f675aec57dc0008ae0952" },
{ "email": "test@orcid.org" }
]
The script builds the delete query from member scope and removes all matching
documents in the target collection.

Usage
-----
python delete_documents.py --database <database> --collections <collection1> [collection2 ...] --file_name <file.json>
python delete_documents.py --database assertionservice --collections assertion --member-id <member_id>
python delete_documents.py --database assertionservice --collections assertion --member-salesforce-id <sf_id>

Examples
--------
python delete_documents.py --database assertionservice --collections assertions orcid_record --file_name json/delete.json
python delete_documents.py --database assertionservice --collections assertion --member-id 689f6f2abc1234def56789ab
"""

import argparse
import json
import re
import sys
from pathlib import Path
from typing import List, Dict, Any
from typing import List, Dict, Any, Optional
from pymongo.errors import OperationFailure
from bson import ObjectId

Expand All @@ -61,13 +60,27 @@

logger = setup_logger(__name__, log_file='delete_documents.log')

SALESFORCE_ID_PATTERN = re.compile(r"^[a-zA-Z0-9]{18}$")


class DeleteDocuments:

def __init__(self, connection: MongoDBConnection, collection_name: str, file_name: str):
def __init__(
self,
connection: MongoDBConnection,
collection_name: str,
inline_filters: Optional[List[Dict[str, Any]]] = None
):
self.connection = connection
self.collection = connection.get_collection(collection_name)
self.file_name = file_name
self.inline_filters = inline_filters or []

def _load_items(self) -> Optional[List[Dict[str, Any]]]:
if self.inline_filters:
return self.inline_filters

logger.error("No inline filters were provided")
return None

def find_problematic_collections(self) -> List[Dict[str, Any]]:
"""
Expand All @@ -76,15 +89,9 @@ def find_problematic_collections(self) -> List[Dict[str, Any]]:
Returns:
List of problematic documents
"""
try:
with open(self.file_name, 'r') as f:
items = json.load(f)
except FileNotFoundError:
print(f"Error: {self.file_name} not found.")
return
except json.JSONDecodeError:
print(f"Error: {self.file_name} contains invalid JSON.")
return
items = self._load_items()
if not items:
return []

cleaned_items = [self.prepare_item(item) for item in items]

Expand All @@ -94,8 +101,8 @@ def find_problematic_collections(self) -> List[Dict[str, Any]]:
logger.info(f"Searching for documents in collection '{self.collection.name}' where '{query}'")
documents = list(self.collection.find(query))
for doc in documents:
logger.info(f"Document with _id '{doc['_id']}' is problematic")
logger.info(f"Found {len(documents)} documents to fix")
logger.info(f"Document with _id '{doc['_id']}' found")
logger.info(f"Found {len(documents)} documents to delete")
return documents
except OperationFailure as e:
logger.error(f"Failed to query documents: {e}")
Expand All @@ -106,34 +113,34 @@ def find_problematic_collections(self) -> List[Dict[str, Any]]:

def print_report(self, documents: List[Dict[str, Any]]):
if not documents:
logger.info("No problematic documents found")
logger.info("No assertions found")
return

logger.info("\n" + "="*80)
logger.info("PROBLEMATIC DOCUMENTS REPORT")
logger.info("ASSERTIONS DOCUMENTS REPORT")
logger.info("="*80)

status_counts = {}
for doc in documents:
status = doc.get('_id', 'Unknown')
status_counts[status] = status_counts.get(status, 0) + 1

logger.info(f"\nTotal documents to fix: {len(documents)}")
logger.info(f"\nTotal documents to delete: {len(documents)}")

logger.info("\n" + "="*80)

def fix_documents(self, documents: List[Dict[str, Any]]) -> int:
def delete_documents(self, documents: List[Dict[str, Any]]) -> int:
"""
Fix the status of problematic documents.
Delete the specified documents.

Returns:
Number of documents successfully updated
Number of documents successfully deleted
"""
if not documents:
logger.info("No documents to fix")
logger.info("No documents to delete")
return 0

logger.info(f"\n Applying fixes to {len(documents)} documents...")
logger.info(f"\n Deleting {len(documents)} documents...")

document_ids = [doc['_id'] for doc in documents if '_id' in doc and doc['_id'] is not None]

Expand Down Expand Up @@ -192,18 +199,56 @@ def parse_arguments():
MONGO_URI or MONGO_DB - MongoDB connection string
MONGO_DATABASE or DATABASE - Database name (default: assertionservice)
MONGO_COLLECTION or COLLECTION - Collection name (default: assertion)
FILE_NAME - Collection name (default: json/delete.json)
"""
)

parser.add_argument('--mongo-uri', help='MongoDB URI (overrides env)')
parser.add_argument('--database', help='MongoDB database name (overrides env)')
parser.add_argument('--collections', help='MongoDB collection(s) name')
parser.add_argument('--file_name', help='File that contains the documents to delete')
parser.add_argument('--member-id', dest='member_id', help='Internal member ID to clean all affiliations for')
parser.add_argument(
'--member-salesforce-id',
dest='member_salesforce_id',
help='Salesforce ID to resolve member and clean all affiliations for that member'
)

return parser.parse_args()


def resolve_member_id_from_salesforce_id(mongo_uri: str, salesforce_id: str) -> Optional[str]:
if not SALESFORCE_ID_PATTERN.match(salesforce_id):
logger.error(
"Invalid Salesforce ID: %r. Expected exactly 18 alphanumeric characters.",
salesforce_id
)
return None

members_connection = MongoDBConnection(mongo_uri, 'memberservice')
try:
if not members_connection.connect():
logger.error("Failed to connect to memberservice database to resolve Salesforce ID")
return None

member_collection = members_connection.get_collection('member')
if member_collection is None:
logger.error("Could not access memberservice.member collection")
return None

member = member_collection.find_one({'salesforce_id': salesforce_id}, {'_id': 1, 'salesforce_id': 1})
if not member:
logger.error("No member found for salesforce_id=%s", salesforce_id)
return None

member_id = str(member.get('_id'))
logger.info("Resolved member salesforce_id=%s to member_id=%s", salesforce_id, member_id)
return member_id
except Exception as e:
logger.error("Unexpected error resolving member by salesforce id: %s", e)
return None
finally:
members_connection.disconnect()


def main():
args = parse_arguments()

Expand All @@ -212,15 +257,42 @@ def main():
mongo_uri = args.mongo_uri or config.mongo_uri
database = args.database or config.mongo_database
collections = args.collections or config.mongo_collection
file_name = args.file_name or config.file_name
member_id = args.member_id
member_salesforce_id = args.member_salesforce_id

if member_id and member_salesforce_id:
logger.error("Use either --member-id or --member-salesforce-id, not both")
return 1

if not member_id and not member_salesforce_id:
logger.error("A member selector is required. Use --member-id or --member-salesforce-id")
return 1

if member_salesforce_id:
resolved_member_id = resolve_member_id_from_salesforce_id(mongo_uri, member_salesforce_id)
if not resolved_member_id:
return 1
member_id = resolved_member_id

if member_id and collections != 'assertion':
logger.warning(
"Member cleanup is intended for Affiliation Manager data in 'assertion'. "
"Current collection is '%s'.",
collections,
)

inline_filters: List[Dict[str, Any]] = []
if member_id:
inline_filters = [{'member_id': member_id}]

logger.info("="*80)
logger.info("MongoDB Delete Documents Script")
logger.info("="*80)
logger.info(f"MongoDB URI: {mongo_uri[:20]}..." if len(mongo_uri) > 20 else f"MongoDB URI: {mongo_uri}")
logger.info(f"Database: {database}")
logger.info(f"Collection: {collections}")
logger.info(f"Filename: {file_name}")
if member_id:
logger.info(f"Member ID scope: {member_id}")
logger.info("="*80 + "\n")

connection = MongoDBConnection(mongo_uri, database)
Expand All @@ -230,14 +302,14 @@ def main():
logger.error("Failed to connect to MongoDB. Exiting.")
return 1

fixer = DeleteDocuments(connection, collections, file_name)
fixer = DeleteDocuments(connection, collections, inline_filters)

documents = fixer.find_problematic_collections()

fixer.print_report(documents)

if not documents:
logger.info("\n No fixes needed. All documents are correct.")
logger.info("\n No deletions needed.")
return 0

logger.info("\n" + "="*80)
Expand All @@ -254,7 +326,7 @@ def main():
logger.info("\n\n Operation cancelled by user")
return 1

deleted_count = fixer.fix_documents(documents)
deleted_count = fixer.delete_documents(documents)

if deleted_count > 0:
if not fixer.verify_fixes():
Expand Down
Loading
Loading