Создание API-маршрута Next.js для получения данных спонсоров GitHub

Изображение обложки с сайта tinykat.cafe

Вступление

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

Кроме того, настройка GraphQL в проекте, где будет использоваться только один запрос, казалась мне излишеством. В любом случае, я обнаружил, что мы можем отправить запрос GraphQL в обычном запросе REST API, используя параметр body. Для справки я использовал этот репозиторий.

TL:DR; Вы можете найти доработанный код проекта по адресу https://github.com/jahirfiquitiva/sponsors-edge-api.

Запрос GraphQL

У меня ушло довольно много времени на изучение документации GitHub GraphQL API и создание запроса, чтобы получить всю необходимую информацию, поэтому я пропущу подробности. В основном я использовал их GraphQL Explorer и просмотрел множество Interfaces, Objects и других данных, выполняя процесс проб и ошибок, чтобы получить окончательный запрос.

Некоторые из данных, которые я использовал, были следующими:

  • Пользователь
  • Спонсируемый
  • SponsorConnection
  • SponsorsListing
  • SponsorsTierConnection
  • SponsorsTier
  • SponsorsTierAdminInfo
  • Спонсорство
  • Спонсор

Окончательный запрос выглядит следующим образом:

{
  user(login: "jahirfiquitiva") {
    sponsorsListing {
      id
      tiers(first: 20) {
        nodes {
          ... on SponsorsTier {
            id
            adminInfo {
              sponsorships(first: 100) {
                totalRecurringMonthlyPriceInDollars
                nodes {
                  ... on Sponsorship {
                    sponsorEntity {
                      ... on User {
                        login
                        avatarUrl
                        name
                        websiteUrl
                      }
                      ... on Organization {
                        login
                        avatarUrl
                        name
                        websiteUrl
                      }
                    }
                    tierSelectedAt
                  }
                }
              }
            }
            monthlyPriceInDollars
            isOneTime
            isCustomAmount
            name
            description
          }
        }
      }
    }
    ... on Sponsorable {
      sponsors {
        totalCount
      }
    }
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Что делает этот запрос?

В основном, он получает данные о спонсорах GitHub и общее количество спонсоров для пользователя, определенного в начале запроса: user(login: "jahirfiquitiva") здесь я использовал свое имя пользователя GitHub, но вы можете заменить его своим.

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

Из данных списка спонсоров я получаю различные уровни. Уровень — это, по сути, вариант пожертвования, например, у меня есть 6 ежемесячных уровней: звезда, хрустальный шар, ракета, робот, молния и бриллиант, основанные на разной цене. Это потому, что я «назвал» их, хотя на самом деле у них нет названия по умолчанию. Это просто помогает мне распределять спонсоров по категориям.

tiers(first: 20) вернет первые 20 уровней из списка ваших спонсоров. Как я уже сказал, у меня есть 6 ежемесячных и 3 разовых тиеров: всего 9 тиеров, так что даже 20 — это больше, чем нужно. Кроме того, у вас может быть только 10 опубликованных ежемесячных и 10 опубликованных разовых тиеров.

С каждого тиера я получаю следующую информацию:

      • Кроме того, sponsorEntity включает tierSelectedAt, который определяет дату и время, когда данный уровень был выбран для спонсорства.

Если вам нужна дополнительная информация о спонсорах или спонсорстве, вы можете изучить документацию.

Авторизация

Прежде чем мы действительно используем этот запрос для доступа к этим данным, мы должны создать Personal Access Token, поскольку для этого требуется авторизация.

Для этого зайдите в Настройки вашего аккаунта на GitHub, затем перейдите в Настройки разработчика и, наконец, выберите Personal Access Tokens, или просто перейдите по этой ссылке: https://github.com/settings/tokens.

Там нажмите на кнопку Generate new token, дайте ему определенное имя, установите срок действия по своему усмотрению, хотя этот токен только считывает данные, поэтому я думаю, что No expiration вполне подойдет.

Для этого запроса требуются следующие диапазоны:

  • [ ] admin:org
    • [x] read:org
  • [ ] user
    • [x] read:user

Затем прокрутите вниз и нажмите на Generate token.

Убедитесь, что сохранили токен в безопасном и доступном месте, так как вы больше не сможете получить доступ к его значению.

Настройка проекта

Давайте создадим новый проект NextJS. В этом руководстве мы будем использовать TypeScript, поэтому сделаем это с помощью следующей команды:

npx create-next-app --ts {folder}
Войти в полноэкранный режим Выйти из полноэкранного режима

Замените {folder} на имя папки, в которой должен находиться проект.

Теперь откройте проект в вашем любимом редакторе или IDE.

Создайте файл .env.local с ранее сгенерированным Personal Access Token:

GH_PAT=ghp_XXxxXXxxXX
Войдите в полноэкранный режим Выйти из полноэкранного режима

Вы можете назвать переменную по-другому, но будьте внимательны, когда мы будем обращаться к ней в коде позже.

Начальный запрос

Давайте быстро настроим API и код для выполнения начального запроса.

Чтобы все было упорядочено, создадим папку lib в корне проекта, а внутри нее еще одну папку sponsors.

Создайте файл с именем request.ts :

const { GH_PAT: githubPat = '' } = process.env;

const graphQlQuery = `
...
`;

export const getSponsorsGraphQLResponse = async () => {
  return fetch('https://api.github.com/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${githubPat}` },
    body: JSON.stringify({ query: graphQlQuery }),
  }).then((res) => res.json());
};
Вход в полноэкранный режим Выйдите из полноэкранного режима

Поместите содержимое запроса, показанного в начале этого поста, внутри обратных знаков в graphQlQuery.

Здесь мы получаем GitHub Personal Access Token (PAT) через переменную окружения GH_PAT (установленную ранее в .env.local) и создаем функцию, которая будет выполнять простой fetch POST запрос к https://api.github.com/graphql, отправляя PAT в заголовке Authorization, затем получать JSON тело из ответа и возвращать его.

Теперь создайте файл с именем index.ts :

export * from './request';
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы просто экспортируем все, что уже экспортировано в файле request.ts.

Теперь давайте настроим маршрут API. Перейдите в файл pages/api/hello.ts и переименуйте его в sponsors.ts, там измените его так, чтобы он выглядел следующим образом:

import type { NextApiRequest, NextApiResponse } from 'next';
import { getSponsorsGraphQLResponse } from './../../lib/sponsors/request';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const rawResponse = await getSponsorsGraphQLResponse();
  return res.status(200).json(rawResponse);
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы импортируем ранее созданную функцию, затем вызываем ее с помощью функции async handler, затем получаем из нее тело JSON и возвращаем его в качестве ответа API.

Тестирование

Давайте протестируем наш простой API, для этого запустите npm run dev или yarn dev в терминале или CMD из корня проекта.

Когда проект будет запущен, вы увидите следующее:

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
Вход в полноэкранный режим Выход из полноэкранного режима

Далее, перейдите по адресу http://localhost:3000/api/sponsors, если все было настроено правильно, вы увидите необработанный ответ от API, формат которого не очень приятен для чтения или использования, и выглядит он примерно так:

{
  "data": {
    "user": {
      "sponsorsListing": {
        "id": "MDExxxxxxxxxxxxxxxx==",
        "tiers": {
          "nodes": [
            {
              "id": "MDExxxxxxxxxxxxxxxx==",
              "adminInfo": {
                "sponsorships": {
                  "totalRecurringMonthlyPriceInDollars": 2,
                  "nodes": [
                    {
                      "sponsorEntity": {
                        "login": "xxxx",
                        "avatarUrl": "https://avatars.githubusercontent.com/u/xxxxxx",
                        "name": "Xxxxx Xxxxx",
                        "websiteUrl": "https://jahir.dev/"
                      },
                      "tierSelectedAt": "2022-03-02T07:59:51Z"
                    }
                  ]
                }
              },
              "monthlyPriceInDollars": 2,
              "isOneTime": false,
              "isCustomAmount": false,
              "name": "$2 a month",
              "description": "Lorem ipsum dolor sit amet."
            },
            ...
          ]
        }
      },
      "sponsors": {
        "totalCount": 1
      }
    }
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Я рекомендую использовать расширение JSON viewer для более удобного чтения ответа.

Ввод необработанного ответа

Давайте определим интерфейсы для сырого ответа от GitHub GraphQL API. Создайте файл с именем types.ts в папке lib/sponsors.

Мы можем начать с самого глубокого вложенного объекта, которым будет sponsorEntity.

export interface SponsorEntity {
  login: string;
  name?: string;
  avatarUrl: string;
  websiteUrl?: string;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь мы можем перейти на один уровень вверх к sponsorships.

interface Sponsorships {
  totalRecurringMonthlyPriceInDollars: number;
  nodes: Array<{
    sponsorEntity: SponsorEntity;
    tierSelectedAt?: string; // TimeStamp
  }>;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Поскольку adminInfo включает только свойство sponsorships, давайте поднимемся на пару уровней вверх:

export interface SponsorsTier {
  id: string;
  adminInfo?: {
    sponsorships: Sponsorships;
  };
  monthlyPriceInDollars: number;
  isOneTime: boolean;
  isCustomAmount: boolean;
  name: string;
  description?: string;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь перейдем к sponsorsListing.

interface SponsorsListing {
  id: string;
  tiers: {
    nodes: Array<SponsorsTier>;
  };
}
Войти в полноэкранный режим Выйти из полноэкранного режима

И, наконец, весь ответ:

export interface SponsorsResponse {
  data?: {
    user: {
      sponsorsListing: SponsorsListing;
      sponsors: {
        totalCount: number;
      };
    };
  };
  message?: string;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь можно импортировать интерфейс SponsorsResponse в request.ts и ввести функцию getSponsorsGraphQLResponse, так что это будет выглядеть следующим образом:

import type { SponsorsResponse } from './types';

...

export const getSponsorsGraphQLResponse = async (): Promise<SponsorsResponse> => {
  ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Ввод ответа не повлияет ни на что в API в его нынешнем виде, но позволит нам преобразовать эти данные в более читабельный формат простым способом.

Преобразование необработанного ответа

Сначала давайте спланируем нужный формат объекта, чтобы сделать ответ более удобным для чтения:

{
  "tiers": [
    {
      "id": "MDExxxxxxxxxxxxxxxx==",
      "price": 2,
      "isOneTime": false,
      "isCustomAmount": false,
      "name": "$2 a month",
      "description": "Lorem ipsum dolor sit amet.",
            "totalEarningsPerMonth": 2,
      "sponsors": [
        {
          "username": "xxxx",
          "name": "Xxxxx Xxxxx",
          "avatar": "https://avatars.githubusercontent.com/u/xxxxxx",
          "website": "https://jahir.dev/",
          "since": "2022-03-02T07:59:51Z"
        },
        ...
      ]
    },
    ...
  ],
  "total": 1
}
Войти в полноэкранный режим Выход из полноэкранного режима

При таком формате у нас есть объект с 2 свойствами: tiers и total. Уровни будут содержать всю соответствующую информацию, включая массив объектов sponsors с информацией для каждого спонсора. Несколько полей переименованы из необработанного ответа, чтобы сделать их немного проще:

Давайте создадим интерфейсы для этого нового объекта в lib/sponsors/types.ts:

export interface Sponsor {
  username: string;
  name?: string;
  avatar: string;
  website?: string;
  since?: string;
}

export interface Tier {
  id: string;
  price: number;
  isOneTime: boolean;
  isCustomAmount: boolean;
  name: string;
  description?: string;
  totalEarningsPerMonth: number;
  sponsors: Array<Sponsor>;
}

export interface Sponsors {
  tiers: Array<Tier>;
  total: number;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, давайте создадим функцию для преобразования объекта SponsorsResponse в объект Sponsors в файле lib/sponsors/request.ts:

import type {
  SponsorsResponse,
  Sponsors,
  SponsorEntity,
  Sponsor,
  SponsorsTier,
  Tier,
} from './types';

...

const transformSponsorEntityIntoSponsor = (
  entity: SponsorEntity,
  tierSelectedAt?: string
): Sponsor => {
  return {
    name: entity.name,
    username: entity.login,
    avatar: entity.avatarUrl,
    website: entity.websiteUrl,
    since: tierSelectedAt,
  };
};

const transformRawTierIntoTier = (tier: SponsorsTier): Tier => {
  return {
    id: tier.id,
    price: tier.monthlyPriceInDollars,
    isOneTime: tier.isOneTime,
    isCustomAmount: tier.isCustomAmount,
    name: tier.name,
    description: tier.description,
    totalEarningsPerMonth: tier.adminInfo?.sponsorships.totalRecurringMonthlyPriceInDollars || 0,
    sponsors: (tier.adminInfo?.sponsorships?.nodes || []).map((node) => {
      // Transform `SponsorEntity` into `Sponsor` using the `transformSponsorEntityIntoSponsor` function
      // and the `tierSelectedAt` property
      return transformSponsorEntityIntoSponsor(node.sponsorEntity, node.tierSelectedAt);
    }),
  };
};

export const transformResponseIntoSponsorships = (rawResponse: SponsorsResponse): Sponsors => {
  // Get the listing and sponsors object from the raw response.
  // We rename `sponsorsListing` to just `listing` for ease.
  // This is done locally only and does not modify the `rawResponse` object.const { sponsorsListing: listing, sponsors } = rawResponse.data?.user || {};
  if (!listing || !sponsors) {
    return { tiers: [], total: 0 };
  }
  return {
    // Transform `SponsorsTier` into `Tier` using the `transformRawTierIntoTier` function
    tiers: listing.tiers.nodes.map(transformRawTierIntoTier),
    total: sponsors.totalCount,
  };
};
Войти в полноэкранный режим Выход из полноэкранного режима

Наконец, давайте обновим наш API, чтобы использовать новую функцию:

import type { NextApiRequest, NextApiResponse } from 'next';
import {
  getSponsorsGraphQLResponse,
  transformResponseIntoSponsorships,
} from './../../lib/sponsors/request';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const rawResponse = await getSponsorsGraphQLResponse();
  return res.status(200).json(transformResponseIntoSponsorships(rawResponse));
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, если мы перейдем по адресу http://localhost:3000/api/sponsors, мы увидим ответ с форматом, который мы изначально планировали. 🎉

Вы можете попробовать развернутую версию этой конечной точки по адресу https://sponsors-edge-api.vercel.app/api/sponsors.

Дополнительно: Edge Runtime

Дополнительно, и это совершенно необязательно, мы можем модифицировать API для использования нового Edge Runtime.

Next.js Edge Runtime основан на стандартных веб-интерфейсах API. Маршруты Edge API позволяют создавать высокопроизводительные API с помощью Next.js. Используя Edge Runtime, они часто работают быстрее, чем API-маршруты на базе Node.js.

Чтобы достичь этого, мы можем модифицировать API таким образом:

- import type { NextApiRequest, NextApiResponse } from 'next';
+ import type { NextRequest } from 'next/server';
import {
  getSponsorsGraphQLResponse,
  transformResponseIntoSponsorships,
} from './../../lib/sponsors/request';

export const config = {
  runtime: 'experimental-edge',
};

- export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ export default async function handler(req: NextRequest) {
  const rawResponse = await getSponsorsGraphQLResponse();
-  return res.status(200).json(transformResponseIntoSponsorships(rawResponse));
+  return new Response(JSON.stringify(transformResponseIntoSponsorships(rawResponse)), {
+    status: 200,
+    headers: { 'content-type': 'application/json' },
+  });
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Как вы видите, мы меняем тип NextApiRequest на NextRequest, удаляем параметр res из функции обработчика и меняем способ возврата JSON-ответа, используя объект Response.

Вы можете попробовать развернутую версию этой конечной точки по адресу https://sponsors-edge-api.vercel.app/api/sponsors-edge.

Закрытие

Вот и все, теперь у вас есть API для получения данных о спонсорах GitHub и использования этих данных для чего угодно.

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

Доработанный код проекта вы можете найти по адресу https://github.com/jahirfiquitiva/sponsors-edge-api.

Хорошего дня! 👋😀

Оцените статью
devanswers.ru
Добавить комментарий