Soft Launch Apollo Server Cloud Function(Node.js) 버전의 Gateway 만들기

개요


Cloud Function의 술집은 매번 번거롭지만 바꾸기만 하면 새롭게 디자인하거나 이름을 바꾸면 해결할 수 있다.
그러나 단점을 바꾸지 않고 일부 사용자에게만 새 버전카나리아을 발표하거나 특정 사용자에게만 새 버전Soft Launch을 발표할 확률이 있다면 곤란하다.
이번에는 아폴로 Server Cloud Function에서 그래픽QL의 API Gateway를 만들던 중 API Gateway 자체 새 버전을 소프트런치에 발표하려고 클라이언트 쪽 외관에 맞춰 여러 버전을 다루는 Gateway의 Wrapper Gateway를 제작했다.

전제 조건


GraphiQL 서버의 바그닝에 대해 공식적인 견해가 있다.
https://graphql.org/learn/best-practices/#versioning
Versioning
While there's nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.
Why do most APIs version? When there's limited control over the data that's returned from an API endpoint, any change can be considered a breaking change, and breaking changes require a new version. If adding new features to an API requires a new version, then a tradeoff emerges between releasing often and having many incremental versions versus the understandability and maintainability of the API.
In contrast, GraphQL only returns the data that's explicitly requested, so new capabilities can be added via new types and new fields on those types without creating a breaking change. This has led to a common practice of always avoiding breaking changes and serving a versionless API.
결론적으로 "GraphiQL은 고객이 원하는 속성을 선택할 수 있기 때문에 속성을 삭제할 필요가 없고 업데이트가 필요하면 새로운 속성을 추가하면 된다."
저는 개인적으로 Schema 자신의 편집에 전적으로 동의하지만 Resolver의 실시 부분에 대해 versioning을 원하는 마음이 있습니다.원래 다른 속성을 만들어서 옛 속성에 대해 deprecate를 진행해야 하는데, 서버 안에 그 부작용을 숨기려는 경우도 있다.
따라서 우리는'Schema가 공동으로 Resolver에 여러 버전이 있을 때 어떤 기준에 따라 분류할 수 있다'는 것을 실현해 보았다.물론 Scheema가 증가하면 과거 버전의 Resolver에서 그게 이루어지지 않았기 때문에 (정상적으로 null 등으로 되돌아오는 등) 고려해야 한다.

컨디션

  • TypeScript
  • Node.js
  • Google Cloud Functions (Firebase Functions)
  • Apollo Server Cloud Functions
  • Functions Framework
  • Apollo Studio
  • Rover

  • GraphQL Codegen(사실 느낌Apollo로 codegen을 진행할 수 있어요.근데 잘 안 될 것 같아서 포기)
  • 단일체


    보통 이런 느낌이에요.
    v1.ts
    const singleFunction_v1 = functions.region('asia-northeast1')
    .https.onRequest(new ApolloServer(
        {
            typeDefs: require('./pass-to-schema'),
            resolvers: require('./pass-to-resolver'),
            formatError: (err: GraphQLError) => {
                return err
            },
            context: context,
        }
    ).....
    
    exports.singleFunction = singleFunction_v1;
    

    이루어지다


    로컬 시작


    가입Functions Framework
    https://cloud.google.com/functions/docs/functions-framework?hl=ja
    npm run build
    functions-framework --target=singleFunction
    

    Schema gen


    가입Rover CLIhttp://localhost:8080/ 서 있는 상태
    rover graph introspect http://localhost:8080/ > schema.graphql
    
    코드젠용 예쁜 schema 파일 만들기

    codegen


    가입GraphQL Codegen
    graphql-codegen --config codegen.yml
    
    codegen.yml 이런 느낌이에요.
    codegen.yml
    overwrite: true
    schema: "path/to/schema"
    documents: null
    generates:
      ./generated/graphql.ts:
        plugins:
          - "typescript"
          - "typescript-resolvers"
          - "typescript-document-nodes"
    
    예쁜 타입을 만들 수 있어요.

    Gateway


    functions.ts
    import { Resolvers } from './generated/graphql'
    const clonedeep = require('lodash/cloneDeep');
    
    export interface StrKeyObject {
        [key: string]: any;
    }
    
    // 安定バージョン
    const stable = 0
    // 存在するバージョンのリスト
    const versions = [0, 1, 2] 
    
    // load versions
    const resolverVersions: StrKeyObject = {}
    versions.forEach(version => {
        resolverVersions[version] = require(`pass/to/resolver/v${version}`)
    })
    
    // クエリの引数によってバージョンを出し分ける
    const versionMapper = (args: StrKeyObject): number => {
        // ここをいい感じに書く
    } 
    
    // init
    const resolvers: Resolvers = {}
    
    // set stable version
    Object.assign(resolvers, clonedeep(resolverVersions[stable]))
    
    // replace with other versions
    for (const [parentKey, parentValue] of Object.entries(resolvers)) {
        for (const [key, value] of Object.entries(parentValue)) {
            if(value && value.constructor.name === "AsyncFunction"){
                Object.assign(parentValue,  Object.fromEntries([[key, async (parent:any, args:any, context:any, info:any) => {
    		// 子クエリは親と同じバージョンにしたいのでバージョンの計算は初回のみ
                    const version = context.version ? context.version : versionMapper(args);
                    context.version = version
                    console.log("Execute", parentKey + "." + key + ", version:", version)
                    return await resolverVersions[version][parentKey][key](parent, args, context, info)
                }]]))
            }
        }
    }
    
    const schema = loadSchemaSync(join(__dirname, 'path/to/schema.graphql'), {
        loaders: [new GraphQLFileLoader()],
    });
    const schemaWithResolvers = addResolversToSchema({ schema, resolvers });
    
    exports.gateway = functions.region('asia-northeast1')
    .https.onRequest(
        new ApolloServer({
            schema: schemaWithResolvers, 
            formatError: (err: GraphQLError) => {
                return err
            },
            context: context,
            playground: true,
            introspection: true
        }).....
    );
    

    보태다


    결과적으로 하는 일은 schema에서 wrapper로서의resolver까지 그 중의 루트에서 개별적인resolver로 갈 뿐이니 논리를 마음대로 조정하십시오

    좋은 웹페이지 즐겨찾기