Initial commit: MOPC platform with Docker deployment setup
Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
258
src/lib/notion.ts
Normal file
258
src/lib/notion.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user