GithubHelp home page GithubHelp logo

Comments (2)

benjie avatar benjie commented on June 3, 2024 1

You’ll be pleased to hear V5 already uses this (or similar) technique. I’m hesitant to add a feature like this to v4 that would be soon replaced by a V5 alternative which cannot be back-ported due to a dramatic shift in technology. I think it may be best for you to maintain a fork of the relevant package/plugin (or even a patch-package patch) for now.

from graphile-engine.

nick-kang avatar nick-kang commented on June 3, 2024 1

Awesome! Looking forward to V5.

For anyone reading this before V5, this is what we did:

// CustomNodePlugin.ts
import type { Build, DataForType } from 'graphile-build'
import type {
  GraphQLInterfaceType,
  GraphQLResolveInfo,
  GraphQLType,
} from 'graphql'
import type { ResolveTree } from 'graphql-parse-resolve-info'
import type { Plugin, SchemaBuilder } from 'postgraphile'

export type Transform = (input: string) => string

const defaultEncode: Transform = (str) =>
  Buffer.from(String(str)).toString('base64')
const defaultDecode: Transform = (str) =>
  Buffer.from(String(str), 'base64').toString('utf8')

export type NodeFetcher = (
  data: any,
  identifiers: any[],
  context: any,
  parsedResolveInfoFragment: ResolveTree,
  type: GraphQLType,
  resolveData: DataForType,
  resolveInfo: GraphQLResolveInfo,
) => {}

export type BuildExtensionNode = {
  nodeIdFieldName: string
  $$nodeType: any
  nodeFetcherByTypeName: Record<string, NodeFetcher>
  getNodeIdForTypeAndIdentifiers(
    Type: GraphQLType,
    ...identifiers: any[]
  ): string
  getTypeAndIdentifiersFromNodeId(nodeId: string): {
    Type: GraphQLType
    identifiers: any[]
  }
  addNodeFetcherForTypeName(typeName: string, fetcher: NodeFetcher): void
  getNodeAlias(typeName: string): string
  getNodeType(alias: string): GraphQLType
  setNodeAlias(typeName: string, alias: string): void
}

