Repository: artsy/metaphysics
Pull request #6588: feat: Add query to fetch single partner contact
Author: login: jpotts244
Created: 2025-04-03T00:22:55Z | Merged: 2025-04-03T18:51:15Z

Welp I didnt get too far sketching out the [edit functionality in volt](https://github.com/artsy/volt/compare/jpotts244-contact-delete-edit?expand=1)

Adding the ability to fetch a singular partner contact ✅  
Will need this to rehydrate the contact records on the front end after a user edits a record + we dont have to use the larger `contactsConnection` query only in Volt 📈 

--- 


```graphql
{
  partner(id: "commerce-test-partner") {
    internalID
    contact(contactId: "4820b8cb-983f-4cf8-a35b-90e0f7954d1a") {
      internalID
      name
      email
      phone
      position
      canContact
      location {
        internalID
        display
      }
    }
  }
}

```


<img width="1481" alt="Screenshot 2025-04-02 at 8 21 25 PM" src="https://github.com/user-attachments/assets/2c7590aa-8780-4a74-b347-69523a56a408" />




Diff:
diff --git a/_schemaV2.graphql b/_schemaV2.graphql
index 3f97f4f144..301055dc34 100644
--- a/_schemaV2.graphql
+++ b/_schemaV2.graphql
@@ -16321,6 +16321,12 @@ type Partner implements Node {
   claimed: Boolean
   collectingInstitution: String
 
+  # A Singular Contact belonging to the Partner
+  contact(
+    # The slug or ID of the Contact
+    contactId: String!
+  ): Contact
+
   # A connection of contacts from a Partner.
   contactsConnection(
     after: String
diff --git a/src/lib/loaders/loaders_with_authentication/gravity.ts b/src/lib/loaders/loaders_with_authentication/gravity.ts
index 65b3d517ac..ac041f94bd 100644
--- a/src/lib/loaders/loaders_with_authentication/gravity.ts
+++ b/src/lib/loaders/loaders_with_authentication/gravity.ts
@@ -900,6 +900,12 @@ export default (accessToken, userID, opts) => {
       {},
       { headers: true }
     ),
+    partnerContactLoader: gravityLoader<
+      any,
+      { partnerId: string; contactId: string }
+    >(
+      ({ partnerId, contactId }) => `partner/${partnerId}/contact/${contactId}`
+    ),
     partnerContactsLoader: gravityLoader(
       (id) => `partner/${id}/contacts`,
       {},
diff --git a/src/schema/v2/partner/__tests__/partner.test.js b/src/schema/v2/partner/__tests__/partner.test.js
index 9b6e1300fb..7e0c011cba 100644
--- a/src/schema/v2/partner/__tests__/partner.test.js
+++ b/src/schema/v2/partner/__tests__/partner.test.js
@@ -509,6 +509,62 @@ describe("Partner type", () => {
     })
   })
 
+  describe("#contact", () => {
+    let singleContactResponse
+
+    const partnerContactLoader = jest.fn(() => {
+      return Promise.resolve(singleContactResponse)
+    })
+
+    const partnerLoader = jest.fn(() => {
+      return Promise.resolve(partnerData)
+    })
+
+    beforeEach(() => {
+      singleContactResponse = {
+        id: "contact-1",
+        name: "Molly D",
+        email: "md@artsy.net",
+        partner_location: { display: "123 Happy St, NY NY" },
+      }
+    })
+
+    it("returns a given partner contact", async () => {
+      context = {
+        partnerContactLoader,
+        partnerLoader,
+      }
+
+      const query = `
+        {
+          partner(id:"bau-xi-gallery") {
+            contact(contactId: "contact-1") {
+              name
+              email
+              location {
+                display
+              }
+            }
+          }
+        }
+      `
+
+      const data = await runAuthenticatedQuery(query, context)
+
+      expect(data).toEqual({
+        partner: {
+          contact: {
+            name: "Molly D",
+            email: "md@artsy.net",
+            location: {
+              display: "123 Happy St, NY NY",
+            },
+          },
+        },
+      })
+    })
+  })
+
   describe("#contactsConnection", () => {
     let contactsResponse
 
diff --git a/src/schema/v2/partner/partner.ts b/src/schema/v2/partner/partner.ts
index 61de2c314e..ca333ade6d 100644
--- a/src/schema/v2/partner/partner.ts
+++ b/src/schema/v2/partner/partner.ts
@@ -60,7 +60,7 @@ import {
   ViewingRoomsConnection,
   ViewingRoomStatusEnum,
 } from "../viewingRoomConnection"
-import { contactsConnection } from "schema/v2/Contacts"
+import { contactsConnection, ContactType } from "schema/v2/Contacts"
 
 const isFairOrganizer = (type) => type === "FairOrganizer"
 const isGallery = (type) => type === "PartnerGallery"
@@ -853,6 +853,28 @@ export const PartnerType = new GraphQLObjectType<any, ResolverContext>({
           return Array.from(new Set(cities)).filter(Boolean)
         },
       },
+      contact: {
+        type: ContactType,
+        description: "A Singular Contact belonging to the Partner",
+        args: {
+          contactId: {
+            type: new GraphQLNonNull(GraphQLString),
+            description: "The slug or ID of the Contact",
+          },
+        },
+        resolve: async ({ id }, args, { partnerContactLoader }) => {
+          const { contactId } = args
+
+          if (!partnerContactLoader) return null
+
+          const contact = await partnerContactLoader({
+            partnerId: id,
+            contactId: contactId,
+          })
+
+          return contact
+        },
+      },
       defaultProfileID: {
         type: GraphQLString,
         resolve: ({ default_profile_id }) => default_profile_id,


Reviews:
user: login: jpotts244
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T00:30:42Z
user: login: damassi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T00:49:43Z
user: login: jpotts244
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T00:54:06Z
user: login: mzikherman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:12:10Z
user: login: jpotts244
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T11:52:41Z
user: login: damassi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:51:28Z
user: login: damassi
body: One more tiny req, but otherwise lgtm 👍 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:52:06Z
user: login: jpotts244
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T17:49:58Z
user: login: damassi
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T18:31:32Z

Review comments:
diff_hunk: @@ -900,6 +900,14 @@ export default (accessToken, userID, opts) => {
       {},
       { headers: true }
     ),
+    partnerContactLoader: gravityLoader<
+      any,
+      { partnerId: string; contactId: string }
+    >(
+      ({ partnerId, contactId }) => `partner/${partnerId}/contact/${contactId}`,
+      {},
path: src/lib/loaders/loaders_with_authentication/gravity.ts
user: login: jpotts244
body: [Hitting this guy](https://github.com/artsy/gravity/blob/main/app/api/v1/partners_contacts_endpoint.rb#L67)
created_at: 2025-04-03T00:30:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -900,6 +900,14 @@ export default (accessToken, userID, opts) => {
       {},
       { headers: true }
     ),
+    partnerContactLoader: gravityLoader<
+      any,
+      { partnerId: string; contactId: string }
+    >(
+      ({ partnerId, contactId }) => `partner/${partnerId}/contact/${contactId}`,
+      {},
+      { headers: true }
path: src/lib/loaders/loaders_with_authentication/gravity.ts
user: login: damassi
body: Probably best to only return the headers form when you need it (like for connection pagination) 
created_at: 2025-04-03T00:49:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -900,6 +900,14 @@ export default (accessToken, userID, opts) => {
       {},
       { headers: true }
     ),
+    partnerContactLoader: gravityLoader<
+      any,
+      { partnerId: string; contactId: string }
+    >(
+      ({ partnerId, contactId }) => `partner/${partnerId}/contact/${contactId}`,
+      {},
+      { headers: true }
path: src/lib/loaders/loaders_with_authentication/gravity.ts
user: login: jpotts244
body: Hmm, I wonder if there is some auth-y thing going on, when I remove the headers I start getting `null` back for the `contact`, will dig a bit
created_at: 2025-04-03T00:54:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -900,6 +900,14 @@ export default (accessToken, userID, opts) => {
       {},
       { headers: true }
     ),
+    partnerContactLoader: gravityLoader<
+      any,
+      { partnerId: string; contactId: string }
+    >(
+      ({ partnerId, contactId }) => `partner/${partnerId}/contact/${contactId}`,
+      {},
+      { headers: true }
path: src/lib/loaders/loaders_with_authentication/gravity.ts
user: login: mzikherman
body: The signature of the data that is resolved from the loader changes when you remove `headers: true`, there's no more `{body, headers}`, it's just the actual data (so `const contact = await ....`).
created_at: 2025-04-03T02:12:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -900,6 +900,14 @@ export default (accessToken, userID, opts) => {
       {},
       { headers: true }
     ),
+    partnerContactLoader: gravityLoader<
+      any,
+      { partnerId: string; contactId: string }
+    >(
+      ({ partnerId, contactId }) => `partner/${partnerId}/contact/${contactId}`,
+      {},
+      { headers: true }
path: src/lib/loaders/loaders_with_authentication/gravity.ts
user: login: jpotts244
body: :doh: yes that would do it
created_at: 2025-04-03T11:52:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -853,6 +853,28 @@ export const PartnerType = new GraphQLObjectType<any, ResolverContext>({
           return Array.from(new Set(cities)).filter(Boolean)
         },
       },
+      contact: {
+        type: ContactType,
+        description: "A Singular Contact belonging to the Partner",
+        args: {
+          contactId: {
+            type: new GraphQLNonNull(GraphQLString),
+            description: "The slug or ID of the Contact",
+          },
+        },
+        resolve: async ({ id }, args, { partnerContactLoader }) => {
+          const { contactId } = args
+
+          if (!partnerContactLoader) return null
+
+          const contact = await partnerContactLoader({
+            partnerId: id,
+            contactId: contactId,
+          })
+
+          return contact ?? null
path: src/schema/v2/partner/partner.ts
user: login: damassi
body: Maybe just return contact? No reason to handle the ?? case as the system should do this for you
created_at: 2025-04-03T16:51:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -853,6 +853,28 @@ export const PartnerType = new GraphQLObjectType<any, ResolverContext>({
           return Array.from(new Set(cities)).filter(Boolean)
         },
       },
+      contact: {
+        type: ContactType,
+        description: "A Singular Contact belonging to the Partner",
+        args: {
+          contactId: {
+            type: new GraphQLNonNull(GraphQLString),
+            description: "The slug or ID of the Contact",
+          },
+        },
+        resolve: async ({ id }, args, { partnerContactLoader }) => {
+          const { contactId } = args
+
+          if (!partnerContactLoader) return null
+
+          const contact = await partnerContactLoader({
+            partnerId: id,
+            contactId: contactId,
+          })
+
+          return contact ?? null
path: src/schema/v2/partner/partner.ts
user: login: jpotts244
body: You got it
created_at: 2025-04-03T17:49:58Z
author_association: CONTRIBUTOR

PR comments: