4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita株式会社Advent Calendar 2024

Day 10

graphql-tools でコード中の GraphQL Query を抜き出す

Posted at

フロントエンドで以下のようにコード中に GraphQL を書いたとする。

code-with-queries.js
const queryA = graphql(`
query QueryA {
  viewer {
    ...FragmentA
  }
}
`)

const fragmentA = graphql(`
query FragmentA on Viewer {
  id
  name
  ...FragmentB
}
`)

const fragmentB = graphql(`
query FragmentB on Viewer {
  followers(first: 3) {
    node {
      id
      name
    }
  }
}
`)

ここで書いた Query を諸事情で、以下のように抜き出して利用したいことがある。

QueryA-and-dependencies.gql
# QueryA と利用している Fragment のみを抜き出す。
query QueryA {
  viewer {
    ...FragmentA
  }
}

query FragmentA on Viewer {
  id
  name
  ...FragmentB
}

query FragmentB on Viewer {
  followers(first: 3) {
    node {
      id
      name
    }
  }
}

こういうことは、 GraphQL-Tools の loader を使えば行える。

今回やりたいことを実際に行うサンプルコードから用意したので、これを元に解説する。

1. @graphql-tools/load, @graphql-tools/xxx-loader を使って GraphQL Operation を抜き出せる

graphql-tools の loader を使って、以下のように GraphQL コードを抜き出せる。

import { loadDocuments } from '@graphql-tools/load'

// 読み込む元のファイルの種類ごとの loader が用意されている
import { CodeFileLoader } from '@graphql-tools/code-file-loader'

async function main() {
  const sources = await loadDocuments('./src/**/*.{ts,tsx}', {
    loaders: [new CodeFileLoader()],
    pluckConfig: {
      globalGqlIdentifierName: 'graphql',
    },
  })

  // ...
}

2. loader した GraphQL コードの AST を走査する

読み込んだコードは graphql-tools の Source として返されて、この中にコードの文字列やファイル名と GraphQL コードの AST が含まれている。

この AST は graphql.js の Visitor API を使って走査出来る。

import { visit } from 'graphql'

/** @typedef {import("@graphql-tools/utils").Source} Source */
/** @typedef {import("graphql").ExecutableDefinitionNode} ExecutableDefinitionNode */

class DocumentDependencyGraph {
  /**
   * @param {Source[]} sources
   * @return {SchemaDependencyGraph}
   */
  static fromSources(sources) {
    const graph = new DocumentDependencyGraph()

    sources.forEach(source => {
      const document = source.document
      let currentName = null

      visit(document, {
        // 各 query, mutation 宣言それぞれに対して呼び出す
        OperationDefinition(node) {
          if (node.name) {
            currentName = node.name.value
            graph.addDefinition(node.name.value, node)
          }
        },

        // 各 fragment 宣言それぞれに対して呼び出す
        FragmentDefinition(node) {
          currentName = node.name.value
          graph.addDefinition(node.name.value, node)
        },

        // `...Fragment` のような fragment を展開する箇所それぞれで呼び出す
        FragmentSpread(node) {
          if (currentName) {
            graph.addDependency(currentName, node.name.value)
          }
        },
      })
    })

    return graph
  }
  
  constructor() {
    /** @type {Object<string, ExecutableDefinitionNode>} */
    this.definitions = {}

    /** @type {Object<string, Set<string>>} */
    this.dependencies = {}
  }
  
  // ...
}

AST の各 Node の定義は ↓ にまとまっているのでこれらを参照。

自分が見た感じ、 各 Node の定義は GraphQL の Spec 内の名前とほぼ同じなので、こちらを参考にしてもよいかも。

3. print

走査した GraphQL AST は GraphQL.js の print 関数 で文字列として出力出来る。

これで、関連する fragment を含めた GraphQL operation の定義を出力できる。

import { print } from 'graphql'

  /**
   * @param {string} name
   * @return {string}
   */
  getSourceWithDependencies(name) {
    const source = print(this.definitions[name])
    const dependencies = this.getDependencies(name)
    return (
      dependencies.reduce((source, dependency) => {
        const dependencySource = this.definitions[dependency]

        return source + '\n\n' + print(dependencySource)
      }, source) + '\n'
    )
  }
4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?