Чтобы сделать свое веб-приложение гибким, я хотел отказаться от использования Firebase SDK и использовать GraphQL, но как мне запустить Apollo на Firebase?

После запуска альфа-версии Reciprocal.dev в октябре 2021 года я начал изучать, как предложение, которое мы собрали, согласуется с проблемой, которую мы намеревались решить.

Хотя я был доволен готовым продуктом, в моей голове было одно непреодолимое чувство — мы создали веб-приложение, которое позволяло пользователям создавать интерактивные карты пути пользователя, но когда мы впервые определили нашу потребность в таком продукте, мы создавали нашего пользователя. карты путешествий в Миро.

Осознав это, я провел проверку концепции того, как легко было бы принести предложения из нашего веб-приложения и перевести их в плагин Miro, и хотя результат этого мы все еще оцениваем. выявил потенциальную возможность для бизнеса, которая была бы заблокирована нашей технической реализацией.

Модель данных для альфа-версии была очень сосредоточена на данных, необходимых для веб-приложения, и мы широко использовали Firebase SDK, что означало, что поддержка нескольких интерфейсов для этих данных была нецелесообразной, учитывая разницу в том, как SDK каждой системы определяет сущности. на холсте и ограничения на библиотеки, которые можно использовать.

Я использовал GraphQL в своей повседневной работе и чувствовал, что функциональность условного типа хорошо подходит для нашей проблемы, поскольку она позволяет плагину Miro запрашивать представление объекта Miro, а веб-приложение может запрашивать представление веб-приложения. , сохраняя при этом структуру остальных данных прежней.

Использование GraphQL (или любого другого API) также позволит создать внешний интерфейс на основе интерфейса, что означает, что мы можем провести рефакторинг внутреннего интерфейса, не влияя на внешний интерфейс, при условии, что интерфейс не сломан.

На внешнем интерфейсе решение об использовании API также должно устранить зависимость от Firebase SDK для внутренних вызовов, поскольку я использовал functions.httpsCallable для экономии времени, поскольку это упрощает работу с аутентификацией во внутренней реализации. .

Без необходимости использовать Firebase SDK для аутентификации, загрузки/сохранения данных и работы с хранилищем, это означает, что интерфейс теперь будет более переносимым, если мы захотим перейти на другой технический стек.

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

Шаг 1: Создание схемы

В отличие от REST API, который зависит от вспомогательной информации (Swagger, схема JSON и т. д.) для определения типа объекта, возвращаемого из API, GraphQL встраивает эти типы в само определение API с помощью схемы, которая не только определяет тип возвращаемых объектов, а также операции и аргументы для этих операций.

Если вы использовали схему JSON раньше, вам должно быть относительно легко работать со схемами GraphQL, хотя есть несколько отличий, таких как отсутствие в GraphQL встроенной поддержки объектов Map / Object / Dict и необходимость иметь отдельные Input типы для аргументов, даже если они совпадают с другим типом в схеме.

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

Мне нравится использовать записи архитектурных решений (ADR) в моих кодовых базах, так как это позволяет мне собирать информацию о решениях, которые я принимаю, поэтому у меня есть заметки для просмотра позже, если мне придется рефакторить мой код. Поэтому я создал новый ADR, чтобы зафиксировать решение изменить существующую модель данных, которая была реализована путем создания набора классов для каждой сущности в TypeScript и создания схемы GraphQL с этими объектами, используя инструменты генерации кода для создания типов, которые можно было бы использовать. вместо этого в TypeScript.

В рамках ADR я зафиксировал изменения, которые я буду вносить в объекты Step и Connection, чтобы разрешить определение нескольких представлений этих объектов, чтобы система могла поддерживать несколько интерфейсов.

Я использую Русалку для определения диаграмм классов, чтобы было визуальное представление изменений, так как это легче понять.

При построении схемы я использовал инструмент graphql-schema-linter, чтобы убедиться, что моя схема действительна (хотя мне пришлось отключить правила, связанные с Relay, из-за конфликта имен с моей сущностью Connection), и как только схема была построена, я запустил локальный сервер Apollo. для проверки правильности регистрации определений.

Шаг 2. Запуск сервера Apollo на Firebase Functions

