https://graphql-wrike.herokuapp.com/
https://graphql-wrike.herokuapp.com/voyager
By default is using token from my demo account. But you may provide your token via Authorization
header:
Get your Authorization token here: https://www.wrike.com/frontend/apps/index.html#/api
Docs about Wrike's authorization: https://developers.wrike.com/oauth-20-authorization/
docker run --rm -p 3000:3000 -e AUTH_TOKEN="XXXX" docker.io/nodkz/wrike-graphql:latest
yarn add axios debug
- Настраиваем axios
- Настраиваем дебаггинг
- пишем первый примитивный тест
yarn add graphql apollo-server ts-node-dev
- добавляем dev-скрипт
DEBUG=axios:request ts-node-dev --no-notify --respawn --watch src/schema src/server.ts
- поднимаем болванку схемы
yarn add graphql-compose graphql-compose-modules
Документация АПИ метода https://developers.wrike.com/documentation/api/methods/query-tasks
- Проблемы апи
- Микс из полей (фильтр, сортировка, проекция, поиск по subTasks)
- Тяжело подобрать формат
- например
fields
надо передавать как['name1', 'name2']
effortAllocation
,billingType
в проекции работают только для каких-то платных аккаунтовrecurrent
вообще не возвращает данные
- например
- Есть
limit
, но нетoffset
- можно использовать курсор
nextPageToken
, только он протухает через час-два (и опять запрашивать все данные сначала)
- можно использовать курсор
- Пишем примитивный тест
- Добавляем в фильтр поля
folderId
иspaceId
, которые по капотом будут роутиться на другие эндпоинты - Т.к. есть проекция полей, то необходимо обернуть
findMany()
и добавить в него обработкуinfo: GraphQLResolveInfo
полученную в резолвере, чтоб она конвертировалась вprojection
- Возвращаются все поля без проекции как в
task/findMany.ts
- Можно запросить до 100 записей
- Пишем примитивный тест
- Проблема дат в разных форматах
- ❌ startDate={"start":"2020-03-06T14:47:49.000Z"}
- ❌ startDate={"start":"2020-03-06T14:47:49Z"}
- ✅ startDate={"start":"2020-03-06T14:47:49"}
- ✅ startDate={"start":"2020-03-06"}
- ❌ createdDate={"start":"2020-03-06"}
- ✅ createdDate={"start":"2020-03-06T14:47:49Z"}
- ❌ createdDate={"start":"2020-03-06T14:47:49.000Z"}
- Не информативные ошибки
- "errorDescription": "Parameter 'createdDate' value is invalid",
- Проблема с курсорной пагинацией
pageSize=2
, если задан текстовый поискtitle="TaskList"
– не возвращается курсор и кол-во элементов
- Создаем TaskTC.ts
yarn add graphql-compose-json
- Добавляем в схему поле
taskByIds.ts
- Добавляем в схему поле
taskFindMany.ts
GraphQL запрос:
{
taskFindMany(
filter: { title: "TaskList", createdDate: { start: "2020-03-06T14:47:49Z" } },
limit: 3,
sort: CREATED_DATE_DESC
) {
id
createdDate
title
sharedIds
hasAttachments
authorIds
}
}
трансформируется в
axios:request ✅ 200 get /tasks
axios:request title="TaskList"
axios:request createdDate={"start":"2020-03-06T14:47:49Z"}
axios:request limit=3
axios:request sortField="CreatedDate"
axios:request sortOrder="Desc"
axios:request fields="[\"hasAttachments\",\"sharedIds\"]"
- Вендор АПИ метод vendor/task/create.ts
- Заводим Скаляры и Энумы
- Заводим первую мутацию taskCreate
- 🛑 Боль
- Ушло порядка 1 часа чтобы описать все поля (нет статической типизации)
- Есть пример CURL-запроса, но нет примера JSON data (я б за 5 сек сгенерил инпут тип)
- Нахожусь в Task а отправлять POST-запрос нужно в папки
/folders/${folderId}/tasks
- Неочевилное наличие полей
add
,remove
- для
parents
,shareds
,responsibles
- есть метод
addFollowers
, но нет методаremoveFollowers
(баг или фича? 😜) - нет простой возможности отредактировать
parents
,shareds
,responsibles
,followers
– типо получили таск, а потом просто отправилили новые массивы с данным. Их надо на клиенте дробить на два массива: кого добавлять, кого удалять. - делаем методы добавления/удаления
parents
,shareds
,responsibles
ЯВНЫМИ, т.е. отдельными мутациями
- для
- 🛑🤔 А где endpoint по работе с
CustomStatuses
?CustomStatusID
есть, а ендпоинта нет.
Документация АПИ метода https://developers.wrike.com/documentation/api/methods/query-user
- Можно запросить одного пользователя, но возвращает массив
- Пишем простой тест
- создаем
UserTC.ts
- в схему добавляем
userById
- в
TaskTC
добавляем новые поля (связи), которые будут возвращать пользователей:- shareds
- responsibles
- authors
- followers
- в
ContactTC
добавляем новые поля (связи), которые будут возвращать таски- tasksAuthored
- tasksResponsible
- Чем отличаются
Contacts
отUsers
(типы вроде как индентичны) - GET /contacts нет сортировки, пагинации, слабая фильтрация
- Поле
metadata
может хранить массив{ key/value }
, но фильтровать может только по одному ключу
- GET /groups нет сортировки, пагинации, слабая фильтрация
- Чутка непонятно как из контактов сделать Union type из Groups и Users
- Странно что группу можно отредактировать (metadata) через два места
put /groups/:id
иput /contacts/:id
addMembers
,removeMember
вынесены в отдельные методы
- Странно что эндпоинт не заканчивается на
s
–GET /account
- Странно что есть
Metadata filter
для получения аккаунта, хотя метод редактирования не поддерживает передачу AccountID. Какой смысл фильтровать по мете?
- Объединяем 3 ендпоинта получения списка папок
- 🛑 Ошибка в описании апи
Folder.project.status
может вернуть пустое значение - 🤬 Почему я не могу запросить данные рутовой папки через
/folders/IEADMUW4I7777777
падает с ошибкой "Operation is not allowed for logical folder". Пипец как не удобно дергать их через аккаунт. Ну и бог с ним, что папка виртуальная: вы же этот айдишник возвращаете в тасках – верните и через folders эндпоинт ее данные. - ❓Не описаны типы для 10 полей, которые возвращаются через
fields
Note: when any of query filter parameters are present (e.g. descendants=false, metadata) response is switched to Folder model.
– эта штука убивает при поиске 10 полей, которые возвращаются черезfields
. Понятно что нагрузка, но надо посмотреть как вернуть дляdescendants=true
- Что такое
customColumnIds
и как оно коррелирует сcustomFieldIds
? - Печально что папка не возвращает массив родителей 😢
- ❗️Кстати, нельзя при создании папки указать несколько родителей. А при редактировании можно.
- Завел отдельные мутации для
addParents
,removeParents
,addShareds
,removeShareds
- Заводим асинхронное копирование папок (какое-то детское мак кол-во для копирования – 250)
- Честно вообще на первый взгляд не понятно как эта штука работает
- Метод modify позволяет только отредактировать один статус, а их в самой моделе массив. Плюс тип у параметра
customStatus
не расписан (надо гадать). - У кастомного статуса свой набор цветов
StatusColorEnum
, хотя уже существует расширенный набор цветовColorEnum
. Специально два разных типа, или просто дубль?
- ❓ А где метод удаления?
- Объединяем GET endpoint's в один (ставим taskId, folderId в параметры фильтра)
- Для списка комментов, нет сортировки, нет пагинации
- А вообще есть ли полнотекстовый поиск в АПИ 🤔
- Нет возможности получить реакции к комментарию через АПИ
- Методы не проверены, т.к. нет премиум доступа
- Странно что через API нет возможности завести свои категории
- Чем ProjectContractTypeEnum (в Folders) отличается от BillingTypeEnum (в Timelogs)?
- 🤯🤯🤯 Что трекается только по целым часам? И почему диапазон от 0 до 24? Клиенты наверняка страдают от такого негибкого трекинга.
- Объединяем 5 ендпоинтов в один через фильтр по полям folderId, taskId, contactId, timelogCategoryId.
- GET /timelog_categories/{timelog_categoryId}/timelogs содержит параметр
timelogCategories
. Странно это. - При создании и редактировании параметр plainText и fields не относятся к редактируемому объекту и должны быть вынесены из Input типа.
- ❗️ Странно что trackedDate пишем в формате yyyy-MM-dd, а ищем в формате yyyy-MM-dd'T'HH:mm:ss'Z'
- Ну вот тут пришло время работать с бинарниками – тут GraphQL не нужен!!! Не реализовываем методы create, modify, download. 🤘
- 🛑🤔 А где endpoint по работе с
Review
?ReviewID
есть, а ендпоинта нет. - Странно но в описании не подставили типы для taskId, folderId, commentId (до последнего думал что апи сгенерировано, но оказца аккуратно все написано руками)
- Да, много где встречаю по АПИ, что поле required но его может не быть, если приходит другое поле. Например либо taskId, folderId и commentId. В любом случае эти поля уже опциональны. previewUrl тоже опциональный.
- Когда запрашиваем
url
и его не вернул сервер (например через /attachments/{attachmentId},{attachmentId}) то делаем подзапрос, чтоб получить урл на /attachments/{attachmentId}/url
- Странно что под текущую 4ую версию возвращается
{ major: 1 minor: 0}
- Добавляем вычисляемое поле
full
- Вжух, и готово за 1 минуту
- Вжух, и готово за 30 секунд
- Доступен только на интерпрайзе
- Сперва подумал, что зря все схемы руками состовлял. Можно было сгенерить GraphQL-схему из этих файлов. Но по доке увидел что не все Ентити заведены и всего 4 базовых типа. Получилась бы кака схема, и без привязки к эндпоинтам.
- Нет интерпрайз доступа, метод не проверен.
- Опять taskId и folderId required-поля, хотя может быть только одно.
- Забыли добавить поле
id
в описание Response - В APPROVAL DECISION описане начинает со строчной буквы. Реально везде с прописной, а тут со строчной - во мне перфекционист плачет 😜
- ❓ А как вообще заапрувить Апрувалс??? Не нашел способа в публичном апи, либо не понял. Т.е. я могу себя добавить в Approvers, но не могу поставить статус для своего апрува.
- Метод Update разбил на 5 GraphQL-методов, основной и addApprovers, removeApprovers, addAttachments, removeAttachments
- Странно что пагинация есть, а сортировки нет.
workweek
плохо расписан тип возвращаемых данных- Обновление workweek разнес на 3 мутации – update, addUsers, removeUsers
- Не удалось протестировать методы, т.к. ограничения по аккаунту
- Странно но endpoint называется
exclusions
. Чревато опечатками.
Даталоадеры позволяют решить проблему N+1 и сократить кол-во HTTP запросов к REST API.
- Находим 9 entity которые имеют findByIds метод
- Пишем генераторы DataLoader'ов
- 8 глобальных (записи возвращаются полностью, смело можно использовать глобально в рамках запроса)
- 4 fieldNode-specific дата-лоадера (зависят от запрошенных полей в запросе)
- Пишем генераторы резолверов
resolveManyViaDL
иresolveOneViaDL
- Подключаем к TaskTC наши генераторы даталоадеров (старые методы комментируем для примера)
Relations/Резолверы позволяют избавиться от копипасты. Позволяют расширять логику, например добавлять аргументы, оборачивать в даталоадеры и пр.
- Создал 14 резолверов (генераторов FieldConfig)
- Резолверы по-умолчанию используют Даталоадеры (можно отключить через
DISABLE_DATALOADERS
) - Подцепил резолверы по прямым связям ко всем Entity -
49 штук
(можно отключить черезDISABLE_RELATIONS
)- Пару связей уже было в РЕСТ АПИ, например
Folder.customFields
- Пару связей уже было в РЕСТ АПИ, например
- Прикрутил обратных связей по id –
25 штук
(можно отключить черезDISABLE_BACK_RELATIONS
) - Рсширяем дополнительными аргументами реляции:
- getRelationApprovalsByApproverUserId
- getRelationApprovalsByPendingApproverUserId
- getRelationAttachmentsByFolderId
- getRelationAttachmentsByTaskId
- getRelationFoldersBySpaceId
- getRelationTasksBySpaceId
- getRelationTasksByResponsibleId
- getRelationTasksByAuthorId
- getRelationTasksByFolderId
- getRelationTasksBySpaceId
- getRelationTimelogsByContactId
- getRelationTimelogsByFolderId
- getRelationTimelogsByTaskId
- getRelationTimelogsByTimelogCategoryId
- getRelationUserScheduleExclusionByUserId
Создали плагин к аполло серверу queryCostPlugin
и подключили его.
QueryCost отрабатывает перед запуском выполнения запроса. Фактически мы пытаемся посчитать максимально возможно кол-во полей, которое вернет сервер, исходя из запроса и переданных переменных. И если он будет больше 10000, как в нашем примере, то пользователю вернется ошибка.
- Добавлять
complexity
к релейшенам, которые возвращают списки- плохо, что не везде есть лимиты и сортировки (аттачменты, комменты) и мы запрашиваем тупо по ID
- если нет аргументов limit или pageSize, то ставим
extensions: { complexity: ({ childComplexity }) => childComplexity * 10 }
(считаем что в списках в среднем возвращается 10 элементов) - 🛑 особенно печально, что поиск Folders нельзя итерировать. И чер его знает сколько там може вернуться записей, поэтому тяжело спрогнозировать сложность запроса.
- ☝️ Везде где используется
childComplexity * 10
(без учета аргументов) крайне важно прикрутить лимиты. Иначе есть риск потерять сервер если прилетит мааааленький запрос, но с большой вложенностью – то серверу может не хватить памяти его обработать.
- Добавляем
complexity
ко всем полям Query (точкам входа в наш граф)- для finsByIds есть возможность посчитать кол-во запрошенных данных (например query.approvalByIds). А вот релейшенах такой возможности нет, т.к. надо сперва выполнить запрос, чтоб узнать сколько записей будет запрошено.
- 🛑🛑🛑 неее ну вы серьезно вернете мне миллион моих аттачментов, если я сделаю запрос
GET /attachments
. Камон, нужно везде крутить лимиты и не вываливать на бедную голову фронта километровые ответы, нехай пагинирует если ему всё надо.
- Пробрасываем Authorizaition заголовок, а также куки (на вырост) в контекст. Затем этот контекст пробрасываем в axios, чтоб он мог их использовать в своих подзапросах.
- 🔥 Боль и печаль – пришлось отредактировать больше 200 файлов. Надо сразу заводить.
docker build -t nodkz/wrike-graphql .
docker push nodkz/wrike-graphql:latest
docker run -it --rm wrike-graphql:latest /bin/sh
yarn docker-build
RUN VIA
docker run --rm -p 3000:3000 -e AUTH_TOKEN="XXXX" docker.io/nodkz/wrike-graphql:latest
heroku login
heroku container:login
heroku container:push web -a graphql-wrike
heroku container:release web -a graphql-wrike
- Добавить кастомные поля с привязкой к гитхабу. Например к коммитам для прикручивания федерации. В другой раз, не расспыляемся на федерацию/mesh.
- Сделать пример для нового v4 АПИ (идея была собрать на АполлеКлиенте простенькое приложение. Но и так было потрачено на все апи нереально много времени. В топку идею.)
- Заюзать Web-hooks для сабкрипшенов (лажа, нужен полноценный PubSub. Что если запущено 5 инстансов сервера? Нужно деплоить пример и потом вязать хуки в аккаунте – костыльно как-то. Нет смысла неправильным вещам учить людей).
-
Когда обарачивал Task по response Json, то упустил кучу специфичных полей и получил неполное описание типа (упустил CustomFields, recurrent, dependencies потерял документацию)
-
Микросервисная боль: Несколько сервисов необходимо вызывать, чтобы создать пользовательский аккаунт, назначить права, создать саб-аккаунт. Печально если ваши клиенты занимаются таким колхозом.
-
ВАЖНАЯ МЫСЛЬ: бэкендеры привыкли подстраивать РЕСТ АПИ под текущие требования приложения. Зачастую АПИ не готово под новые требования фронтендеров (еще чаще сами фронтендеров тупят, не зная что оказца можно запросить). К примеру, Графкуэль позволяет описать все связи между сущностями; что позволяет клиенту запросить любые связные данные под его задачу не дожидаясь бэкендера. Например, нет возможности получить календарь для конкретного пользователя (сперва дергай все календари, затем перебором ищи в нем нужного пользователя, а потом только рисуй календарик). Т.е. проблема в передаче знаний о связях между Entities в вашей Data Domain.