Skip to content

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

Package listing

@kentcdodds/notion

src/accounts.ts

105 lines · 3.8 KB · TypeScript
import { kody } from 'kody:runtime'

export const DEFAULT_INTEGRATION = 'notion'

export type NotionAccountParams = {
  account?: string
  integration?: string
}

export type NotionAccountSummary = {
  account: string | null
  integration: string
  default: boolean
  useWhen: string
}

function normalizeIntegrationName(name: string): string {
  return name.trim().toLowerCase()
}

/**
 * Resolve which OAuth integration to use.
 *
 * - `integration` wins when set (exact integration name, e.g. `notion-work`)
 * - `account: 'work'` → `notion-work` (Kody `<provider>-<purpose>` convention)
 * - `account: 'default'` / `'notion'` / omitted → `notion`
 * - bare `account` values that already start with `notion-` are used as-is
 */
export function resolveIntegrationName(params: NotionAccountParams = {}): string {
  if (typeof params.integration === 'string' && params.integration.trim()) {
    return normalizeIntegrationName(params.integration)
  }
  if (typeof params.account === 'string' && params.account.trim()) {
    const account = normalizeIntegrationName(params.account)
    if (account === 'default' || account === 'notion') return DEFAULT_INTEGRATION
    if (account === DEFAULT_INTEGRATION || account.startsWith('notion-')) return account
    return 'notion-' + account
  }
  return DEFAULT_INTEGRATION
}

export function accountAliasForIntegration(integrationName: string): string | null {
  const name = normalizeIntegrationName(integrationName)
  if (name === DEFAULT_INTEGRATION) return null
  if (name.startsWith('notion-')) return name.slice('notion-'.length)
  return name
}

/**
 * List connected Notion OAuth integrations (`notion` and `notion-*`).
 * Discovered from the signed-in user's saved integrations — not a hard-coded alias table.
 */
export async function listNotionAccounts(): Promise<{
  accounts: NotionAccountSummary[]
  defaultIntegration: string
}> {
  const listed = await kody.integration_list({})
  const items = Array.isArray(listed)
    ? listed
    : Array.isArray((listed as { integrations?: unknown[] })?.integrations)
      ? (listed as { integrations: unknown[] }).integrations
      : Array.isArray((listed as { items?: unknown[] })?.items)
        ? (listed as { items: unknown[] }).items
        : []

  const accounts = items
    .map((item) => {
      const name =
        item && typeof item === 'object' && typeof (item as { name?: unknown }).name === 'string'
          ? normalizeIntegrationName((item as { name: string }).name)
          : ''
      if (!name || (name !== DEFAULT_INTEGRATION && !name.startsWith('notion-'))) return null
      const alias = accountAliasForIntegration(name)
      return {
        account: alias,
        integration: name,
        default: name === DEFAULT_INTEGRATION,
        useWhen:
          name === DEFAULT_INTEGRATION
            ? 'Default Notion workspace. Use when the user does not name a specific workspace.'
            : 'Use when the user asks for the ' +
              alias +
              ' Notion workspace (integration ' +
              name +
              ').',
      } satisfies NotionAccountSummary
    })
    .filter((entry): entry is NotionAccountSummary => entry !== null)
    .sort(
      (a, b) => Number(b.default) - Number(a.default) || a.integration.localeCompare(b.integration),
    )

  return { accounts, defaultIntegration: DEFAULT_INTEGRATION }
}

/**
 * List connected Notion accounts (default export for `kody:@kentcdodds/notion/accounts`).
 * @example
 * import listNotionAccounts from 'kody:@kentcdodds/notion/accounts'
 * const { accounts } = await listNotionAccounts()
 * // => { accounts: [{ account: null, integration: 'notion', default: true }, { account: 'work', integration: 'notion-work', default: false }] }
 */
export default async function listNotionAccountsEntrypoint() {
  return await listNotionAccounts()
}