259 lines
6.5 KiB
TypeScript
259 lines
6.5 KiB
TypeScript
|
|
import { Client } from '@notionhq/client'
|
||
|
|
import type {
|
||
|
|
DatabaseObjectResponse,
|
||
|
|
PageObjectResponse,
|
||
|
|
PartialDatabaseObjectResponse,
|
||
|
|
PartialPageObjectResponse,
|
||
|
|
} from '@notionhq/client/build/src/api-endpoints'
|
||
|
|
|
||
|
|
// Type for Notion database schema
|
||
|
|
export interface NotionDatabaseSchema {
|
||
|
|
id: string
|
||
|
|
title: string
|
||
|
|
properties: NotionProperty[]
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface NotionProperty {
|
||
|
|
id: string
|
||
|
|
name: string
|
||
|
|
type: string
|
||
|
|
}
|
||
|
|
|
||
|
|
// Type for a Notion page/record
|
||
|
|
export interface NotionRecord {
|
||
|
|
id: string
|
||
|
|
properties: Record<string, unknown>
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a Notion client with the provided API key
|
||
|
|
*/
|
||
|
|
export function createNotionClient(apiKey: string): Client {
|
||
|
|
return new Client({ auth: apiKey })
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test connection to Notion API
|
||
|
|
*/
|
||
|
|
export async function testNotionConnection(
|
||
|
|
apiKey: string
|
||
|
|
): Promise<{ success: boolean; error?: string }> {
|
||
|
|
try {
|
||
|
|
const client = createNotionClient(apiKey)
|
||
|
|
// Try to get the bot user to verify the API key
|
||
|
|
await client.users.me({})
|
||
|
|
return { success: true }
|
||
|
|
} catch (error) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : 'Failed to connect to Notion',
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get database schema (properties)
|
||
|
|
*/
|
||
|
|
export async function getNotionDatabaseSchema(
|
||
|
|
apiKey: string,
|
||
|
|
databaseId: string
|
||
|
|
): Promise<NotionDatabaseSchema> {
|
||
|
|
const client = createNotionClient(apiKey)
|
||
|
|
|
||
|
|
const database = await client.databases.retrieve({
|
||
|
|
database_id: databaseId,
|
||
|
|
})
|
||
|
|
|
||
|
|
// Type guard for full database object
|
||
|
|
if (!('properties' in database)) {
|
||
|
|
throw new Error('Could not retrieve database properties')
|
||
|
|
}
|
||
|
|
|
||
|
|
const db = database as DatabaseObjectResponse
|
||
|
|
|
||
|
|
const properties: NotionProperty[] = Object.entries(db.properties).map(
|
||
|
|
([name, prop]) => ({
|
||
|
|
id: prop.id,
|
||
|
|
name,
|
||
|
|
type: prop.type,
|
||
|
|
})
|
||
|
|
)
|
||
|
|
|
||
|
|
// Get database title
|
||
|
|
const titleProp = db.title?.[0]
|
||
|
|
const title = titleProp?.type === 'text' ? titleProp.plain_text : databaseId
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: db.id,
|
||
|
|
title,
|
||
|
|
properties,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Query all records from a Notion database
|
||
|
|
*/
|
||
|
|
export async function queryNotionDatabase(
|
||
|
|
apiKey: string,
|
||
|
|
databaseId: string,
|
||
|
|
limit?: number
|
||
|
|
): Promise<NotionRecord[]> {
|
||
|
|
const client = createNotionClient(apiKey)
|
||
|
|
|
||
|
|
const records: NotionRecord[] = []
|
||
|
|
let cursor: string | undefined
|
||
|
|
|
||
|
|
do {
|
||
|
|
const response = await client.databases.query({
|
||
|
|
database_id: databaseId,
|
||
|
|
start_cursor: cursor,
|
||
|
|
page_size: 100,
|
||
|
|
})
|
||
|
|
|
||
|
|
for (const page of response.results) {
|
||
|
|
if (isFullPage(page)) {
|
||
|
|
records.push({
|
||
|
|
id: page.id,
|
||
|
|
properties: extractProperties(page.properties),
|
||
|
|
})
|
||
|
|
|
||
|
|
if (limit && records.length >= limit) {
|
||
|
|
return records.slice(0, limit)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
cursor = response.has_more ? response.next_cursor ?? undefined : undefined
|
||
|
|
} while (cursor)
|
||
|
|
|
||
|
|
return records
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Type guard for full page response
|
||
|
|
*/
|
||
|
|
function isFullPage(
|
||
|
|
page: PageObjectResponse | PartialPageObjectResponse | DatabaseObjectResponse | PartialDatabaseObjectResponse
|
||
|
|
): page is PageObjectResponse {
|
||
|
|
return 'properties' in page && page.object === 'page'
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract property values from a Notion page
|
||
|
|
*/
|
||
|
|
function extractProperties(
|
||
|
|
properties: PageObjectResponse['properties']
|
||
|
|
): Record<string, unknown> {
|
||
|
|
const result: Record<string, unknown> = {}
|
||
|
|
|
||
|
|
for (const [name, prop] of Object.entries(properties)) {
|
||
|
|
result[name] = extractPropertyValue(prop)
|
||
|
|
}
|
||
|
|
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract a single property value
|
||
|
|
*/
|
||
|
|
function extractPropertyValue(prop: PageObjectResponse['properties'][string]): unknown {
|
||
|
|
switch (prop.type) {
|
||
|
|
case 'title':
|
||
|
|
return prop.title.map((t) => t.plain_text).join('')
|
||
|
|
case 'rich_text':
|
||
|
|
return prop.rich_text.map((t) => t.plain_text).join('')
|
||
|
|
case 'number':
|
||
|
|
return prop.number
|
||
|
|
case 'select':
|
||
|
|
return prop.select?.name ?? null
|
||
|
|
case 'multi_select':
|
||
|
|
return prop.multi_select.map((s) => s.name)
|
||
|
|
case 'status':
|
||
|
|
return prop.status?.name ?? null
|
||
|
|
case 'date':
|
||
|
|
return prop.date?.start ?? null
|
||
|
|
case 'checkbox':
|
||
|
|
return prop.checkbox
|
||
|
|
case 'url':
|
||
|
|
return prop.url
|
||
|
|
case 'email':
|
||
|
|
return prop.email
|
||
|
|
case 'phone_number':
|
||
|
|
return prop.phone_number
|
||
|
|
case 'files':
|
||
|
|
return prop.files.map((f) => {
|
||
|
|
if (f.type === 'file') {
|
||
|
|
return f.file.url
|
||
|
|
} else if (f.type === 'external') {
|
||
|
|
return f.external.url
|
||
|
|
}
|
||
|
|
return null
|
||
|
|
}).filter(Boolean)
|
||
|
|
case 'relation':
|
||
|
|
return prop.relation.map((r) => r.id)
|
||
|
|
case 'people':
|
||
|
|
return prop.people.map((p) => {
|
||
|
|
if ('name' in p) {
|
||
|
|
return p.name
|
||
|
|
}
|
||
|
|
return p.id
|
||
|
|
})
|
||
|
|
case 'created_time':
|
||
|
|
return prop.created_time
|
||
|
|
case 'last_edited_time':
|
||
|
|
return prop.last_edited_time
|
||
|
|
case 'created_by':
|
||
|
|
return 'name' in prop.created_by ? prop.created_by.name : prop.created_by.id
|
||
|
|
case 'last_edited_by':
|
||
|
|
return 'name' in prop.last_edited_by ? prop.last_edited_by.name : prop.last_edited_by.id
|
||
|
|
case 'formula':
|
||
|
|
return extractFormulaValue(prop.formula)
|
||
|
|
case 'rollup':
|
||
|
|
return extractRollupValue(prop.rollup)
|
||
|
|
case 'unique_id':
|
||
|
|
return prop.unique_id.prefix
|
||
|
|
? `${prop.unique_id.prefix}-${prop.unique_id.number}`
|
||
|
|
: prop.unique_id.number
|
||
|
|
default:
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract formula value
|
||
|
|
*/
|
||
|
|
function extractFormulaValue(
|
||
|
|
formula: { type: 'string'; string: string | null } | { type: 'number'; number: number | null } | { type: 'boolean'; boolean: boolean | null } | { type: 'date'; date: { start: string } | null }
|
||
|
|
): unknown {
|
||
|
|
switch (formula.type) {
|
||
|
|
case 'string':
|
||
|
|
return formula.string
|
||
|
|
case 'number':
|
||
|
|
return formula.number
|
||
|
|
case 'boolean':
|
||
|
|
return formula.boolean
|
||
|
|
case 'date':
|
||
|
|
return formula.date?.start ?? null
|
||
|
|
default:
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract rollup value
|
||
|
|
*/
|
||
|
|
function extractRollupValue(
|
||
|
|
rollup: { type: 'number'; number: number | null; function: string } | { type: 'date'; date: { start: string } | null; function: string } | { type: 'array'; array: Array<unknown>; function: string } | { type: 'incomplete'; incomplete: Record<string, never>; function: string } | { type: 'unsupported'; unsupported: Record<string, never>; function: string }
|
||
|
|
): unknown {
|
||
|
|
switch (rollup.type) {
|
||
|
|
case 'number':
|
||
|
|
return rollup.number
|
||
|
|
case 'date':
|
||
|
|
return rollup.date?.start ?? null
|
||
|
|
case 'array':
|
||
|
|
return rollup.array
|
||
|
|
default:
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
}
|