$30 off During Our Annual Pro Sale. View Details »

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

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

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

Polina Gurtovaya

August 16, 2022
Tweet

More Decks by Polina Gurtovaya

Other Decks in Programming

Transcript

  1. None
  2. Декларативность и магия схем

  3. 3

  4. 4

  5. 5

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

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

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

    на стороне системы. 8
  9. Query Language 9

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

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

    то чего нет. 11
  12. Магия парсинга 12

  13. 13 query GetAllPages { allPages { id text } }

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

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

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

    Syntax Error: Expected Name, found "{".
  16. Как работает парсинг? • Берете грамматику • Берете запрос •

    Строите 🌳 16
  17. Как импортировать .graphql ? • raw-loader • graphqh-tag/loader 17

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

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

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

    всех типов) • страничка • описание типа 20
  21. 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! }
  22. Как выполнять запросы? • Для каждого поля выдаем значения, призывая

    resolver • Хорошие новости: не нужно определять resolver для каждого поля 22
  23. 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": [] }
  24. Путь ленивых: 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 }
  25. 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" } ] } }
  26. Hello world? Мы можем круче 26 const schemaWithMocks = addMocksToSchema({

    schema: schemaAst, mocks: { AlgebraicDataType: () => ({ name: 'Maybe', description: ‘Just or Nothing!', parametersCount: 1, }), }, })
  27. 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!" } ] } }
  28. Apollo link spell 28

  29. 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 }
  30. GraphQL server spell Можно поднять graphql-сервер и выполнять execute на

    заглушках. • graphql-js + express • graphql-yoga • аpollo-server 30
  31. Магия визуализации 31

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

  33. Интроспекция 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>
  34. Документация • GraphQL Explorer • GraphiQL 34

  35. 35

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

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

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

    для вашей IDE. • не забыть гонять проверки на CI 39
  40. graphql.conf i g.js 40 // graphql.config.js const path = require('path')

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

    strangeDynamicQuery = (fieldName: string) => gql` query GetType { ${fieldName} { name description } }
  42. Раздел для ленивых волшебников 42

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

    в нем ошибку. 43
  44. 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> </> )} </> ) }
  45. 45 export declare function useQuery<TData = any, TVariables = OperationVariables>(

    query: DocumentNode | TypedDocumentNode<TData, TVariables>, options?: QueryHookOptions<TData, TVariables> ): QueryResult<TData, TVariables>;
  46. graphql - codegen и вжух 46

  47. graphql - codegen 47 # codege.config.yml overwrite: true schema: 'graphql/schema.graphql'

    generates: generated.tsx: plugins: - typescript Крутая система плагинов позволяет генерировать что угодно. Умеет делать интроспекцию.
  48. 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'] }
  49. Полезные плагины • typescript • typescript-operations • typescript-react-apollo • introspection

    • typescript-apollo-client-helpers 49
  50. Заклинание уборки 🧹 50 Вот это добавить в конфиг codegen

    hooks: afterAllFileWrite: - eslint --fix
  51. Трестирующая магия 51

  52. Apollo client • Делает запросы • Получает ответы • Нормализует

    • Складывает все в cache 52
  53. Про архитектуру при этом думать надо вам : ) •

    Запросы прямо в компонентах. Так удобно… да? 53
  54. Минус 10 к защите от Apollo 54

  55. Вот так делать не надо 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() })
  56. Что тестировать? • Сложную логику процессинга ваших GraphQL данных. •

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

    в вашим GraphQL движком. Если у вас такой нет, подумайте все ли окей с архитектурой вашего приложения :) Забудьте что у вас есть настоящий сервер Замокайте вашу систему Используйте моки в тестах
  58. Apollo client case 58

  59. 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() })
  60. Apollo client case #2 60

  61. 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) })
  62. 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?
  63. Как защититься от злобных уничтожителей полей? 63

  64. Версионирование схем • В GraphQL отсутствует :) • Можно использовать

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

    { allTypes @typeClass(name: "Applicative") { name id } }
  66. Вот новая версия схемы 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! }
  67. А вот так мы ищем 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"]
  68. Добавьте эти проверки на CI 68 graphql-inspector вам в этом

    поможет
  69. Apollo Studio • Трекает изменение схем • Собирает метрики •

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

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

    томах 71 https:/ /hellsquirrel.dev/blog/declarative-schema-parsing-1
  72. Спасибо : ) 72 bit.ly/3K4VdJh