"""Neo4j queries for contributors (LabelParticipant nodes).""" from typing import Any from ddtrace import tracer from neo4j import Session from pydantic import UUID4 get_contributors_by_uuids = """ UNWIND $uuids AS uuid MATCH (lp:LabelParticipant {uuid: uuid}) WHERE $is_admin OR EXISTS { MATCH (p:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->() -[:HAS_LABEL_PARTICIPANT]->(lp) WHERE p.profileId IN $profile_ids } WITH DISTINCT lp OPTIONAL MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(:Vendor)-[:HAS_ARTIST]->(ai:ArtistInfo) WHERE lp.normalizedName = toLower(trim(apoc.text.regreplace(ai.name, '[ ]+', ''))) WITH lp, collect(DISTINCT ai.id) AS artistInfoIds OPTIONAL MATCH (lp)<-[:REPRESENTS]-(gp:GlobalParticipant) OPTIONAL MATCH (gp)-[:REPRESENTS]->(:Participant:Chartmetric) -[:HAS_AGGREGATED_PARTICIPANT_SOCIAL_STAT]->(stat:AggregatedParticipantSocialStat) WITH lp, artistInfoIds, gp, max(toInteger(stat.monthlyListeners)) AS monthlyListeners MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(vendor:Vendor) OPTIONAL MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(subaccount:Subaccount) RETURN lp, artistInfoIds, gp, monthlyListeners, CASE WHEN subaccount IS NOT NULL THEN { uuid: subaccount.uuid, vendorId: coalesce(vendor.vendorId, lp.vendorId), ownerVendorId: coalesce(vendor.vendorId, lp.vendorId), subaccountId: subaccount.subaccountId } ELSE { uuid: vendor.uuid, vendorId: coalesce(vendor.vendorId, lp.vendorId), ownerVendorId: coalesce(vendor.vendorId, lp.vendorId), subaccountId: 0 } END AS label """ get_label_for_contributor = """ MATCH (lp:LabelParticipant {uuid: $uuid}) MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(vendor:Vendor) OPTIONAL MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(subaccount:Subaccount) WITH lp, CASE WHEN subaccount IS NOT NULL THEN subaccount ELSE vendor END AS access_label, CASE WHEN subaccount IS NOT NULL THEN { uuid: subaccount.uuid, vendorId: coalesce(vendor.vendorId, lp.vendorId), ownerVendorId: coalesce(vendor.vendorId, lp.vendorId), subaccountId: subaccount.subaccountId } ELSE { uuid: vendor.uuid, vendorId: coalesce(vendor.vendorId, lp.vendorId), ownerVendorId: coalesce(vendor.vendorId, lp.vendorId), subaccountId: 0 } END AS label WHERE $is_admin OR EXISTS { MATCH (userProfile:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->(hat)-[:OWNS*0..1]->(access_label) WHERE userProfile.profileId IN $profile_ids } RETURN DISTINCT label """ get_global_participant_by_id = """ MATCH (gp:GlobalParticipant {id: $id}) WHERE gp.name <> "Various Artists" AND NOT (gp)-[:MERGED_TO]->(:GlobalParticipant) AND ($is_admin OR EXISTS { MATCH (gp)-[:REPRESENTS]->(lp:LabelParticipant) <-[:HAS_LABEL_PARTICIPANT]-()<-[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO] -(p:Profile {profileType: 'LabelProfile'}) WHERE p.profileId IN $profile_ids }) OPTIONAL MATCH (gp)-[:REPRESENTS]->(:Participant:Chartmetric) -[:HAS_AGGREGATED_PARTICIPANT_SOCIAL_STAT]->(stat:AggregatedParticipantSocialStat) WITH gp, max(toInteger(stat.monthlyListeners)) AS monthlyListeners RETURN gp, monthlyListeners """ get_global_participants_by_ids = """ UNWIND $ids AS id MATCH (gp:GlobalParticipant {id: id}) WHERE gp.name <> "Various Artists" AND NOT (gp)-[:MERGED_TO]->(:GlobalParticipant) AND ($is_admin OR EXISTS { MATCH (gp)-[:REPRESENTS]->(lp:LabelParticipant) <-[:HAS_LABEL_PARTICIPANT]-()<-[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO] -(p:Profile {profileType: 'LabelProfile'}) WHERE p.profileId IN $profile_ids }) WITH DISTINCT gp OPTIONAL MATCH (gp)-[:REPRESENTS]->(:Participant:Chartmetric) -[:HAS_AGGREGATED_PARTICIPANT_SOCIAL_STAT]->(stat:AggregatedParticipantSocialStat) WITH gp, max(toInteger(stat.monthlyListeners)) AS monthlyListeners RETURN gp, monthlyListeners """ contributors_by_global_participant_id = """ MATCH (gp:GlobalParticipant {id: $global_participant_id}) WHERE gp.name <> "Various Artists" AND NOT (gp)-[:MERGED_TO]->(:GlobalParticipant) OPTIONAL MATCH (gp)-[:REPRESENTS]->(lp:LabelParticipant) WHERE lp IS NULL OR $is_admin OR EXISTS { MATCH (p:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->() -[:HAS_LABEL_PARTICIPANT]->(lp) WHERE p.profileId IN $profile_ids } WITH gp, lp OPTIONAL MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(vendor:Vendor) OPTIONAL MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(subaccount:Subaccount) WITH gp, lp, vendor, subaccount, CASE WHEN lp IS NULL THEN NULL WHEN subaccount IS NOT NULL THEN { uuid: subaccount.uuid, vendorId: coalesce(vendor.vendorId, lp.vendorId), ownerVendorId: coalesce(vendor.vendorId, lp.vendorId), subaccountId: subaccount.subaccountId } ELSE { uuid: vendor.uuid, vendorId: coalesce(vendor.vendorId, lp.vendorId), ownerVendorId: coalesce(vendor.vendorId, lp.vendorId), subaccountId: coalesce(lp.subaccountId, 0) } END AS label OPTIONAL MATCH (gp)-[:REPRESENTS]->(:Participant:Chartmetric) -[:HAS_AGGREGATED_PARTICIPANT_SOCIAL_STAT]->(stat:AggregatedParticipantSocialStat) WITH gp, lp, label, max(toInteger(stat.monthlyListeners)) AS gp_monthly_listeners ORDER BY lp.uuid RETURN gp.id AS global_participant_id, collect(CASE WHEN lp IS NOT NULL THEN {lp: lp, label: label, gp: gp, gp_monthly_listeners: gp_monthly_listeners} END) AS contributors """ create_contributor = """ WITH $created_by AS by, toInteger(rand() * datetime.realtime().epochMillis) AS lpID MERGE ( lp:LabelParticipant:Orchard { normalizedName: toLower(trim(apoc.text.regreplace($name, '[ ]+', ''))), vendorId: toInteger($vendor_id), subaccountId: COALESCE(toInteger($subaccount_id), 0) } ) ON CREATE SET lp.id = lpID, lp.name = trim(apoc.text.regreplace($name, '[ ]{2,}', ' ')), lp.uuid = randomUUID(), lp.createdAt = datetime(), lp.createdBy = by SET lp.spotifyId = COALESCE($spotify_id, lp.spotifyId), lp.appleMusicId = $apple_music_id, lp.spotifyArtistKey = $spotify_artist_key, lp.isni = $isni, lp.lastModifiedAt = datetime(), lp.lastModifiedBy = by WITH * MATCH (vendor:Vendor {vendorId: toInteger($vendor_id)}) OPTIONAL MATCH (sa:SubAccount)<-[:OWNS]-(vendor) WHERE sa.subaccountId = toInteger($subaccount_id) AND $subaccount_id IS NOT NULL WITH *, COALESCE(sa, vendor) AS label MERGE (label)-[:HAS_LABEL_PARTICIPANT]->(lp) FOREACH (isSubAccount IN CASE WHEN sa IS NOT NULL THEN [1] ELSE [] END | MERGE (vendor)-[:HAS_LABEL_PARTICIPANT]->(lp) ) WITH * FOREACH (hasSpotifyId IN CASE WHEN lp.spotifyId IS NOT NULL THEN [1] ELSE [] END | MERGE (gp:GlobalParticipant {spotifyId: lp.spotifyId}) ON CREATE SET gp.id = randomUUID(), gp.name = lp.name, gp.appleMusicId = lp.appleMusicId, gp.createdAt = datetime(), gp.createdBy = by MERGE (gp)-[gpToLp:REPRESENTS]->(lp) ON CREATE SET gpToLp.createdAt = datetime(), gpToLp.createdBy = by ) WITH * OPTIONAL MATCH (oldGp:GlobalParticipant)-[oldRepresents:REPRESENTS]->(lp) WHERE oldGp.spotifyId <> lp.spotifyId FOREACH (hasOldGp IN CASE WHEN oldGp IS NOT NULL THEN [1] ELSE [] END | MERGE (oldGp)-[deletedRepresents:DELETED_REPRESENTS]->(lp) SET deletedRepresents.deletedAt = datetime(), deletedRepresents.deletedBy = by DELETE oldRepresents ) WITH * OPTIONAL MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(v:Vendor) OPTIONAL MATCH (v)<-[:HAS_LABEL]-(cb:CompanyBrand)-[:BELONGS_TO]->(pc:ParentCompany) OPTIONAL MATCH (lp)<-[:HAS_LABEL_PARTICIPANT]-(sa:SubAccount) SET lp.companyBrandUUID = cb.uuid, lp.vendorUUID = v.uuid, lp.subaccountUUID = sa.uuid, lp.parentCompanyUUID = pc.uuid RETURN DISTINCT lp """ update_contributor = """ WITH $updated_by AS by MATCH (lp:LabelParticipant {uuid: $uuid}) WHERE $is_admin OR EXISTS { MATCH (p:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->() -[:HAS_LABEL_PARTICIPANT]->(lp) WHERE p.profileId IN $profile_ids } SET lp.spotifyId = COALESCE($spotify_id, lp.spotifyId), lp.appleMusicId = $apple_music_id, lp.spotifyArtistKey = $spotify_artist_key, lp.isni = $isni, lp.lastModifiedAt = datetime(), lp.lastModifiedBy = by WITH * FOREACH (hasSpotifyId IN CASE WHEN lp.spotifyId IS NOT NULL THEN [1] ELSE [] END | MERGE (gp:GlobalParticipant {spotifyId: lp.spotifyId}) ON CREATE SET gp.id = randomUUID(), gp.name = lp.name, gp.appleMusicId = lp.appleMusicId, gp.createdAt = datetime(), gp.createdBy = by MERGE (gp)-[gpToLp:REPRESENTS]->(lp) ON CREATE SET gpToLp.createdAt = datetime(), gpToLp.createdBy = by ON MATCH SET gpToLp.lastModifiedAt = datetime(), gpToLp.lastModifiedBy = by ) WITH * OPTIONAL MATCH (oldGp:GlobalParticipant)-[oldRepresents:REPRESENTS]->(lp) WHERE oldGp.spotifyId <> lp.spotifyId OR COALESCE(lp.spotifyId, '') = '' FOREACH (hasOldGp IN CASE WHEN oldGp IS NOT NULL THEN [1] ELSE [] END | MERGE (oldGp)-[deletedRepresents:DELETED_REPRESENTS]->(lp) SET deletedRepresents.deletedAt = datetime(), deletedRepresents.deletedBy = by DELETE oldRepresents ) RETURN DISTINCT lp """ rename_contributor = """ MATCH (lp:LabelParticipant {uuid: $uuid}) WHERE $is_admin OR EXISTS { MATCH (p:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->() -[:HAS_LABEL_PARTICIPANT]->(lp) WHERE p.profileId IN $profile_ids } SET lp.name = $name, lp.lastModifiedBy = $updated_by, lp.normalizedName = toLower(replace($name, ' ', '')), lp.lastModifiedAt = datetime() RETURN lp as node """ merge_contributors = """ MATCH (contributor:LabelParticipant {uuid: $contributor_uuid}) MATCH (duplicate:LabelParticipant {uuid: $duplicate_uuid}) WHERE $is_admin OR ( EXISTS { MATCH (p:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->(v:Vendor) -[:HAS_LABEL_PARTICIPANT]->(contributor) WHERE p.profileId IN $profile_ids } AND EXISTS { MATCH (p2:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->(v2:Vendor) -[:HAS_LABEL_PARTICIPANT]->(duplicate) WHERE p2.profileId IN $profile_ids } ) CALL apoc.refactor.mergeNodes( [contributor, duplicate], { properties: "discard", mergeRels: true } ) YIELD node SET contributor.updatedBy = $updated_by, contributor.updatedAt = datetime(), contributor.normalizedName = toLower(replace(contributor.name, ' ', '')) RETURN contributor as node """ check_vendor_access_query = """ MATCH (p:Profile {profileType: 'LabelProfile'}) -[:HAS_ACCESS_TO|HAS_ADMIN_ACCESS_TO]->(v:Vendor {vendorId: toInteger($vendor_id)}) WHERE p.profileId IN $profile_ids RETURN count(v) > 0 AS authorized """ full_catalog_access_query = """ MATCH (p:Profile) WHERE p.fullCatalogAccess = true AND ANY(prof IN $profiles WHERE p.profileType = prof.profile_type AND p.profileId = toInteger(prof.profile_id)) RETURN count(p) > 0 AS has_full_access """ @tracer.wrap() def has_full_catalog_access(session: Session, profiles: list[dict]) -> bool: result = session.run(full_catalog_access_query, profiles=profiles) record = result.single() return bool(record["has_full_access"]) if record else False @tracer.wrap() def check_vendor_access( session: Session, profile_ids: list[int], vendor_id: int ) -> bool: result = session.run( check_vendor_access_query, profile_ids=profile_ids, vendor_id=vendor_id ) record = result.single() return bool(record["authorized"]) if record else False def _str_or_none(value) -> str | None: return str(value) if value is not None else None def _gp_node_to_key(gp) -> dict[str, Any] | None: """Return only the ID key for a global participant.""" if gp is None: return None return { "id": gp["id"], } def _label_node_to_dict(label) -> dict[str, Any] | None: if label is None: return None vendor_id = label.get("vendorId") if vendor_id is None: vendor_id = label.get("ownerVendorId") if vendor_id is None: vendor_id = label.get("vendor_id") subaccount_id = label.get("subaccountId") if subaccount_id is None: subaccount_id = label.get("subaccount_id", 0) return { "uuid": label.get("uuid"), "vendor_id": vendor_id, "subaccount_id": subaccount_id, } def _node_to_dict( lp, artist_info_ids: list[int] | None = None, gp=None, label=None, gp_monthly_listeners: int | None = None, ) -> dict[str, Any]: contributor = { "id": lp["id"], "uuid": lp["uuid"], "name": lp["name"], "spotify_id": _str_or_none(lp.get("spotifyId")), "apple_music_id": _str_or_none(lp.get("appleMusicId")), "spotify_artist_key": _str_or_none(lp.get("spotifyArtistKey")), "isni": _str_or_none(lp.get("isni")), "company_brand_uuid": lp.get("companyBrandUUID"), "parent_company_uuid": lp.get("parentCompanyUUID"), "artist_info_ids": artist_info_ids or [], "global_participant": _gp_node_to_key(gp), "label": _label_node_to_dict(label), } return contributor @tracer.wrap() def get_by_uuids( session: Session, uuids: list[str], profile_ids: list[int], is_admin: bool = False, ) -> list[dict[str, Any]]: result = session.run( get_contributors_by_uuids, uuids=uuids, profile_ids=profile_ids, is_admin=is_admin, ) contributors = [] for record in result: try: monthly_listeners = record["monthlyListeners"] except KeyError, TypeError: monthly_listeners = None contributors.append( _node_to_dict( record["lp"], artist_info_ids=record["artistInfoIds"], gp=record["gp"], gp_monthly_listeners=monthly_listeners, label=record["label"], ) ) return contributors @tracer.wrap() def get_label_by_contributor_uuid( session: Session, *, uuid: str, profile_ids: list[int], is_admin: bool = False, ) -> dict[str, Any] | None: result = session.run( get_label_for_contributor, uuid=uuid, profile_ids=profile_ids, is_admin=is_admin, ) record = result.single() return dict(record["label"]) if record and record["label"] else None @tracer.wrap() def create( session: Session, vendor_id: int, subaccount_id: int | None, name: str, spotify_id: str | None, apple_music_id: str | None, spotify_artist_key: str | None, isni: str | None, global_participant_uuid: str | None, created_by: str, ) -> dict: result = session.run( create_contributor, vendor_id=vendor_id, subaccount_id=subaccount_id, name=name, spotify_id=spotify_id, apple_music_id=apple_music_id, spotify_artist_key=spotify_artist_key, isni=isni, created_by=created_by, ) record = result.single() if record is None: raise RuntimeError(f"Failed to create contributor for name: {name!r}") return _node_to_dict( record["lp"], label=get_label_by_contributor_uuid( session, uuid=record["lp"]["uuid"], profile_ids=[], is_admin=True ), ) @tracer.wrap() def update( session: Session, uuid: str, profile_ids: list[int], spotify_id: str | None, apple_music_id: str | None, spotify_artist_key: str | None, isni: str | None, global_participant_uuid: str | None, updated_by: str, is_admin: bool = False, ) -> dict | None: result = session.run( update_contributor, uuid=uuid, profile_ids=profile_ids, is_admin=is_admin, spotify_id=spotify_id, apple_music_id=apple_music_id, spotify_artist_key=spotify_artist_key, isni=isni, updated_by=updated_by, ) record = result.single() return ( _node_to_dict( record["lp"], label=get_label_by_contributor_uuid( session, uuid=record["lp"]["uuid"], profile_ids=profile_ids, is_admin=is_admin, ), ) if record else None ) @tracer.wrap() def get_gp_by_id( session: Session, id: str, profile_ids: list[int], is_admin: bool = False ) -> dict[str, Any] | None: from contributor.queries.neo4j import ( global_participants as global_participant_queries, ) return global_participant_queries.get_by_id( session=session, id=id, profile_ids=profile_ids, is_admin=is_admin, ) @tracer.wrap() def get_gp_by_ids( session: Session, ids: list[str], profile_ids: list[int], is_admin: bool = False ) -> list[dict[str, Any]]: from contributor.queries.neo4j import ( global_participants as global_participant_queries, ) return global_participant_queries.get_by_ids( session=session, ids=ids, profile_ids=profile_ids, is_admin=is_admin, ) @tracer.wrap() def get_contributors_for_global_participant( session: Session, global_participant_id: str, profile_ids: list[int], is_admin: bool = False, ) -> list[dict[str, Any]] | None: from contributor.queries.neo4j import ( global_participants as global_participant_queries, ) return global_participant_queries.get_contributors( session=session, global_participant_id=global_participant_id, profile_ids=profile_ids, is_admin=is_admin, ) @tracer.wrap() def rename( session: Session, uuid: str, name: str, updated_by: str, profile_ids: list[int], is_admin: bool = False, ) -> dict[str, Any] | None: result = session.run( rename_contributor, uuid=uuid, name=name, updated_by=updated_by, profile_ids=profile_ids, is_admin=is_admin, ) record = result.single() return ( _node_to_dict( record["node"], label=get_label_by_contributor_uuid( session, uuid=record["node"]["uuid"], profile_ids=profile_ids, is_admin=is_admin, ), ) if record else None ) @tracer.wrap() def merge( session: Session, contributor_uuid: UUID4, duplicate_uuid: UUID4, updated_by: str, profile_ids: list[int], is_admin: bool = False, ) -> dict[str, Any] | None: result = session.run( merge_contributors, contributor_uuid=str(contributor_uuid), duplicate_uuid=str(duplicate_uuid), updated_by=updated_by, profile_ids=profile_ids, is_admin=is_admin, ) record = result.single() return ( _node_to_dict( record["node"], label=get_label_by_contributor_uuid( session, uuid=record["node"]["uuid"], profile_ids=profile_ids, is_admin=is_admin, ), ) if record else None )