Создав схему и убедившись, что она верна, я перешел к выяснению того, как запустить сервер GraphQL в функциях Firebase. Я выбрал для этого Аполлон, так как именно с сервером GraphQL у меня был наибольший опыт.

Первоначальный поиск в Google того, как это сделать, дал два подхода: один — использовать apollo-server-express, а затем использовать это, поскольку функции Firebase совместимы с Express, а другой — использовать apollo-server-cloud-functions, который обеспечивает оболочку поверх другого подхода, поэтому меньше кода для написания.

Я выбрал подход apollo-server-cloud-functions, следуя описанию Фабио (https://medium.com/@piuccio/running-apollo-server-on-firebase-cloud-functions-265849e9f5b8) но поскольку я использую TypeScript в своей кодовой базе функций Firebase, я все равно столкнулся с проблемой в apollo-server-express, поскольку определения типов для CORS вызывали ошибки компиляции.

Чтобы решить проблему с типами в apollo-server-express, мне пришлось добавить skipLibCheck: true в мой tsconfig.json, который я нашел в этой проблеме Github: https://github.com/apollographql/apollo-server/issues/927#issuecomment-445537360

Как только я решил проблему с типами, я смог запустить сервер Apollo на эмуляторе Firebase (устанавливаемый через пакет firebase-tools), а после копирования моей схемы GraphQL и реализации некоторых фиктивных преобразователей я смог использовать A pollo Sandbox Explorer работает с URL-адресом эмулятора для конечной точки Apollo для тестирования API.

Шаг 3. Делаем схему GraphQL более полезной

Хотя схема, используемая сервером GraphQL, отлично подходит для того, чтобы позволить потребителям API знать, что отправлять и что они получат, эта схема была бы еще более полезной, если бы сам код приложения мог использовать типы сущностей для более быстрого обнаружения ошибок.

К счастью, есть способ сделать это с помощью инструмента под названием graphql-codegen, который берет схему GraphQL и преобразует ее в код. В моем случае мне просто нужно было сгенерировать набор типов, которые я мог бы использовать в своем коде TypeScript, но существует множество способов использования схемы с помощью этого инструмента.

Чтобы сгенерировать нужные мне типы, я настроил graphql-codegen для создания файла types.d.ts, включающего операции и преобразователи.

После того, как у меня были типы, сгенерированные из схемы, следующим шагом была публикация как схемы, так и типов, чтобы они могли использоваться нижестоящими службами, такими как мой код функций Firebase, веб-приложение и плагин Miro.

Для этого я убедился, что и файл схемы, и сгенерированный types.d.ts находятся в корне репозитория (то есть не в каталоге), прежде чем добавлять их в массив files в package.json. Это будет означать, что при запуске npm publish оба файла будут упакованы и доступны из корня библиотеки.

Шаг 4: Загрузка схемы GraphQL из библиотеки

После публикации схемы и типов в виде библиотеки для последующего использования следующим шагом будет импорт схемы из этой библиотеки в конфигурацию сервера Apollo, используемую функцией GraphQL Firebase.

Чтобы загрузить схему из файла и использовать ее в качестве значения typeDefs конфигурации Apollo, мне нужно было импортировать GraphQLFileLoader из библиотеки @graphql-tools/graphql-file-loader и передать ее экземпляр в аргумент loaders для loadTypedefsSync из библиотеки @graphql-tools/load.

Шаг 5: Аутентификация пользователя

Помимо одного или двух исключений, большинство операций, которые будет обрабатывать API, который я создаю, требуют аутентификации пользователя, чтобы серверная часть могла работать с записями, к которым у них есть доступ.

Во внутренней альфа-реализации любая потребность в аутентификации решалась с помощью функций requests.onCall, поскольку они обрабатывали аутентификацию и предоставляли данные аутентификации функции через объект контекста.

В альфа-версии интерфейса эти функции будут вызываться с использованием functions.httpsCallable, который является частью Firebase SDK. Это позаботится о любой аутентификации, необходимой внешнему коду.

Зависимость альфа-реализации от Firebase SDK означала, что у меня не было существующей реализации для аутентификации за пределами этого SDK. К счастью, SDK администратора Firebase предоставляет средства для создания JWT для пользователя и средства для проверки этих токенов, когда они используются для аутентификации.

Сервер Apollo не определен как обычная функция Firebase, поэтому я не мог просто добавить проверку в начало реализации функции, вместо этого мне нужно было бы использовать свойство context конфигурации сервера Apollo, чтобы определить обратный вызов, который мог бы проверьте аутентификацию и добавьте любые соответствующие значения в контекст Apollo, которые позже могут использоваться преобразователями.

Этот контекст Apollo оказался весьма удобным не только для получения идентификатора пользователя из JWT, но и для предоставления распознавателям доступа к экземпляру Firestore, который был создан в модуле функций верхнего уровня, поскольку это позволило разделить код для упрощения. обслуживание.

Создание JWT

Административный SDK Firebase имеет функцию admin.auth().createCustomToken, которая может создать JWT, который можно использовать для аутентификации пользователя. В функцию можно передать набор пользовательских данных для создания токена, причем минимальным требованием является предоставление идентификатора пользователя.

Проверка JWT

В обратном вызове контекста GraphQL к JWT можно получить доступ из объекта запроса, переданного в обратный вызов через request.headers.authorization path.

Функция проверки JWT требует, чтобы строка содержала только токен, поэтому при чтении заголовка authorization вам нужно будет удалить Bearer часть строки.

Административный SDK Firebase имеет функцию admin.auth().verifyIdToken, которая декодирует JWT в объект, из которого вы можете прочитать закодированные значения. Идентификатор пользователя будет доступен в свойстве uid этого декодированного объекта.

Проблемы с эмулятором

JWT, созданный с помощью createCustomToken, не может быть проверен verifyIdToken, поскольку свойство aud отличается от ожидаемого verifyIdToken. Чтобы создать токен с правильным свойством aud, необходимо выполнить дополнительный вызов, который создаст JWT, который можно проверить.

После того, как вы сгенерировали JWT через createCustomToken, вам нужно отправить этот токен как часть POST конечной точки /www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken, доступной через эмулятор аутентификации, который, когда вы установите returnSecureToken в true, вернет JWT, который будет работать правильно. Код на следующем шаге показывает, как это сделать.

Шаг 6: Проверяем, все ли работает

Теперь аутентификация настроена в Apollo, пришло время попробовать пример вызова, но для этого нам нужно создать пользователя, создать токен JWT для его аутентификации и создать некоторые тестовые данные для возврата.

Если вы еще не включили аутентификацию или эмуляторы Firestore, вы можете запустить firebase init и выбрать дополнительные эмуляторы для установки.

В эмуляторе аутентификации вам будет показан список пользователей с кнопкой «Добавить пользователя», при нажатии которой вы увидите форму для создания нового пользователя. Вы можете создать нового пользователя, указав адрес электронной почты и пароль.

После того, как вы создали пользователя, у вас будет пользователь в системе, вы можете создать JWT для использования createCustomToken, но вам нужно будет указать UID пользователя для этой функции, а созданный токен нужно будет преобразовать в принятый эмулятор. токен через verifyCustomToken .

Чтобы упростить себе задачу, я перенес управление токенами и создание тестовых данных в конечную точку, на которую я мог отправить идентификатор пользователя, и это вернуло удобный для эмулятора JWT.

Затем я использовал возвращенный JWT для установки заголовка авторизации в пользовательском интерфейсе Apollo Sandbox Explorer (в нижней части панели операций есть средства для предоставления переменных и заголовков).

После предоставления некоторых переменных, необходимых для запроса, который я хотел выполнить, я смог успешно выполнить аутентифицированный вызов GraphQL для сервера Apollo, работающего на эмуляторе Firebase Functions.

Краткое содержание

Запуск GraphQL на Firebase относительно прост, но аутентификация требует немного больше работы, если вы хотите протестировать что-то локально через эмулятор Firebase.

Перейдя от использования Firebase SDK для взаимодействия с Firestore к реализации API для этого, мы теперь получили гораздо больше гибкости в том, как мы рефакторим приложение и как мы будем расширять Reciprocal.dev.

Используя инструменты graphql-codegen, я могу иметь один источник достоверности для сущностей в моем приложении через схему GraphQL, а затем генерировать код, помогающий нижестоящим использовать эти сущности, такие как определения типов для TypeScript.

Дальнейшее чтение







Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord . Заинтересованы в хакинге роста? Ознакомьтесь с разделом Схема.