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.
*/
export async function getProductHuntAuthenticatedFetch(
integration = PRODUCTHUNT_INTEGRATION,
): Promise<typeof fetch> {
return await createAuthenticatedFetch(integration)
}
/**
* Compact authenticated Product Hunt GraphQL request helper.
* Auth: `createAuthenticatedFetch('producthunt')` against the v2 GraphQL endpoint.
*/
export async function request<T = unknown>(
options: ProductHuntRequestInput,
): Promise<ProductHuntRequestResult<T>> {
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<T> = {
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)
}