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. Декларативность и магия
    схем

    View full-size slide

  2. Давайте скрафтим wiki для
    любителей FP. Будем использовать
    GraphQL
    6

    View full-size slide

  3. Декларативно и императивно
    7
    Императивно – выдать системе
    инструкции. Мы будем ожидать
    что в результате получится то, что
    мы хотим.


    Декларативно – явно сообщить
    системе что мы хотим. Система
    сама определит порядок действий,
    чтобы выдать нам желаемое.

    View full-size slide

  4. В первом случае контроль на
    нашей стороне, во втором – на
    стороне системы.
    8

    View full-size slide

  5. Query Language
    9

    View full-size slide

  6. Не важно как мы отправим
    запрос и получим ответ
    10

    View full-size slide

  7. Как сделать неправильный запрос?
    • Ошибиться в грамматике.


    • Запросить то чего нет.
    11

    View full-size slide

  8. Магия парсинга
    12

    View full-size slide

  9. 13
    query GetAllPages {


    allPages {


    id


    text


    }


    }


    import { gql } from '@apollo/client'


    export const query = gql`


    query GetAllPages {


    allPages {


    id


    text


    }


    }


    `


    console.log(query)


    View full-size slide

  10. 14
    {


    "kind": "Document",


    "definitions": [


    {


    "kind": "OperationDefinition",


    "operation": "query",


    "name": {


    "kind": "Name",


    "value": "GetAllPages"


    },


    } …





    View full-size slide

  11. 15
    query GetAllPages {


    allPages {{


    id


    text


    }


    }
    Syntax Error: Expected Name, found "{".

    View full-size slide

  12. Как работает парсинг?
    • Берете грамматику


    • Берете запрос


    • Строите 🌳
    16

    View full-size slide

  13. Как импортировать .graphql ?
    • raw-loader


    • graphqh-tag/loader
    17

    View full-size slide

  14. Магия заглушек
    18

    View full-size slide

  15. Настало время спроектировать схему
    19

    View full-size slide

  16. Нам понадобится
    • оглавление (список всех страниц)


    • глоссарий (список всех типов)


    • страничка


    • описание типа
    20

    View full-size slide

  17. 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!


    }


    View full-size slide

  18. Как выполнять запросы?
    • Для каждого поля выдаем значения, призывая resolver


    • Хорошие новости: не нужно определять resolver для
    каждого поля
    22

    View full-size slide

  19. 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": []


    }


    View full-size slide

  20. Путь ленивых: 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


    }


    View full-size slide

  21. 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"


    }


    ]


    }


    }


    View full-size slide

  22. Hello world? Мы можем круче
    26
    const schemaWithMocks = addMocksToSchema({


    schema: schemaAst,


    mocks: {


    AlgebraicDataType: () => ({


    name: 'Maybe',


    description: ‘Just or Nothing!',


    parametersCount: 1,


    }),


    },


    })

    View full-size slide

  23. 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!"


    }


    ]


    }


    }


    View full-size slide

  24. Apollo link spell
    28

    View full-size slide

  25. 29
    class MockLink extends ApolloLink {


    request(operation: Operation): Observable {


    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


    }


    View full-size slide

  26. GraphQL server spell
    Можно поднять graphql-сервер и выполнять execute на
    заглушках.


    • graphql-js + express


    • graphql-yoga


    • аpollo-server
    30

    View full-size slide

  27. Магия визуализации
    31

    View full-size slide

  28. Где мои стрелочки
    ? ?
    32
    graphql
    -
    voyager

    View full-size slide

  29. Интроспекция
    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


    View full-size slide

  30. Документация
    • GraphQL Explorer


    • GraphiQL
    34

    View full-size slide

  31. Магия линтеров
    36

    View full-size slide

  32. Если есть схема, по ней можно валидировать
    запросы
    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))


    }


    View full-size slide

  33. Как использовать валидацию?
    • настроить линтеры graphql-eslint


    • настроить расширение для вашей IDE.


    • не забыть гонять проверки на CI
    39

    View full-size slide

  34. graphql.conf
    i
    g.js
    40
    // graphql.config.js


    const path = require('path')


    module.exports = {


    schema: path.join(__dirname, '/schema.graphql'),


    }


    View full-size slide

  35. Опасные заклинания
    41
    import { gql } from '@apollo/client'


    const strangeDynamicQuery = (fieldName: string) => gql`


    query GetType {


    ${fieldName} {


    name


    description


    }


    }

    View full-size slide

  36. Раздел для ленивых
    волшебников
    42

    View full-size slide

  37. Давайте поиграем
    :
    ) На следующем слайде будет
    код. Найдите в нем ошибку.
    43

    View full-size slide

  38. 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...'


    ) : (


    <>


    {data.algebraicDataType.name}


    {data.algebraicDataType.description}


    {data.algebraicDataType.parametersCount}


    >


    )}


    >


    )


    }


    View full-size slide

  39. 45
    export declare function useQuery(


    query: DocumentNode | TypedDocumentNode,


    options?: QueryHookOptions


    ): QueryResult;

    View full-size slide

  40. graphql
    -
    codegen и вжух
    46

    View full-size slide

  41. graphql
    -
    codegen
    47
    # codege.config.yml


    overwrite: true


    schema: 'graphql/schema.graphql'


    generates:


    generated.tsx:


    plugins:


    - typescript


    Крутая система плагинов
    позволяет генерировать
    что угодно.


    Умеет делать
    интроспекцию.


    View full-size slide

  42. GraphQL
    - >
    TS
    48
    type AlgebraicDataType {


    id: ID!


    name: String!


    description: String!


    parametersCount: Int!


    constructors: [TypeConstructor!]!


    isAliasFor: AlgebraicDataType


    }


    export type AlgebraicDataType = {


    __typename?: 'AlgebraicDataType'


    constructors: Array


    description: Scalars['String']


    id: Scalars['ID']


    isAliasFor?: Maybe


    name: Scalars['String']


    parametersCount: Scalars['Int']


    }

    View full-size slide

  43. Полезные плагины
    • typescript


    • typescript-operations


    • typescript-react-apollo


    • introspection


    • typescript-apollo-client-helpers
    49

    View full-size slide

  44. Заклинание уборки 🧹
    50
    Вот это добавить в конфиг codegen
    hooks:


    afterAllFileWrite: - eslint --fix


    View full-size slide

  45. Трестирующая магия
    51

    View full-size slide

  46. Apollo client
    • Делает запросы


    • Получает ответы


    • Нормализует


    • Складывает все в cache
    52

    View full-size slide

  47. Про архитектуру при этом думать надо вам
    :
    )
    • Запросы прямо в компонентах. Так удобно… да?
    53

    View full-size slide

  48. Минус 10 к защите от Apollo
    54

    View full-size slide

  49. Вот так делать не надо
    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()


    })


    View full-size slide

  50. Что тестировать?
    • Сложную логику процессинга ваших GraphQL данных.


    • Компонент, который использует GraphQL.


    • Хитрый хук, который как-то использует GraphQL.
    56

    View full-size slide

  51. Как тестировать?
    57
    Вам понадобится сущность, которая
    инкапсулирует логику работы в вашим GraphQL
    движком. Если у вас такой нет, подумайте все
    ли окей с архитектурой вашего приложения :)


    Забудьте что у вас есть настоящий сервер


    Замокайте вашу систему


    Используйте моки в тестах

    View full-size slide

  52. Apollo client case
    58

    View full-size slide

  53. 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(











    )


    expect(await screen.findByText('Loading...')).toBeInTheDocument()


    expect(await screen.findByText('Maybe')).toBeInTheDocument()


    })


    View full-size slide

  54. Apollo client case #2
    60

    View full-size slide

  55. 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(











    )


    expect(await screen.findByText('Loading...')).toBeInTheDocument()


    expect(await screen.findAllByText('Hello World')).toHaveLength(2)


    })

    View full-size slide

  56. 62
    // raw-transformer.js


    module.exports = {


    process(sourceText) {


    return {


    code: `module.exports = ${JSON.stringify(sourceText)}`,


    }


    },


    }


    // jest.config.js


    module.exports = {


    // ...


    transform: {


    '.(graphql|gql)$': '/raw-transformer.js',


    },


    }


    Как завести Jest?

    View full-size slide

  57. Как защититься от злобных
    уничтожителей полей?
    63

    View full-size slide

  58. Версионирование схем
    • В GraphQL отсутствует :)


    • Можно использовать директиву @deprecated
    64

    View full-size slide

  59. Что такое директива?
    65
    directive @typeClass(name: String!) on FIELD
    query {


    allTypes @typeClass(name: "Applicative") {


    name


    id


    }


    }

    View full-size slide

  60. Вот новая версия схемы
    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!


    }


    View full-size slide

  61. А вот так мы ищем 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>((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"]

    View full-size slide

  62. Добавьте эти проверки на CI
    68
    graphql-inspector вам в этом поможет

    View full-size slide

  63. Apollo Studio
    • Трекает изменение схем


    • Собирает метрики


    • Поддерживает суперграф


    • Валидирует схемы


    • Доки тоже есть
    69

    View full-size slide

  64. Вот и все. Что там в нашей книжечке теперь?
    • Мы разобрались с тем как устроена GraphQL схема, как ее парсить и траверсить.


    • Мы посмотрели на различные представления GraphQL схемы и подумали о том,
    как они могут помочь в разработке и коммуникации с командой.


    • Мы понимаем как и зачем валидировать запросы, как настроить свою IDE и CI
    чтобы при помощи схемы сделать наше приложение более надежным.


    • Ленивые Эффективные разработчики теперь будут еще эффективнее, ибо знают
    как настроить codegen.


    • Мы посмотрели как писать и рефакторить тесты для наших ографкуэленых
    компонентов.


    • Мы научились правильно депрекейтить поля и готовы запустить нашу схему в
    космос.
    70

    View full-size slide

  65. Если спелбука не достаточно, у меня есть
    роман в 6 томах
    71
    https:/
    /hellsquirrel.dev/blog/declarative-schema-parsing-1

    View full-size slide

  66. Спасибо
    :
    )
    72
    bit.ly/3K4VdJh

    View full-size slide