@reiwa

セールスフォースのGraphQLをTypeScriptで扱う

typescript
salesforce
graphql

セールスフォースのカスタムオブジェクトをGraphQLを用いて読み書きできるみたいです。

https://developer.salesforce.com/docs/platform/graphql/overviewhttps://developer.salesforce.com/docs/platform/graphql/overview

アクセストークンを取得する

GraphQLのAPIを利用する前にアクセストークンを取得する必要があります。

環境変数のうちusernamepasswordは自分のログイン情報を使用しています。

type Resp = { access_token: string }

const params = {
  grant_type: "password",
  client_id: "__環境変数__",
  client_secret: "__環境変数__",
  username: "__環境変数__",
  password: "__環境変数__",
}

const searchParams = new URLSearchParams(params)

const baseURL = `https://xxx.my.salesforce.com/services/oauth2/token`

const input = `${baseURL}?${searchParams.toString()}`

const resp = await fetch(input, { method: "POST" })

const data: Resp = await resp.json()

const result = data.access_token // アクセストークン

設定の方法はこちらで詳しく解説されています。

https://zenn.dev/hid3/articles/c1d19c60a894cehttps://zenn.dev/hid3/articles/c1d19c60a894ce

このアクセストークンは一定時間が経過すると無効になるので、再度取得する必要があります。

GraphQLのスキーマを取得する

アクセストークンを用いてGraphQLのスキーマを取得します。

サイズが大きくそこそこ時間がかかります。弊社の場合は119980行あって、これはHTTPieのようなアプリではIntrospectionが動作せずGraphQLのクエリが補完されませんでした。

const resp = await fetch(
  "https://xxx.my.salesforce.com/services/data/v57.0/graphql",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      query: `query IntrospectionQuery {
        __schema {
          queryType {
            name
          }
          mutationType {
            name
          }
          types {
            ...FullType
          }
          directives {
            name
            description
            args {
              ...InputValue
            }
            onOperation
            onFragment
            onField
            locations
          }
        }
      }
      fragment FullType on __Type {
        kind
        name
        description
        fields(includeDeprecated: false) {
          name
          description
          args {
            ...InputValue
          }
          type {
            ...TypeRef
          }
        }
        inputFields {
          ...InputValue
        }
        interfaces {
          ...TypeRef
        }
        enumValues(includeDeprecated: true) {
          name
          description
        }
        possibleTypes {
          ...TypeRef
        }
      }
      fragment InputValue on __InputValue {
        name
        description
        type {
          ...TypeRef
        }
        defaultValue
      }
      fragment TypeRef on __Type {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
            }
          }
        }
      }
      `,
    }),
  },
)

const json = await resp.json()

Bunを用いてファイルを書き込みます。ファイル名はschema.graphqlとします。

import { buildClientSchema, printSchema } from "graphql"

const json = await resp.json()

// await Bun.write("schema.graphql.json", JSON.stringify(json.data, null, 2))

const schema = buildClientSchema(json.data)

const schemaText = printSchema(schema)

await Bun.write("schema.graphql", schemaText)

適当にファイルを作って実行します。

$ bun create-graphql-schema.ts

json形式のスキーマに関しては使う予定があれば取得します。

Zeusを使ってライブラリを生成する

Zeusというライブラリを用いてGraphQLのスキーマからTypeScriptのライブラリを生成します。

https://github.com/graphql-editor/graphql-zeushttps://github.com/graphql-editor/graphql-zeus

先ほど生成したGraphQLのスキーマを使ってライブラリを生成します。

$ bunx graphql-zeus schema.graphql ./lib --es

この場合はlibディレクトリにzeusというディレクトリが生成されます。

生成されたライブラリを使ってGraphQLのクエリを実行します。

const chain = Chain(
  "https://xxx.my.salesforce.com/services/data/v57.0/graphql",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  },
)

const query = chain("query")

const resp = await query({
  uiapi: {
    query: {
      Xxx_c: [
        {
          first: 1
        },
        {
          edges: {
            node: {
              Id: true
            }
          }
        }
      ]
    }
  }
})

ただ自分の場合はこんな感じでエラーになります。

[
  {
    errorCode: "INVALID_INPUT",
    message: "GraphQL input is required for POST.",
  }
]

これはこのようにheadersを上書きしていた場合にエラーが発生するみたいです。

const chain = Chain(
  "https://xxx.my.salesforce.com/services/data/v57.0/graphql",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  },
)

生成されたコードはこのようになっておりheadersが上書きされる仕様になってるみたいです。

return fetch(`${options[0]}`, {
  body: JSON.stringify({ query, variables }),
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  ...fetchOptions,
})

そこでContent-Typeも追加することで解決します。

const chain = Chain(
  "https://xxx.my.salesforce.com/services/data/v57.0/graphql",
  {
    method: "POST",
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
    },
  },
)

レスポンスのerrorsが壊れる

Fetch以降の処理はこのようになっており、もしerrorsが空の配列で返却されるとGraphQLErrorが返されて壊れるみたいです。

return fetch()
  .then(handleFetchResponse)
  .then((response: GraphQLResponse) => {
    if (response.errors) {
      throw new GraphQLError(response);
    }
    return response.data;
  });

JavaScriptの世界では空の配列がIF文に使用された場合はTRUEとなります。

if ([]) {
  console.log("ERROR")
}

これはセールスフォースのGraphQLのAPIが空のerrorsを返すことが原因と思います。

生成されたコードを書き換えるのは避けたいのでカスタムのクライアントを作ることにしました。

カスタムのクライアントを作る

どうやらThunderという関数を使うとカスタムのクライアントを作ることができるみたいです。

https://graphqleditor.com/docs/tools/zeus/basics/custom-fetch/https://graphqleditor.com/docs/tools/zeus/basics/custom-fetch/

import { Thunder } from "@/lib/zeus"

async function customFetch(query: string) {
  const accessToken = await createAccessToken()

  const response = await fetch(
    "https://xxx.my.salesforce.com/services/data/v57.0/graphql",
    {
      body: JSON.stringify({ query }),
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${accessToken}`,
      },
    },
  )

  if (response.ok === false) {
    const json = await response.json()
    console.error(json)
    throw new Error(`Failed to fetch ${response.status}`)
  }

  const json = await response.json()

  if (json.errors.length !== 0) {
    for (const error of json.errors) {
      throw new Error(error.message)
    }
  }

  return json.data
}

const customClient = Thunder(customFetch)

このようにしておけば少し便利です。

const query = customClient("query")

const mutation = customClient("mutation")

export const client = {
  query,
  mutation,
}

このように使用できます。

const resp = await client.query({
  uiapi: {
    query: {
      Xxx_c: [
        {
          first: 1
        },
        {
          edges: {
            node: {
              Id: true
            }
          }
        }
      ]
    }
  }
})

クエリする

クエリのwhereは型が補完されますがObjectになる点に注意してください。

const resp = await client.query({
  uiapi: {
    query: {
      SamplePost__c: [
        {
          first: 1,
          where: {
            IsDeleted__c: { eq: false },
          },
        },
        {}
      ]
    }
  }
})

値はGraphQLの返り値の殆どで、このように取り出せます。

const nodes = resp.uiapi.query.SamplePost__c?.edges?.map((edge) => {
  return edge.node!
})

if (nodes === undefined) {
  throw new Error("nodes is undefined")
}

const [node] = nodes

console.log(node)

その他のタグ

関連する記事