import { createAuthenticatedFetch } from 'kody:runtime' import type { ProductHuntGraphQLError, ProductHuntRequestInput, ProductHuntRequestResult, } from './types.ts' export const PRODUCTHUNT_GRAPHQL_URL = 'https://api.producthunt.com/v2/api/graphql' export const PRODUCTHUNT_INTEGRATION = 'producthunt' export const PRODUCTHUNT_ACCESS_TOKEN_SECRET = 'producthuntAccessToken' export const PRODUCTHUNT_CLIENT_SECRET_SECRET = 'producthuntClientSecret' export const PRODUCTHUNT_AUTHORIZE_URL = 'https://api.producthunt.com/v2/oauth/authorize' export const PRODUCTHUNT_TOKEN_URL = 'https://api.producthunt.com/v2/oauth/token' export const PRODUCTHUNT_APPLICATIONS_URL = 'https://api.producthunt.com/v2/oauth/applications' /** Error thrown when a Product Hunt GraphQL request fails. */ export class ProductHuntRequestError extends Error { result: ProductHuntRequestResult constructor(result: ProductHuntRequestResult) { const graphql = result.errors?.map((error) => error.message).filter(Boolean) super( graphql?.length ? `Product Hunt GraphQL failed with ${result.status}: ${graphql.join('; ')}` : `Product Hunt request failed with ${result.status} ${result.statusText}`, ) this.name = 'ProductHuntRequestError' this.result = result } } /** * Authenticated `fetch` bound to the saved Product Hunt OAuth integration. * Always uses the literal `producthunt` name so publish can stamp it. */ export async function getProductHuntAuthenticatedFetch( integration = PRODUCTHUNT_INTEGRATION, ): Promise { if (integration !== PRODUCTHUNT_INTEGRATION) { throw new Error( `Only the "${PRODUCTHUNT_INTEGRATION}" OAuth integration is supported (got "${integration}").`, ) } return await createAuthenticatedFetch('producthunt') } /** * Compact authenticated Product Hunt GraphQL request helper. * Auth: `createAuthenticatedFetch('producthunt')` against the v2 GraphQL endpoint. */ export async function request( options: ProductHuntRequestInput, ): Promise> { const query = options.query?.trim() if (!query) throw new Error('query is required.') const authFetch = await getProductHuntAuthenticatedFetch( options.integration ?? PRODUCTHUNT_INTEGRATION, ) const variables = Object.fromEntries( Object.entries(options.variables ?? {}).filter(([, value]) => value !== undefined), ) const response = await authFetch(PRODUCTHUNT_GRAPHQL_URL, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ query, ...(Object.keys(variables).length ? { variables } : {}), ...(options.operationName ? { operationName: options.operationName } : {}), }), }) const text = await response.text() let payload: { data?: T | null; errors?: ProductHuntGraphQLError[] } | null = null if (text) { try { payload = JSON.parse(text) as { data?: T | null; errors?: ProductHuntGraphQLError[] } } catch { payload = null } } const errors = payload?.errors?.length ? payload.errors : null const result: ProductHuntRequestResult = { ok: response.ok && !errors, status: response.status, statusText: response.statusText, data: payload?.data ?? null, errors, } if (!result.ok && options.throwOnError !== false) { throw new ProductHuntRequestError(result) } return result } /** * Call the Product Hunt GraphQL API using the saved OAuth token. * @param params.query - GraphQL query or mutation document. * @returns Parsed `data` plus GraphQL `errors` when present. * @example * import request from 'kody:@kentcdodds/producthunt/request' * const result = await request({ * query: 'query { viewer { user { username } } }', * }) * // => { ok: true, status: 200, data: { viewer: { user: { username: '...' } } }, errors: null } */ export default async function requestEntrypoint(params: ProductHuntRequestInput) { return await request(params) }