Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Магия декларативныx схем.

Магия декларативныx схем.

На примере GraphQL посмотрим какие интересные возможности открываются когда ваши апишки используют декларативную схему.

Polina Gurtovaya

August 16, 2022
Tweet

More Decks by Polina Gurtovaya

Other Decks in Programming

Transcript

  1. 3

  2. 4

  3. 5

  4. Декларативно и императивно 7 Императивно – выдать системе инструкции. Мы

    будем ожидать что в результате получится то, что мы хотим. Декларативно – явно сообщить системе что мы хотим. Система сама определит порядок действий, чтобы выдать нам желаемое.
  5. 13 query GetAllPages { allPages { id text } }

    import { gql } from '@apollo/client' export const query = gql` query GetAllPages { allPages { id text } } ` console.log(query)
  6. 14 { "kind": "Document", "definitions": [ { "kind": "OperationDefinition", "operation":

    "query", "name": { "kind": "Name", "value": "GetAllPages" }, } … 

  7. 15 query GetAllPages { allPages {{ id text } }

    Syntax Error: Expected Name, found "{".
  8. 21 type Query { allTypes: [AlgebraicDataType!]! allPages: [Page!]! algebraicDataType(id: ID!):

    AlgebraicDataType page(id: ID): Page } type Page { id: ID! algebraicDataType: AlgebraicDataType! text: String! } type AlgebraicDataType { id: ID! name: String! description: String! parametersCount: Int! constructors: [TypeConstructor!]! isAliasFor: AlgebraicDataType } type TypeConstructor { id: ID! name: String! description: String! parametersCount: Int! }
  9. Как выполнять запросы? • Для каждого поля выдаем значения, призывая

    resolver • Хорошие новости: не нужно определять resolver для каждого поля 22
  10. 23 import { buildSchema, execute, parse } from 'graphql' import

    schema from './schema.graphql' import validQuery from './validQuery.graphql' const schemaAst = buildSchema(schema) const document = parse(validQuery) const rootResolver = { allPages: [], } export const executeQuery = async () => { const result = await execute({ schema: schemaAst, document, rootValue: rootResolver, }) return result } { "data": { "allPages": [] }
  11. Путь ленивых: addMocks 24 import { buildSchema, execute, parse }

    from 'graphql' import { addMocksToSchema } from '@graphql-tools/mock' import schema from './schema.graphql' import validQuery from './validQuery.graphql' const schemaAst = buildSchema(schema) const document = parse(validQuery) const schemaWithMocks = addMocksToSchema({ schema: schemaAst, mocks: {}, }) export const executeQuery = async () => { const result = await execute({ schema: schemaWithMocks, document, }) return result }
  12. 25 { "data": { "allTypes": [ { "id": "36b7fa16-a784-4279-9233-52e41e076dd9", "name":

    "Hello World", "description": "Hello World" }, { "id": "a977f0ff-d3d3-458c-9b12-f614939e5f98", "name": "Hello World", "description": "Hello World" } ] } }
  13. Hello world? Мы можем круче 26 const schemaWithMocks = addMocksToSchema({

    schema: schemaAst, mocks: { AlgebraicDataType: () => ({ name: 'Maybe', description: ‘Just or Nothing!', parametersCount: 1, }), }, })
  14. 27 { "data": { "allTypes": [ { "id": "5118b674-9f5d-4a02-94eb-0de8754448e0", "name":

    "Maybe", "description": "Just or Nothing!" }, { "id": "551a3daa-f6be-476a-9213-55ba50c07124", "name": "Maybe", "description": “Just or Nothing!" } ] } }
  15. 29 class MockLink extends ApolloLink { request(operation: Operation): Observable<FetchResult> {

    return new Observable(observer => { executeQuery(operation.query).then(result => { observer.next(result) observer.complete() }) }) } } const client = new ApolloClient({ cache: new InMemoryCache(), link: new MockLink(), }) export const executeQueryWithApollo = async () => { const result = await client.query({ query: gql(validQuery), }) return result }
  16. GraphQL server spell Можно поднять graphql-сервер и выполнять execute на

    заглушках. • graphql-js + express • graphql-yoga • аpollo-server 30
  17. Интроспекция 33 import { execute, buildSchema, getIntrospectionQuery, parse, ExecutionResult, }

    from 'graphql' import schema from './schema.graphql' const schemaAst = buildSchema(schema) const introspectionAst = parse(getIntrospectionQuery()) export const getIntrospectionResult = async () => execute({ schema: schemaAst, document: introspectionAst, rootValue: {}, }) as Promise<ExecutionResult>
  18. 35

  19. Если есть схема, по ней можно валидировать запросы 37 import

    { validate, buildSchema } from 'graphql' import { gql } from '@apollo/client' import schema from './schema.graphql' export const validateQueries = () => { const validQuery = gql` { allTypes { id name } } ` const invalidQuery = gql` { fieldThatDoesNotExist { id name } } ` return [validQuery, invalidQuery].map(q => validate(buildSchema(schema), q)) }
  20. 38

  21. graphql.conf i g.js 40 // graphql.config.js const path = require('path')

    module.exports = { schema: path.join(__dirname, '/schema.graphql'), }
  22. Опасные заклинания 41 import { gql } from '@apollo/client' const

    strangeDynamicQuery = (fieldName: string) => gql` query GetType { ${fieldName} { name description } }
  23. 44 import { FC } from 'react' import { useQuery,

    gql } from '@apollo/client' export const MaybePageQuery = gql` query MaybePageQuery { algebraicDataType(id: 1) { name id description } } ` export const MaybePage: FC = () => { const { data, loading } = useQuery(MaybePageQuery) return ( <> {loading || data === undefined ? ( 'Loading...' ) : ( <> <h3>{data.algebraicDataType.name}</h3> <p> {data.algebraicDataType.description} </p> <p> {data.algebraicDataType.parametersCount} </p> </> )} </> ) }
  24. 45 export declare function useQuery<TData = any, TVariables = OperationVariables>(

    query: DocumentNode | TypedDocumentNode<TData, TVariables>, options?: QueryHookOptions<TData, TVariables> ): QueryResult<TData, TVariables>;
  25. graphql - codegen 47 # codege.config.yml overwrite: true schema: 'graphql/schema.graphql'

    generates: generated.tsx: plugins: - typescript Крутая система плагинов позволяет генерировать что угодно. Умеет делать интроспекцию.
  26. GraphQL - > TS 48 type AlgebraicDataType { id: ID!

    name: String! description: String! parametersCount: Int! constructors: [TypeConstructor!]! isAliasFor: AlgebraicDataType } export type AlgebraicDataType = { __typename?: 'AlgebraicDataType' constructors: Array<TypeConstructor> description: Scalars['String'] id: Scalars['ID'] isAliasFor?: Maybe<AlgebraicDataType> name: Scalars['String'] parametersCount: Scalars['Int'] }
  27. Про архитектуру при этом думать надо вам : ) •

    Запросы прямо в компонентах. Так удобно… да? 53
  28. Вот так делать не надо 55 const testQuery = gql`

    query TestPages { allPages { id text } } ` test('it works!', async () => { const response = await client.query({ query: testQuery }) expect(response.data.allPages).toBeDefined() })
  29. Что тестировать? • Сложную логику процессинга ваших GraphQL данных. •

    Компонент, который использует GraphQL. • Хитрый хук, который как-то использует GraphQL. 56
  30. Как тестировать? 57 Вам понадобится сущность, которая инкапсулирует логику работы

    в вашим GraphQL движком. Если у вас такой нет, подумайте все ли окей с архитектурой вашего приложения :) Забудьте что у вас есть настоящий сервер Замокайте вашу систему Используйте моки в тестах
  31. 59 import { MaybePage, MaybePageQuery } from './MaybePage' import {

    screen, render } from '@testing-library/react' import { MockedProvider } from '@apollo/client/testing' it('Renders Maybe page', async () => { const mocks = [ { request: { query: MaybePageQuery, }, result: { data: { algebraicDataType: { name: 'Maybe', id: 1, description: '', }, }, }, }, ] render( <MockedProvider mocks={mocks} addTypename={false}> <MaybePage /> </MockedProvider> ) expect(await screen.findByText('Loading...')).toBeInTheDocument() expect(await screen.findByText('Maybe')).toBeInTheDocument() })
  32. 61 import { MaybePage, MaybePageQuery } from './MaybePage' import {

    screen, render } from '@testing-library/react' import { MockedProvider } from '@apollo/client/testing' import { addMocksToSchema } from '@graphql-tools/mock' import schema from './schema.graphql' import { execute, buildSchema, DocumentNode } from 'graphql' const getSchemaWithMocks = (customMocks = {}) => addMocksToSchema({ schema: buildSchema(schema), mocks: customMocks }) const createMock = async (query: DocumentNode, customMocks = {}) => ({ request: { query, }, result: await execute({ schema: getSchemaWithMocks(customMocks), document: MaybePageQuery, }), }) it('Renders Maybe page', async () => { const mocks = [await createMock(MaybePageQuery)] render( <MockedProvider mocks={mocks} addTypename={false}> <MaybePage /> </MockedProvider> ) expect(await screen.findByText('Loading...')).toBeInTheDocument() expect(await screen.findAllByText('Hello World')).toHaveLength(2) })
  33. 62 // raw-transformer.js module.exports = { process(sourceText) { return {

    code: `module.exports = ${JSON.stringify(sourceText)}`, } }, } // jest.config.js module.exports = { // ... transform: { '.(graphql|gql)$': '<rootDir>/raw-transformer.js', }, } Как завести Jest?
  34. Вот новая версия схемы 66 type Query { allTypes: [AlgebraicDataType!]!

    allPages: [Page!]! algebraicDataType(id: ID!): AlgebraicDataType page(id: ID): Page } type Page { id: ID! algebraicDataType: AlgebraicDataType! text: String! } type AlgebraicDataType { id: ID! name: String! description: String! # parametersCount: Int! 👋 parametersCount count constructors: [TypeConstructor!]! isAliasFor: AlgebraicDataType } type TypeConstructor { id: ID! name: String! description: String! parametersCount: Int! }
  35. А вот так мы ищем diff 67 import schema from

    './schema.graphql' import schemaV1 from './schemaV1.graphql' import { parse, DocumentNode, ObjectTypeDefinitionNode } from 'graphql' const oldSchema = parse(schema) const newSchema = parse(schemaV1) const collectFields = (schema: DocumentNode) => schema.definitions .filter(({ kind }) => kind === 'ObjectTypeDefinition') .flatMap(def => (def as ObjectTypeDefinitionNode).fields) .map(field => field?.name.value) .reduce<Record<string, number>>((acc, fieldName) => { if (!fieldName) return acc const prevCount = acc[fieldName] || 0 return { ...acc, [fieldName]: prevCount + 1 } }, {}) export const getRemovedFields = () => { const oldFields = collectFields(oldSchema) const newFields = collectFields(newSchema) return Object.keys(oldFields).filter( fieldName => newFields[fieldName] - oldFields[fieldName] < 0 ) } ["parametersCount"]
  36. Apollo Studio • Трекает изменение схем • Собирает метрики •

    Поддерживает суперграф • Валидирует схемы • Доки тоже есть 69
  37. Вот и все. Что там в нашей книжечке теперь? •

    Мы разобрались с тем как устроена GraphQL схема, как ее парсить и траверсить. • Мы посмотрели на различные представления GraphQL схемы и подумали о том, как они могут помочь в разработке и коммуникации с командой. • Мы понимаем как и зачем валидировать запросы, как настроить свою IDE и CI чтобы при помощи схемы сделать наше приложение более надежным. • Ленивые Эффективные разработчики теперь будут еще эффективнее, ибо знают как настроить codegen. • Мы посмотрели как писать и рефакторить тесты для наших ографкуэленых компонентов. • Мы научились правильно депрекейтить поля и готовы запустить нашу схему в космос. 70
  38. Если спелбука не достаточно, у меня есть роман в 6

    томах 71 https:/ /hellsquirrel.dev/blog/declarative-schema-parsing-1