Azure의 GraphQL: 다섯 번째 부분 - GraphQL 유형을 코드에서 안전하게 할 수 있습니까?

This article is part of #ServerlessSeptember (https://aka.ms/ServerlessSeptember2020). You’ll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles from community members and cloud advocates are published every week from Monday to Thursday through September.

Find out more about how Microsoft Azure enables your Serverless functions at https://docs.microsoft.com/azure/azure-functions/


최근에 저는 Azure Functions에서GraphQL을 이용하여 많은 작업을 했습니다. 저는GraphQL 단점을 디자인하는 모델 우선 방법이 매우 효과적임을 발견했습니다.
그러나 내가 발견한 주요 단점은 강한 유형 모델부터 시작하지만 해석기를 실현하고 데이터 모델을 사용할 때 이 유형의 정보를 잃어버린다는 것이다.
그래서 이 문제를 어떻게 해결하는지 봅시다. 우리는Azure 함수에서GraphQL로 응용 프로그램을 구축하고 CosmosDB의 데이터 모델로 지원할 수 있습니다. 이 모든 것은TypeScript로 작성된 것입니다.

To learn how to get started with GraphQL on Azure Functions, check out this article from #ServerlessSeptember (with the main difference being we're using TypeScript, not JavaScript).



우리 모드 만들기
우리가 오늘 구축하고자 하는 API는 자질구레한 API이다. (이것은 Open Trivia DB에서 온 데이터를 원본으로 사용한다.)
먼저 API를 graphql 폴더의 이름이 schema.graphql인 파일로 표시하는 모드를 정의합니다.
type Question {
  id: ID!
  question: String!
  correctAnswer: String!
  answers: [String!]!
}

type Query {
  question(id: ID!): Question
  getRandomQuestion: Question
}

type Answer {
  questionId: ID
  question: String!
  submittedAnswer: String!
  correctAnswer: String!
  correct: Boolean
}

type Mutation {
  answerQuestion(id: ID, answer: String): Answer
}

schema {
  query: Query
  mutation: Mutation
}
우리의 모델은 두 가지 핵심 유형을 정의했는데 그것이 바로 QuestionAnswer이다. 그리고 일부 검색과 하나의 변체이다. 이 모든 유형은 유용한GraphQL 유형 주석으로 장식되어 있는데 이것은 우리의 해석 프로그램의 TypeScript 실현에 유용하다.

해결자 만들기
Query Guide 를 시작으로 CosmosDB 에서 데이터를 가져와 소비자에게 되돌려야 합니다.
const resolvers = {
  Query: {
    question(_, { id }, { dataStore }) {
      return dataStore.getQuestionById(id);
    },
    async getRandomQuestion(_, __, { dataStore }) {
      const questions = await dataStore.getQuestions();
      return questions[Math.floor(Math.random() * questions.length) + 1];
    },
  },
};

export default resolvers;
이것은 구조 중의 모델의 조회 부분과 일치하지만, 우리는 어떻게 해석기 함수를 실현하는지 알 수 있습니까?questiongetRandomQuestion에 대해 우리는 어떤 논거를 가지고 있습니까?우리는 questionid 매개 변수를 수신할 것을 알고 있지만 어떻게 수신합니까?만약 우리가 TypeScript에서 이 점을 보았다면 곳곳에 any이 있었을 것이다. 이것은 우리가 TypeScript에서 얼마의 가치를 얻지 못했다는 것을 의미한다.
여기서, 우리는 우리가 작성하고 있는 코드와 우리가 사용하고 있는 패턴 사이에 불균형이 생기기 시작한다.

GraphQL 코드 생성기 입력
고맙게도, 우리가 이 문제를 해결하는 데 도움을 줄 수 있는 도구가 하나 있다. GraphQL Code Generator.설치 도구를 사용하여 다음을 설정합니다.
npm install --save-dev @graphql-codegen/cli
Functions 응용 프로그램의 루트 디렉토리에 config.yml이라는 구성 파일을 설정합니다.
overwrite: true
schema: "./graphql/schema.graphql"
generates:
  graphql/generated.ts:
    plugins:
      - typescript
      - typescript-resolvers
