Ka-Note/ka-note/server/src/routes/push.ts

148 lines
4.4 KiB
TypeScript

import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
import { db } from '../db/connection.js';
import { contexts, topics, historyEntries } from '../db/schema.js';
import { and, eq, sql, max } from 'drizzle-orm';
import { isLocked } from '../lib/ai-export-service.js';
import { ensureDailyLog } from '../lib/sync-service.js';
import type { AuthEnv } from '../middleware/auth.js';
import { randomUUID } from 'crypto';
const JOURNAL_TOPIC_ID = 'daily-log-journal';
const MAX_TITLE_LENGTH = 22;
function now(): string {
return new Date().toISOString();
}
function normalizeTitle(title: string, body: string): { title: string; text: string } {
if (title.length <= MAX_TITLE_LENGTH) {
const text = body.trim() ? `${title}\n\n${body}` : title;
return { title, text };
}
const short = title.slice(0, MAX_TITLE_LENGTH) + '…';
const fullBody = body.trim() ? `${title}\n\n${body}` : title;
return { title: short, text: `${short}\n\n${fullBody}` };
}
async function ensureJournalTopic(userId: string): Promise<void> {
const existing = await db.select({ id: topics.id }).from(topics)
.where(and(eq(topics.id, JOURNAL_TOPIC_ID), eq(topics.userId, userId)))
.get();
if (existing) return;
await db.insert(topics).values({
id: JOURNAL_TOPIC_ID,
userId,
contextId: 'daily-log',
title: '__journal__',
status: 'active',
snoozeUntil: null,
sortOrder: -1,
isNew: false,
updatedAt: now(),
deletedAt: null,
version: 1,
});
}
const DailyLogBody = z.object({
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD'),
scope: z.enum(['private', 'company']),
title: z.string().min(1).max(200),
body: z.string(),
});
const DailyLogResponse = z.object({
contextId: z.string(),
topicId: z.string(),
historyEntryId: z.string(),
});
const pushDailyLogRoute = createRoute({
method: 'post',
path: '/daily-log',
summary: 'Create a new daily log entry',
description: 'Pushes a new journal entry (historyEntry) into the daily-log context. Returns 409 if an entry with the same title already exists on that date.',
security: [{ BearerAuth: [] }],
request: {
body: {
content: { 'application/json': { schema: DailyLogBody } },
required: true,
},
},
responses: {
201: {
description: 'Entry created',
content: { 'application/json': { schema: DailyLogResponse } },
},
409: {
description: 'Duplicate: entry with this title already exists on this date',
content: { 'application/json': { schema: z.object({ error: z.string() }) } },
},
423: {
description: 'AI lock active',
content: { 'application/json': { schema: z.object({ error: z.string() }) } },
},
},
});
const push = new OpenAPIHono<AuthEnv>();
push.openapi(pushDailyLogRoute, async (c) => {
const { userId } = c.get('auth');
const lock = await isLocked(userId);
if (lock) return c.json({ error: 'AI lock active — unlock at /api/ai/unlock' }, 423);
await ensureDailyLog(userId);
await ensureJournalTopic(userId);
const { date, scope, title: rawTitle, body } = c.req.valid('json');
const isPrivate = scope === 'private';
const { title, text } = normalizeTitle(rawTitle, body);
// Duplicate check: same date + title as first line (plain or legacy ## prefix)
const existing = await db.select({ id: historyEntries.id })
.from(historyEntries)
.where(and(
eq(historyEntries.userId, userId),
eq(historyEntries.topicId, JOURNAL_TOPIC_ID),
eq(historyEntries.date, date),
sql`(${historyEntries.text} LIKE ${title + '%'} OR ${historyEntries.text} LIKE ${'## ' + title + '%'})`,
))
.get();
if (existing) {
return c.json({ error: `Entry with title "${title}" already exists for ${date}` }, 409);
}
const maxSortOrder = await db.select({ val: max(historyEntries.sortOrder) })
.from(historyEntries)
.where(and(eq(historyEntries.userId, userId), eq(historyEntries.topicId, JOURNAL_TOPIC_ID)))
.get();
const sortOrder = (maxSortOrder?.val ?? 0) + 1;
const id = randomUUID();
await db.insert(historyEntries).values({
id,
userId,
topicId: JOURNAL_TOPIC_ID,
date,
text,
sortOrder,
linkedContextId: null,
doneAt: null,
wiedervorlageDate: null,
wiedervorlageResolvedAt: null,
isPrivate,
updatedAt: now(),
deletedAt: null,
version: 1,
});
return c.json({ contextId: 'daily-log', topicId: JOURNAL_TOPIC_ID, historyEntryId: id }, 201);
});
export default push;