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:
2026-01-30 13:41:32 +01:00
commit a606292aaa
290 changed files with 70691 additions and 0 deletions

258
src/lib/notion.ts Normal file
View 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
}
}