이것은 우리의 generated.ts을 입력으로 하고 graphql 폴더에 schema.graphql이라는 파일을 생성합니다.출력은 TypeScript이며 typescripttypescript-resolvers 플러그인을 사용하여 파서 서명을 생성하므로 이 플러그인도 설치하는 것이 좋습니다.
npm install --save-dev @graphql-codegen/typescript @graphql-codegen/typescript-resolvers
발전기를 운행할 때가 되었다.
npx graphql-codegen --config codegen.yml

강제 입력 해상도
이 새 유형의 정보를 사용하도록 분석 프로그램을 업데이트할 수 있습니다.
import { Resolvers } from "./generated"

const resolvers: Resolvers = {
  Query: {
    question(_, { id }, { dataStore }) {
      return dataStore.getQuestionById(id);
    },
    async getRandomQuestion(_, __, { dataStore }) {
      const questions = await dataStore.getQuestions();
      return questions[Math.floor(Math.random() * questions.length) + 1];
    },
  },
};

export default resolvers;
현재 우리는 id에 멈춰 서서 그것이 string이라는 것을 볼 수 있지만, 우리는 여전히 한 토막이 부족하다. 무엇이 dataStore이고, 우리는 그것이 어떤 유형인지 어떻게 알 수 있습니까?

데이터 저장소 만들기
먼저 data.ts이라는 새 파일을 만듭니다.이것은 CosmosDB와 합작한 API를 수용할 것입니다. 우리는 CosmosDB를 사용하기 때문에 노드 모듈을 가져와야 합니다.
npm install --save @azure/cosmos
왜 우주 데이터베이스입니까?코스모스DB는 Azure 함수 중의 서버가 없는 GraphQL 호스트의 이념과 잘 어울리는 serverless plan을 방금 출시했다.서버 없는 호스트와 서버 없는 데이터 저장소는 승리로 들린다!
모듈을 설치한 후 데이터 저장소를 구축할 수 있습니다.
import { CosmosClient } from "@azure/cosmos";

export type QuestionModel = {
  id: string;
  question: string;
  category: string;
  incorrect_answers: string[];
  correct_answer: string;
  type: string;
  difficulty: "easy" | "medium" | "hard";
};

interface DataStore {
  getQuestionById(id: string): Promise<QuestionModel>;
  getQuestions(): Promise<QuestionModel[]>;
}

class CosmosDataStore implements DataStore {
  #client: CosmosClient;
  #databaseName = "trivia";
  #containerName = "questions";

