export type JsonRecord = Record<string, unknown>
export type ProductHuntAuthMode = 'oauth' | 'token'
export type ProductHuntAuthInput = {
/** Saved OAuth integration name. Defaults to `producthunt`. */
integrationName?: string
/** Alias of `integrationName`. */
integration?: string
/**
* Short account alias. `work` → `producthunt-work`. Values that already
* start with `producthunt-` are used as-is. `default` / `producthunt` /
* omitted → `producthunt`.
*/
account?: string
/** User secret name for a developer or access token. Defaults to `producthuntAccessToken`. */
secretName?: string
/** Force OAuth or developer-token auth when both exist. */
auth?: ProductHuntAuthMode
}
export type MutationInput = {
confirm?: boolean
dryRun?: boolean
}
export type ProductHuntGraphQLError = {
message: string
path?: Array<string | number>
locations?: Array<{ line: number; column: number }>
extensions?: Record<string, unknown>
}
export type ProductHuntRequestInput = ProductHuntAuthInput &
MutationInput & {
/** GraphQL query or mutation document. */
query: string
/** GraphQL variables object. */
variables?: JsonRecord
/** Optional GraphQL operation name. */
operationName?: string
/** Throw `ProductHuntRequestError` on HTTP or GraphQL errors. Default true. */
throwOnError?: boolean
}
export type ProductHuntRequestResult<T = unknown> = {
ok: boolean
status: number
statusText: string
data: T | null
errors: ProductHuntGraphQLError[] | null
}
export type ProductHuntPageInfo = {
endCursor: string | null
hasNextPage: boolean
hasPreviousPage: boolean
startCursor: string | null
}
export type ProductHuntConnection<T> = {
nodes: T[]
pageInfo: ProductHuntPageInfo
}
export type ProductHuntPostsOrder = 'FEATURED_AT' | 'VOTES' | 'RANKING' | 'NEWEST'
export type ProductHuntTopicsOrder = 'NEWEST' | 'FOLLOWERS_COUNT'
export type ProductHuntCollectionsOrder = 'NEWEST' | 'FOLLOWERS_COUNT' | 'FEATURED_AT'
export type ProductHuntCommentsOrder = 'NEWEST' | 'VOTES_COUNT'
export type ProductHuntUserSummary = {
id: string
name: string
username: string
headline: string | null
url: string
profileImage: string | null
twitterUsername: string | null
websiteUrl: string | null
isMaker: boolean
followersCount: number
followingCount: number
}
export type ProductHuntTopicSummary = {
id: string
name: string
slug: string
description?: string
url?: string
followersCount?: number
postsCount?: number
isFollowing?: boolean
}
export type ProductHuntCollectionSummary = {
id: string
name: string
tagline: string
description: string | null
url: string
coverImage: string | null
followersCount: number
featuredAt: string | null
createdAt: string
isFollowing: boolean
user?: ProductHuntUserSummary
}
export type ProductHuntPostSummary = {
id: string
name: string
slug: string
tagline: string
description: string | null
url: string
website: string
votesCount: number
commentsCount: number
reviewsCount: number
reviewsRating: number
createdAt: string
featuredAt: string | null
dailyRank: number | null
weeklyRank: number | null
isVoted: boolean
thumbnailUrl: string | null
user: ProductHuntUserSummary
makers: ProductHuntUserSummary[]
topics: ProductHuntTopicSummary[]
}
export type ProductHuntCommentSummary = {
id: string
body: string
url: string
createdAt: string
votesCount: number
isVoted: boolean
parentId: string | null
user: ProductHuntUserSummary
}
export type ProductHuntListPostsOptions = ProductHuntAuthInput & {
featured?: boolean
postedAfter?: string
postedBefore?: string
topic?: string
order?: ProductHuntPostsOrder
first?: number
after?: string
}
export type ProductHuntListTodayPostsOptions = ProductHuntAuthInput & {
/** IANA timezone for the Product Hunt day. Default `America/Los_Angeles`. */
timeZone?: string
featured?: boolean
topic?: string
order?: ProductHuntPostsOrder
first?: number
after?: string
}
export type ProductHuntGetPostOptions = ProductHuntAuthInput & {
id?: string
slug?: string
}
export type ProductHuntGetUserOptions = ProductHuntAuthInput & {
id?: string
username?: string
}
export type ProductHuntListCommentsOptions = ProductHuntAuthInput & {
postId?: string
slug?: string
order?: ProductHuntCommentsOrder
first?: number
after?: string
}
export type ProductHuntGetCollectionOptions = ProductHuntAuthInput & {
id?: string
slug?: string
}
export type ProductHuntListCollectionsOptions = ProductHuntAuthInput & {
postId?: string
userId?: string
featured?: boolean
order?: ProductHuntCollectionsOrder
first?: number
after?: string
}
export type ProductHuntGetTopicOptions = ProductHuntAuthInput & {
id?: string
slug?: string
}
export type ProductHuntListTopicsOptions = ProductHuntAuthInput & {
query?: string
followedByUserid?: string
order?: ProductHuntTopicsOrder
first?: number
after?: string
}
export function requireRecord(value: unknown, label: string): JsonRecord {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must be an object.`)
}
return value as JsonRecord
}
export function requireString(
value: unknown,
label: string,
options: { allowEmpty?: boolean } = {},
): string {
if (typeof value !== 'string' || (!options.allowEmpty && value.trim().length === 0)) {
throw new Error(
`${label} must be ${options.allowEmpty ? 'a string' : 'a non-empty string'}.`,
)
}
return options.allowEmpty ? value : value.trim()
}
export function optionalString(value: unknown, label: string): string | undefined {
if (value === undefined || value === null) return undefined
return requireString(value, label)
}
export function optionalBoolean(value: unknown, label: string): boolean | undefined {
if (value === undefined) return undefined
if (typeof value !== 'boolean') throw new Error(`${label} must be a boolean.`)
return value
}
export function optionalNumber(value: unknown, label: string): number | undefined {
if (value === undefined || value === null) return undefined
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new Error(`${label} must be a finite number.`)
}
return value
}
const POSTS_ORDERS: readonly ProductHuntPostsOrder[] = [
'FEATURED_AT',
'VOTES',
'RANKING',
'NEWEST',
]
const TOPICS_ORDERS: readonly ProductHuntTopicsOrder[] = ['NEWEST', 'FOLLOWERS_COUNT']
const COLLECTIONS_ORDERS: readonly ProductHuntCollectionsOrder[] = [
'NEWEST',
'FOLLOWERS_COUNT',
'FEATURED_AT',
]
const COMMENTS_ORDERS: readonly ProductHuntCommentsOrder[] = ['NEWEST', 'VOTES_COUNT']
export function optionalPostsOrder(
value: unknown,
label = 'order',
): ProductHuntPostsOrder | undefined {
if (value === undefined || value === null) return undefined
const order = requireString(value, label)
if (!(POSTS_ORDERS as readonly string[]).includes(order)) {
throw new Error(`${label} must be one of: ${POSTS_ORDERS.join(', ')}.`)
}
return order as ProductHuntPostsOrder
}
export function optionalTopicsOrder(
value: unknown,
label = 'order',
): ProductHuntTopicsOrder | undefined {
if (value === undefined || value === null) return undefined
const order = requireString(value, label)
if (!(TOPICS_ORDERS as readonly string[]).includes(order)) {
throw new Error(`${label} must be one of: ${TOPICS_ORDERS.join(', ')}.`)
}
return order as ProductHuntTopicsOrder
}
export function optionalCollectionsOrder(
value: unknown,
label = 'order',
): ProductHuntCollectionsOrder | undefined {
if (value === undefined || value === null) return undefined
const order = requireString(value, label)
if (!(COLLECTIONS_ORDERS as readonly string[]).includes(order)) {
throw new Error(`${label} must be one of: ${COLLECTIONS_ORDERS.join(', ')}.`)
}
return order as ProductHuntCollectionsOrder
}
export function optionalCommentsOrder(
value: unknown,
label = 'order',
): ProductHuntCommentsOrder | undefined {
if (value === undefined || value === null) return undefined
const order = requireString(value, label)
if (!(COMMENTS_ORDERS as readonly string[]).includes(order)) {
throw new Error(`${label} must be one of: ${COMMENTS_ORDERS.join(', ')}.`)
}
return order as ProductHuntCommentsOrder
}
/**
* Return exported Product Hunt TypeScript type names for agent discovery.
* @example
* import types from 'kody:@kody/producthunt/types'
* const names = await types()
*/
export default function describeProductHuntTypes() {
return [
'ProductHuntAuthInput',
'ProductHuntAuthMode',
'ProductHuntGraphQLError',
'ProductHuntRequestInput',
'ProductHuntRequestResult',
'ProductHuntPageInfo',
'ProductHuntConnection',
'ProductHuntPostsOrder',
'ProductHuntTopicsOrder',
'ProductHuntCollectionsOrder',
'ProductHuntCommentsOrder',
'ProductHuntUserSummary',
'ProductHuntTopicSummary',
'ProductHuntCollectionSummary',
'ProductHuntPostSummary',
'ProductHuntCommentSummary',
'ProductHuntListPostsOptions',
'ProductHuntListTodayPostsOptions',
'ProductHuntGetPostOptions',
'ProductHuntGetUserOptions',
'ProductHuntListCommentsOptions',
'ProductHuntGetCollectionOptions',
'ProductHuntListCollectionsOptions',
'ProductHuntGetTopicOptions',
'ProductHuntListTopicsOptions',
]
}