const CustomNodePlugin: Plugin = (
  builder: SchemaBuilder,
  { nodeIdFieldName: inNodeIdFieldName, nodeIdGenerator }: any,
): void => {
  const encode = nodeIdGenerator?.encode ?? defaultEncode
  const decode = nodeIdGenerator?.decode ?? defaultDecode

  const nodeIdFieldName: string = inNodeIdFieldName
    ? String(inNodeIdFieldName)
    : 'id'
  builder.hook(
    'build',
    (build: Build): Build & BuildExtensionNode => {
      const nodeFetcherByTypeName: Record<string, any> = {}
      const nodeAliasByTypeName: Record<string, any> = {}
      const nodeTypeNameByAlias: Record<string, any> = {}
      return build.extend(
        build,
        {
          nodeIdFieldName,
          $$nodeType: Symbol('nodeType'),
          nodeFetcherByTypeName,
          getNodeIdForTypeAndIdentifiers(Type: any, ...identifiers: any[]) {
            return encode(
              JSON.stringify([this.getNodeAlias(Type), ...identifiers]),
            )
          },
          getTypeAndIdentifiersFromNodeId(nodeId: any) {
            const [alias, ...identifiers] = JSON.parse(decode(nodeId))
            return {
              Type: this.getNodeType(alias),
              identifiers,
            }
          },
          addNodeFetcherForTypeName(typeName: any, fetcher: any) {
            if (nodeFetcherByTypeName[typeName]) {
              throw new Error("There's already a fetcher for this type")
            }
            if (!fetcher) {
              throw new Error('No fetcher specified')
            }
            nodeFetcherByTypeName[typeName] = fetcher
          },
          getNodeAlias(typeName: any) {
            return nodeAliasByTypeName[typeName] || typeName
          },
          getNodeType(alias: any) {
            return this.getTypeByName(nodeTypeNameByAlias[alias] || alias)
          },
          setNodeAlias(typeName: any, alias: any) {
            if (
              nodeTypeNameByAlias[alias] &&
              nodeTypeNameByAlias[alias] !== typeName
            ) {
              // eslint-disable-next-line no-console
              console.warn(
                `SERIOUS WARNING: two GraphQL types (${typeName} and ${nodeTypeNameByAlias[alias]}) are trying to use the same node alias '${alias}' which may mean that the Relay Global Object Identification identifiers in your schema may not be unique. To solve this, you should skip the PgNodeAliasPostGraphile plugin, but note this will change all your existing Node IDs. For alternative solutions, get in touch via GitHub or Discord`,
              )
            }
            nodeAliasByTypeName[typeName] = alias
            nodeTypeNameByAlias[alias] = typeName
          },
        },
        "Adding 'Node' interface support to the Build",
      )
    },
    ['Node'],
  )

  builder.hook(
    'init',
    function defineNodeInterfaceType(_, build) {
      const {
        $$isQuery,
        $$nodeType,
        getTypeByName,
        newWithHooks,
        graphql: {
          GraphQLNonNull,
          GraphQLID,
          GraphQLInterfaceType,
          getNullableType,
        },
        inflection,
      } = build
      let Query: any
      newWithHooks(
        GraphQLInterfaceType,
        {
          name: inflection.builtin('Node'),
          description: build.wrapDescription(
            'An object with a globally unique `ID`.',
            'type',
          ),
          resolveType: (value: any) => {
            if (value === $$isQuery) {
              if (!Query) {
                Query = getTypeByName(inflection.builtin('Query'))
              }
              return Query
            } else if (value[$$nodeType]) {
              return getNullableType(value[$$nodeType])
            }
          },
          fields: {
            [nodeIdFieldName]: {
              description: build.wrapDescription(
                'A globally unique identifier. Can be used in various places throughout the system to identify this single value.',
                'field',
              ),
              type: new GraphQLNonNull(GraphQLID),
            },
          },
        },
        {
          __origin:
            "graphile-build built-in (NodePlugin); you can omit this plugin if you like, but you'll lose compatibility with Relay",
        },
      )
      return _
    },
    ['Node'],
  )

  builder.hook(
    'GraphQLObjectType:interfaces',
    function addNodeIdToQuery(
      interfaces: GraphQLInterfaceType[],
      build,
      context,
    ) {
      const { getTypeByName, inflection } = build
      const {
        scope: { isRootQuery },
      } = context
      if (!isRootQuery) {
        return interfaces
      }
      const Type = getTypeByName(inflection.builtin('Node'))
      if (Type) {
        return [...interfaces, Type]
      } else {
        return interfaces
      }
    },
    ['Node'],
  )

  builder.hook(
    'GraphQLObjectType:fields',
    (fields, build, context) => {
      const {
        scope: { isRootQuery },
        fieldWithHooks,
      } = context
      if (!isRootQuery) {
        return fields
      }
      const {
        getTypeByName,
        extend,
        graphql: { GraphQLNonNull, GraphQLID },
        inflection,
        resolveNode,
      } = build
      return extend(
        fields,
        {
          [nodeIdFieldName]: {
            description: build.wrapDescription(
              'The root query type must be a `Node` to work well with Relay 1 mutations. This just resolves to `query`.',
              'field',
            ),
            type: new GraphQLNonNull(GraphQLID),
            resolve() {
              return 'query'
            },
          },
          node: fieldWithHooks(
            'node',
            ({ getDataFromParsedResolveInfoFragment }: any): any => ({
              description: build.wrapDescription(
                'Fetches an object given its globally unique `ID`.',
                'field',
              ),
              type: getTypeByName(inflection.builtin('Node')),
              args: {
                [nodeIdFieldName]: {
                  description: build.wrapDescription(
                    'The globally unique `ID`.',
                    'arg',
                  ),
                  type: new GraphQLNonNull(GraphQLID),
                },
              },
              resolve(
                data: any,
                args: any,
                context2: any,
                resolveInfo: any,
              ): any {
                const nodeId = args[nodeIdFieldName]
                return resolveNode(
                  nodeId,
                  build,
                  { getDataFromParsedResolveInfoFragment },
                  data,
                  context2,
                  resolveInfo,
                )
              },
            }),
            {
              isRootNodeField: true,
            },
          ),
        },
        `Adding Relay Global Object Identification support to the root Query via 'node' and '${nodeIdFieldName}' fields`,
      )
    },
    ['Node'],
  )
}

export default CustomNodePlugin

from graphile-engine.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.