  #getContainer = () => {
    return this.#client
      .database(this.#databaseName)
      .container(this.#containerName);
  };

  constructor(client: CosmosClient) {
    this.#client = client;
  }

  async getQuestionById(id: string) {
    const container = this.#getContainer();

    const question = await container.items
      .query<QuestionModel>({
        query: "SELECT * FROM c WHERE c.id = @id",
        parameters: [{ name: "@id", value: id }],
      })
      .fetchAll();

    return question.resources[0];
  }

  async getQuestions() {
    const container = this.#getContainer();

    const question = await container.items
      .query<QuestionModel>({
        query: "SELECT * FROM c",
      })
      .fetchAll();

    return question.resources;
  }
}

export const dataStore = new CosmosDataStore(
  new CosmosClient(process.env.CosmosDB)
);
이 클래스는 CosmosClient을 수신합니다. 이 클래스는CosmosDB를 조회하는 연결을 제공하고, 해상도에서 사용하는 두 함수를 제공합니다.우리는 또 하나의 데이터 모델 QuestionModel을 가지고 있는데 이것은 우리가CosmosDB에 데이터를 저장하는 방식을 대표한다.

To create a CosmosDB resource in Azure, check out their quickstart and here is a data sample that can be uploaded via the Data Explorer in the Azure Portal._


구문 분석기를 사용할 수 있도록 index.ts을 확장하여 GraphQL 컨텍스트에 추가합니다.
import { ApolloServer } from "apollo-server-azure-functions";
import { importSchema } from "graphql-import";
import resolvers from "./resolvers";
import { dataStore } from "./data";

const server = new ApolloServer({
  typeDefs: importSchema("./graphql/schema.graphql"),
  resolvers,
  context: {
    dataStore,
  },
});

export default server.createHandler();
만약 우리가 서버를 실행한다면, 우리는 단점을 조회하고CosmosDB에서 데이터를 추출할 수 있으나, 우리의 해상도는 여전히 dataStore의 유형이 부족하기 때문에, 사용자 정의 맵을 사용할 것입니다.

컨텍스트 유형 사용자화
지금까지 우리가 생성한 유형은 모두 GraphQL 모델에 기반한 내용으로 기본적으로 효과적이지만 차이점도 있다.그 중 하나는 우리가 해상도에서 요청 상하문을 어떻게 사용하는가이다. 패턴상 존재하지 않기 때문에 형식 생성기를 위해 더 많은 작업을 해야 한다.
먼저 컨텍스트 유형을 정의하여 data.ts의 아래쪽에 추가합니다.
export type Context = {
  dataStore: DataStore;
};
이제 구성을 수정하여 GraphQL 코드 생성기에 다음과 같이 사용할 수 있습니다.
overwrite: true
schema: "./graphql/schema.graphql"
generates:
  graphql/generated.ts:
    config:
      contextType: "./data#Context"
    plugins:
      - "typescript"
      - "typescript-resolvers"
새로운 config 노드를 추가했습니다. 이 노드에 contextType의 형식으로 <path>#<type name>을 지정했습니다. 생성기를 실행할 때 이 종류를 사용할 것입니다. 현재 저희 해석기에 dataStore을 입력하세요!

모델 사용자정의
로컬에서 우리의 함수를 실행할 때가 되었다.
npm start
우리 의문을 제기합시다.우리는 랜덤으로 한 문제에 대답할 것이다.
{
  getRandomQuestion {
    id
    question
    answers
  }
}
불행하게도 작업에 장애가 발생하여 다음 오류가 발생했습니다.

Cannot return null for non-nullable field Question.answers.


GraphQL 모드의 Question 유형으로 돌아가면 다음과 같습니다.
type Question {
  id: ID!
  question: String!
  correctAnswer: String!
  answers: [String!]!
}
answers은 공백 문자열([String!]!)이 될 수 없는 공백 그룹이기 때문에 이 오류 메시지는 의미가 있지만 코스모스의 데이터 모델과 비교하면 다음과 같습니다.
export type QuestionModel = {
  id: string;
  question: string;
  category: string;
  incorrect_answers: string[];
  correct_answer: string;
  type: string;
  difficulty: "easy" | "medium" | "hard";
};
answers 필드는 없고 incorrect_answerscorrect_answer만 있습니다.
사용자 정의 모델을 사용하여 생성된 유형을 더욱 확장할 때가 되었다.먼저 구성 파일을 업데이트합니다.
overwrite: true
schema: "./graphql/schema.graphql"
generates:
  graphql/generated.ts:
    config:
      contextType: "./data#Context"
      mappers:
        Question: ./data#QuestionModel
    plugins:
      - "typescript"
      - "typescript-resolvers"
mappers 부분에 대해 생성기에 Question 유형을 찾으면 QuestionModel을 부모 유형으로 사용할 것이라고 알려 줍니다.
그러나 GraphQL에서 answers 필드를 만드는 방법을 알려주지 않았기 때문에 Question 형식에 해상도를 정의해야 합니다.
import { Resolvers } from "./generated";

const resolvers: Resolvers = {
  Query: {
    question(_, { id }, { dataStore }) {
      return dataStore.getQuestionById(id);
    },
    async getRandomQuestion(_, __, { dataStore }) {
      const questions = await dataStore.getQuestions();
      return questions[Math.floor(Math.random() * questions.length) + 1];
    },
  },

  Question: {
    answers(question) {
      return question.incorrect_answers
        .concat([question.correct_answer])
        .sort();
    },
    correctAnswer(question) {
      return question.correct_answer;
    },
  },
};

export default resolvers;
이 필드 분석 프로그램은 부모 파라미터를 첫 번째 파라미터인 QuestionModel으로 받아들여 패턴에 정의된 유형을 되돌려 주고 필요에 따라 유형 간에 데이터를 비추기를 기대한다.
Azure 함수를 다시 시작하고 이전 질의를 실행하면 API가 임의의 문제를 반환합니다.

결론
Azure 함수에 GraphQL을 배치하는 방법과 GraphQL 모델을 어떻게 사용하고 우리 자신의 모델을 결합시켜 TypeScript를 사용하여 유형 보안을 강제로 집행하는지 이해했습니다.
우리는 이 문장에서 돌연변이를 실현하지 못했는데, 이것은 독자가 해결해야 할 문제이다.
GitHub에서 React 전단과 연결하는 방법을 포함한 전체 예시를 볼 수 있습니다.

좋은 웹페이지 즐겨찾기