|
5 | 5 |
|
6 | 6 | from crowdgit.enums import RepositoryPriority, RepositoryState |
7 | 7 | from crowdgit.errors import RepoLockingError |
| 8 | +from crowdgit.models.affiliation_info import RepoAffiliationRegistry |
8 | 9 | from crowdgit.models.repository import Repository |
9 | 10 | from crowdgit.models.service_execution import ServiceExecution |
10 | 11 | from crowdgit.settings import ( |
@@ -552,3 +553,303 @@ async def save_service_execution(service_execution: ServiceExecution) -> None: |
552 | 553 | f"error: {e}" |
553 | 554 | ) |
554 | 555 | # Do not re-raise - we don't want metrics saving to disrupt main operations |
| 556 | + |
| 557 | + |
| 558 | +async def get_repo_affiliation_registry(repo_id: str) -> RepoAffiliationRegistry | None: |
| 559 | + sql_query = """ |
| 560 | + SELECT "repoId", "filePath", "fileHash", "status", "snapshot", "lastRunAt" |
| 561 | + FROM git."repoAffiliationRegistry" |
| 562 | + WHERE "repoId" = $1 |
| 563 | + """ |
| 564 | + result = await fetchrow(sql_query, (repo_id,)) |
| 565 | + if not result: |
| 566 | + return None |
| 567 | + |
| 568 | + return RepoAffiliationRegistry.from_db(dict(result)) |
| 569 | + |
| 570 | + |
| 571 | +async def upsert_repo_affiliation_registry(registry: RepoAffiliationRegistry) -> None: |
| 572 | + snapshot_json = registry.snapshot_for_db() |
| 573 | + sql_query = """ |
| 574 | + INSERT INTO git."repoAffiliationRegistry" ( |
| 575 | + "repoId", "filePath", "fileHash", "status", "snapshot", "lastRunAt", "updatedAt" |
| 576 | + ) |
| 577 | + VALUES ($1, $2, $3, $4, $5::jsonb, NOW(), NOW()) |
| 578 | + ON CONFLICT ("repoId") DO UPDATE SET |
| 579 | + "filePath" = EXCLUDED."filePath", |
| 580 | + "fileHash" = EXCLUDED."fileHash", |
| 581 | + "status" = EXCLUDED."status", |
| 582 | + "snapshot" = EXCLUDED."snapshot", |
| 583 | + "lastRunAt" = NOW(), |
| 584 | + "updatedAt" = NOW() |
| 585 | + """ |
| 586 | + await execute( |
| 587 | + sql_query, |
| 588 | + ( |
| 589 | + registry.repo_id, |
| 590 | + registry.file_path, |
| 591 | + registry.file_hash, |
| 592 | + registry.status, |
| 593 | + snapshot_json, |
| 594 | + ), |
| 595 | + ) |
| 596 | + |
| 597 | + |
| 598 | +async def find_many_member_ids_by_identities(identities: list[dict]) -> list[dict]: |
| 599 | + if not identities: |
| 600 | + return [] |
| 601 | + |
| 602 | + values_parts: list[str] = [] |
| 603 | + params: list[str | bool | int] = [] |
| 604 | + param_index = 1 |
| 605 | + for idx, identity in enumerate(identities): |
| 606 | + values_parts.append( |
| 607 | + f"(${param_index}::int, ${param_index + 1}::text, ${param_index + 2}::boolean," |
| 608 | + f" ${param_index + 3}::text, ${param_index + 4}::text)" |
| 609 | + ) |
| 610 | + params.extend( |
| 611 | + [ |
| 612 | + idx, |
| 613 | + identity["type"], |
| 614 | + identity.get("verified", True), |
| 615 | + identity.get("platform"), |
| 616 | + identity["value"], |
| 617 | + ] |
| 618 | + ) |
| 619 | + param_index += 5 |
| 620 | + |
| 621 | + matches_by_idx: dict[int, set[str]] = {} |
| 622 | + rows = await query( |
| 623 | + f""" |
| 624 | + WITH input_identities (idx, identity_type, verified, platform, value) AS ( |
| 625 | + VALUES {", ".join(values_parts)} |
| 626 | + ) |
| 627 | + SELECT i.idx, mi."memberId" |
| 628 | + FROM input_identities i |
| 629 | + LEFT JOIN "memberIdentities" mi |
| 630 | + ON mi.type = i.identity_type |
| 631 | + AND mi.verified = i.verified |
| 632 | + AND lower(mi.value) = lower(i.value) |
| 633 | + AND mi.platform = i.platform |
| 634 | + AND mi."deletedAt" IS NULL |
| 635 | + ORDER BY i.idx |
| 636 | + """, |
| 637 | + tuple(params), |
| 638 | + ) |
| 639 | + for row in rows: |
| 640 | + if row["memberId"] is None: |
| 641 | + continue |
| 642 | + matches_by_idx.setdefault(row["idx"], set()).add(str(row["memberId"])) |
| 643 | + |
| 644 | + results: list[dict] = [] |
| 645 | + for idx, identity in enumerate(identities): |
| 646 | + member_ids = matches_by_idx.get(idx, set()) |
| 647 | + member_id = next(iter(member_ids)) if len(member_ids) == 1 else None |
| 648 | + results.append( |
| 649 | + { |
| 650 | + "type": identity["type"], |
| 651 | + "platform": identity.get("platform"), |
| 652 | + "value": identity["value"], |
| 653 | + "verified": identity.get("verified", True), |
| 654 | + "member_id": member_id, |
| 655 | + } |
| 656 | + ) |
| 657 | + |
| 658 | + return results |
| 659 | + |
| 660 | + |
| 661 | +async def find_many_organization_ids_by_identities(identities: list[dict]) -> list[dict]: |
| 662 | + if not identities: |
| 663 | + return [] |
| 664 | + |
| 665 | + values_parts: list[str] = [] |
| 666 | + params: list[str | bool | int] = [] |
| 667 | + param_index = 1 |
| 668 | + for idx, identity in enumerate(identities): |
| 669 | + values_parts.append( |
| 670 | + f"(${param_index}::int, ${param_index + 1}::text, ${param_index + 2}::boolean," |
| 671 | + f" ${param_index + 3}::text, ${param_index + 4}::text)" |
| 672 | + ) |
| 673 | + params.extend( |
| 674 | + [ |
| 675 | + idx, |
| 676 | + identity["type"], |
| 677 | + identity.get("verified", True), |
| 678 | + identity["platform"], |
| 679 | + identity["value"], |
| 680 | + ] |
| 681 | + ) |
| 682 | + param_index += 5 |
| 683 | + |
| 684 | + matches_by_idx: dict[int, set[str]] = {} |
| 685 | + rows = await query( |
| 686 | + f""" |
| 687 | + WITH input_identities (idx, identity_type, verified, platform, value) AS ( |
| 688 | + VALUES {", ".join(values_parts)} |
| 689 | + ) |
| 690 | + SELECT i.idx, oi."organizationId" |
| 691 | + FROM input_identities i |
| 692 | + LEFT JOIN "organizationIdentities" oi |
| 693 | + ON oi.type = i.identity_type |
| 694 | + AND oi.verified = i.verified |
| 695 | + AND oi.platform = i.platform |
| 696 | + AND lower(oi.value) = lower(i.value) |
| 697 | + ORDER BY i.idx |
| 698 | + """, |
| 699 | + tuple(params), |
| 700 | + ) |
| 701 | + for row in rows: |
| 702 | + if row["organizationId"] is None: |
| 703 | + continue |
| 704 | + matches_by_idx.setdefault(row["idx"], set()).add(str(row["organizationId"])) |
| 705 | + |
| 706 | + results: list[dict] = [] |
| 707 | + for idx, identity in enumerate(identities): |
| 708 | + organization_ids = matches_by_idx.get(idx, set()) |
| 709 | + organization_id = next(iter(organization_ids)) if len(organization_ids) == 1 else None |
| 710 | + results.append( |
| 711 | + { |
| 712 | + "type": identity["type"], |
| 713 | + "platform": identity["platform"], |
| 714 | + "value": identity["value"], |
| 715 | + "verified": identity.get("verified", True), |
| 716 | + "organization_id": organization_id, |
| 717 | + } |
| 718 | + ) |
| 719 | + |
| 720 | + return results |
| 721 | + |
| 722 | + |
| 723 | +async def fetch_member_organizations(member_ids: list[str]) -> list[dict]: |
| 724 | + if not member_ids: |
| 725 | + return [] |
| 726 | + |
| 727 | + return await query( |
| 728 | + """ |
| 729 | + SELECT "memberId", "organizationId", "dateStart", "dateEnd", source |
| 730 | + FROM "memberOrganizations" |
| 731 | + WHERE "memberId" = ANY($1::uuid[]) |
| 732 | + AND "deletedAt" IS NULL |
| 733 | + """, |
| 734 | + (member_ids,), |
| 735 | + ) |
| 736 | + |
| 737 | + |
| 738 | +async def fetch_segment_affiliations(member_ids: list[str], segment_id: str) -> list[dict]: |
| 739 | + """MSA rows are per segment — filter by segment_id so guards match this repo's project.""" |
| 740 | + if not member_ids: |
| 741 | + return [] |
| 742 | + |
| 743 | + return await query( |
| 744 | + """ |
| 745 | + SELECT "memberId", "segmentId", "organizationId", "dateStart", "dateEnd", verified |
| 746 | + FROM "memberSegmentAffiliations" |
| 747 | + WHERE "memberId" = ANY($1::uuid[]) |
| 748 | + AND "segmentId" = $2::uuid |
| 749 | + AND "deletedAt" IS NULL |
| 750 | + AND "organizationId" IS NOT NULL |
| 751 | + """, |
| 752 | + (member_ids, segment_id), |
| 753 | + ) |
| 754 | + |
| 755 | + |
| 756 | +async def insert_member_organizations(rows: list[dict]) -> None: |
| 757 | + if not rows: |
| 758 | + return |
| 759 | + |
| 760 | + undated_rows: list[tuple] = [] |
| 761 | + open_ended_rows: list[tuple] = [] |
| 762 | + dated_rows: list[tuple] = [] |
| 763 | + |
| 764 | + for row in rows: |
| 765 | + params = ( |
| 766 | + row["member_id"], |
| 767 | + row["organization_id"], |
| 768 | + row.get("date_start"), |
| 769 | + row.get("date_end"), |
| 770 | + row["source"], |
| 771 | + ) |
| 772 | + date_start = row.get("date_start") |
| 773 | + date_end = row.get("date_end") |
| 774 | + if date_start is None and date_end is None: |
| 775 | + undated_rows.append(params) |
| 776 | + elif date_end is None: |
| 777 | + open_ended_rows.append(params) |
| 778 | + else: |
| 779 | + dated_rows.append(params) |
| 780 | + |
| 781 | + insert_sql = """ |
| 782 | + INSERT INTO "memberOrganizations"( |
| 783 | + "memberId", |
| 784 | + "organizationId", |
| 785 | + "dateStart", |
| 786 | + "dateEnd", |
| 787 | + title, |
| 788 | + source, |
| 789 | + "createdAt", |
| 790 | + "updatedAt" |
| 791 | + ) |
| 792 | + VALUES ($1, $2, $3, $4, NULL, $5, NOW(), NOW()) |
| 793 | + """ |
| 794 | + |
| 795 | + if undated_rows: |
| 796 | + sql = ( |
| 797 | + insert_sql |
| 798 | + + """ |
| 799 | + ON CONFLICT ("memberId", "organizationId") |
| 800 | + WHERE ("dateStart" IS NULL AND "dateEnd" IS NULL AND "deletedAt" IS NULL) |
| 801 | + DO NOTHING |
| 802 | + """ |
| 803 | + ) |
| 804 | + await executemany(sql, undated_rows) |
| 805 | + |
| 806 | + if open_ended_rows: |
| 807 | + sql = ( |
| 808 | + insert_sql |
| 809 | + + """ |
| 810 | + ON CONFLICT ("memberId", "organizationId", "dateStart") |
| 811 | + WHERE ("dateEnd" IS NULL AND "deletedAt" IS NULL) |
| 812 | + DO NOTHING |
| 813 | + """ |
| 814 | + ) |
| 815 | + await executemany(sql, open_ended_rows) |
| 816 | + |
| 817 | + if dated_rows: |
| 818 | + sql = ( |
| 819 | + insert_sql |
| 820 | + + """ |
| 821 | + ON CONFLICT ("memberId", "organizationId", "dateStart", "dateEnd") |
| 822 | + WHERE ("deletedAt" IS NULL) |
| 823 | + DO NOTHING |
| 824 | + """ |
| 825 | + ) |
| 826 | + await executemany(sql, dated_rows) |
| 827 | + |
| 828 | + |
| 829 | +async def insert_member_segment_affiliations(rows: list[dict]) -> None: |
| 830 | + if not rows: |
| 831 | + return |
| 832 | + |
| 833 | + await executemany( |
| 834 | + """ |
| 835 | + INSERT INTO "memberSegmentAffiliations"( |
| 836 | + id, |
| 837 | + "memberId", |
| 838 | + "segmentId", |
| 839 | + "organizationId", |
| 840 | + "dateStart", |
| 841 | + "dateEnd" |
| 842 | + ) |
| 843 | + VALUES (gen_random_uuid(), $1, $2, $3, $4, $5) |
| 844 | + """, |
| 845 | + [ |
| 846 | + ( |
| 847 | + row["member_id"], |
| 848 | + row["segment_id"], |
| 849 | + row["organization_id"], |
| 850 | + row.get("date_start"), |
| 851 | + row.get("date_end"), |
| 852 | + ) |
| 853 | + for row in rows |
| 854 | + ], |
| 855 | + ) |
0 commit comments