Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/producthunt

src/types.ts

344 lines · 9.0 KB · TypeScript
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',
	]
}