|
|
|
|
@@ -114,6 +114,17 @@ app.post('/api/v1/auth/login', async (c) => {
|
|
|
|
|
return c.json({ data: { token, userId: user.id }, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get('/api/v1/server-info', async (c) => {
|
|
|
|
|
return c.json({
|
|
|
|
|
data: {
|
|
|
|
|
version: envVars.VERSION || '0.1.0',
|
|
|
|
|
registrationEnabled: envVars.REGISTRATION_ENABLED !== 'false',
|
|
|
|
|
appName: envVars.APP_NAME || 'DearDiary',
|
|
|
|
|
},
|
|
|
|
|
error: null
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post('/api/v1/auth/api-key', async (c) => {
|
|
|
|
|
const authHeader = c.req.header('Authorization');
|
|
|
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
|
|
|
@@ -156,17 +167,22 @@ app.get('/api/v1/days', async (c) => {
|
|
|
|
|
|
|
|
|
|
const journals = await prisma.journal.findMany({
|
|
|
|
|
where: { userId },
|
|
|
|
|
select: { date: true, generatedAt: true },
|
|
|
|
|
select: { date: true, title: true, generatedAt: true, content: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const journalMap = new Map(journals.map(j => [j.date, j]));
|
|
|
|
|
|
|
|
|
|
const result = days.map(day => ({
|
|
|
|
|
date: day.date,
|
|
|
|
|
eventCount: day._count.id,
|
|
|
|
|
hasJournal: journalMap.has(day.date),
|
|
|
|
|
journalGeneratedAt: journalMap.get(day.date)?.generatedAt,
|
|
|
|
|
}));
|
|
|
|
|
const result = days.map(day => {
|
|
|
|
|
const journal = journalMap.get(day.date);
|
|
|
|
|
return {
|
|
|
|
|
date: day.date,
|
|
|
|
|
eventCount: day._count.id,
|
|
|
|
|
hasJournal: !!journal,
|
|
|
|
|
journalTitle: journal?.title,
|
|
|
|
|
journalGeneratedAt: journal?.generatedAt,
|
|
|
|
|
journalExcerpt: journal?.content?.substring(0, 250),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: result, error: null });
|
|
|
|
|
});
|
|
|
|
|
@@ -203,80 +219,78 @@ app.delete('/api/v1/days/:date', async (c) => {
|
|
|
|
|
return c.json({ data: { deleted: true }, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Delete journal only (keeps events)
|
|
|
|
|
app.delete('/api/v1/journal/:date', async (c) => {
|
|
|
|
|
app.get('/api/v1/search', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const { date } = c.req.param();
|
|
|
|
|
|
|
|
|
|
const journal = await prisma.journal.findFirst({ where: { userId, date } });
|
|
|
|
|
if (!journal) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Journal not found' } }, 404);
|
|
|
|
|
const query = c.req.query('q') || '';
|
|
|
|
|
if (query.length < 2) {
|
|
|
|
|
return c.json({ data: { journals: [], events: [] }, error: null });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.journal.delete({ where: { id: journal.id } });
|
|
|
|
|
return c.json({ data: { deleted: true }, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Events routes
|
|
|
|
|
app.post('/api/v1/events', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const { date, type, content, metadata } = await c.req.json();
|
|
|
|
|
|
|
|
|
|
if (!date || !type || !content) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'date, type, and content are required' } }, 400);
|
|
|
|
|
try {
|
|
|
|
|
const safeQuery = query.replace(/['"]/g, '');
|
|
|
|
|
const searchTerm = safeQuery.split(/\s+/).filter(t => t.length > 0).map(t => `${t}*`).join(' OR ') || `${safeQuery}*`;
|
|
|
|
|
|
|
|
|
|
let journalResults: Array<{ userId: string; date: string; title: string; content: string }> = [];
|
|
|
|
|
let eventResults: Array<{ userId: string; date: string; type: string; content: string }> = [];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
journalResults = await prisma.$queryRaw<Array<{ userId: string; date: string; title: string; content: string }>>`
|
|
|
|
|
SELECT userId, date, title, content
|
|
|
|
|
FROM journal_fts
|
|
|
|
|
WHERE userId = ${userId} AND journal_fts MATCH ${searchTerm}
|
|
|
|
|
ORDER BY rank
|
|
|
|
|
LIMIT 20
|
|
|
|
|
`;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Journal FTS search error:', e);
|
|
|
|
|
const journals = await prisma.journal.findMany({
|
|
|
|
|
where: { userId, OR: [
|
|
|
|
|
{ title: { contains: safeQuery } },
|
|
|
|
|
{ content: { contains: safeQuery } }
|
|
|
|
|
]},
|
|
|
|
|
take: 20,
|
|
|
|
|
});
|
|
|
|
|
journalResults = journals.map(j => ({ userId: j.userId, date: j.date, title: j.title || '', content: j.content }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
eventResults = await prisma.$queryRaw<Array<{ userId: string; date: string; type: string; content: string }>>`
|
|
|
|
|
SELECT userId, date, type, content
|
|
|
|
|
FROM event_fts
|
|
|
|
|
WHERE userId = ${userId} AND event_fts MATCH ${searchTerm}
|
|
|
|
|
ORDER BY rank
|
|
|
|
|
LIMIT 20
|
|
|
|
|
`;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Event FTS search error:', e);
|
|
|
|
|
const events = await prisma.event.findMany({
|
|
|
|
|
where: { userId, content: { contains: safeQuery } },
|
|
|
|
|
take: 20,
|
|
|
|
|
});
|
|
|
|
|
eventResults = events.map(e => ({ userId: e.userId, date: e.date, type: e.type, content: e.content }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.json({
|
|
|
|
|
data: {
|
|
|
|
|
journals: journalResults.map(j => ({
|
|
|
|
|
date: j.date,
|
|
|
|
|
title: j.title,
|
|
|
|
|
excerpt: j.content.substring(0, 200),
|
|
|
|
|
})),
|
|
|
|
|
events: eventResults.map(e => ({
|
|
|
|
|
date: e.date,
|
|
|
|
|
type: e.type,
|
|
|
|
|
content: e.content,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
error: null
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Search error:', err);
|
|
|
|
|
return c.json({ data: { journals: [], events: [] }, error: null });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validTypes = ['event'];
|
|
|
|
|
if (!validTypes.includes(type)) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: `type must be one of: ${validTypes.join(', ')}` } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const event = await prisma.event.create({
|
|
|
|
|
data: { userId, date, type, content, metadata: metadata ? JSON.stringify(metadata) : null },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: event, error: null }, 201);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get('/api/v1/events/:id', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const { id } = c.req.param();
|
|
|
|
|
const event = await prisma.event.findFirst({ where: { id, userId } });
|
|
|
|
|
|
|
|
|
|
if (!event) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
|
|
|
|
|
return c.json({ data: event, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.put('/api/v1/events/:id', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const { id } = c.req.param();
|
|
|
|
|
const { content, metadata } = await c.req.json();
|
|
|
|
|
|
|
|
|
|
const existing = await prisma.event.findFirst({ where: { id, userId } });
|
|
|
|
|
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
|
|
|
|
|
|
|
|
|
|
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
|
|
|
|
|
if (journal) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot edit event: journal already generated. Delete the journal first.' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const event = await prisma.event.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data: {
|
|
|
|
|
content: content ?? existing.content,
|
|
|
|
|
metadata: metadata !== undefined ? JSON.stringify(metadata) : existing.metadata,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: event, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.delete('/api/v1/events/:id', async (c) => {
|
|
|
|
|
@@ -302,6 +316,8 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const { date } = c.req.param();
|
|
|
|
|
const body = await c.req.json().catch(() => ({}));
|
|
|
|
|
const additionalInstructions = body.instructions || '';
|
|
|
|
|
|
|
|
|
|
const [events, settings] = await Promise.all([
|
|
|
|
|
prisma.event.findMany({ where: { userId, date }, orderBy: { createdAt: 'asc' } }),
|
|
|
|
|
@@ -324,10 +340,14 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
|
|
|
|
// Build events text
|
|
|
|
|
const eventsText = events.map(event => {
|
|
|
|
|
let text = `[EVENT] ${event.createdAt.toISOString()}\n${event.content}`;
|
|
|
|
|
if (event.placeName) {
|
|
|
|
|
text += `\nLocation: ${event.placeName}`;
|
|
|
|
|
} else if (event.latitude && event.longitude) {
|
|
|
|
|
text += `\nLocation: ${event.latitude}, ${event.longitude}`;
|
|
|
|
|
}
|
|
|
|
|
if (event.metadata) {
|
|
|
|
|
try {
|
|
|
|
|
const meta = JSON.parse(event.metadata);
|
|
|
|
|
if (meta.location) text += `\nLocation: ${meta.location.lat}, ${meta.location.lng}`;
|
|
|
|
|
if (meta.duration) text += `\nDuration: ${meta.duration}s`;
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
@@ -356,22 +376,57 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
|
|
|
|
|
|
|
|
|
if (previousJournals.length > 0) {
|
|
|
|
|
previousJournalsText = `PREVIOUS DIARIES:\n${previousJournals.map(j =>
|
|
|
|
|
`[${j.date}]\n${j.content}`
|
|
|
|
|
`[${j.date}]\n${j.title ? `Title: ${j.title}\n` : ''}${j.content}`
|
|
|
|
|
).join('\n\n')}\n\n`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build prompts: 1. system prompt, 2. previous journals, 3. today's events
|
|
|
|
|
const systemPrompt = settings?.journalPrompt || 'You are a thoughtful journal writer.';
|
|
|
|
|
const userPrompt = `${previousJournalsText}EVENTS FROM TODAY (${date}):\n${eventsText}\n\nWrite a thoughtful, reflective journal entry based on the events above.`;
|
|
|
|
|
const jsonInstruction = `IMPORTANT: Return ONLY valid JSON in this exact format, nothing else:
|
|
|
|
|
{"title": "A short, descriptive title for this day (max 50 characters)", "content": "Your diary entry text here..."}
|
|
|
|
|
|
|
|
|
|
Do not include any text before or after the JSON. Do not use markdown code blocks.`;
|
|
|
|
|
|
|
|
|
|
// Build system prompt from user's settings + JSON instruction
|
|
|
|
|
const defaultPrompt = `You are a factual diary summarizer. Your ONLY job is to summarize the entries provided to you - nothing more.
|
|
|
|
|
|
|
|
|
|
CRITICAL RULES:
|
|
|
|
|
1. ONLY use information explicitly stated in the entries below
|
|
|
|
|
2. NEVER invent, assume, or hallucinate any detail not in the entries
|
|
|
|
|
3. NEVER add activities, emotions, weather, or context not directly mentioned
|
|
|
|
|
4. If something is unclear in the entries, simply state what IS clear
|
|
|
|
|
5. Keep the summary grounded and factual - no embellishment
|
|
|
|
|
6. Do not write in an overly creative or story-telling style
|
|
|
|
|
7. Only reference what the user explicitly recorded
|
|
|
|
|
8. NEVER write any preamble, meta-commentary, or statements about how you are writing
|
|
|
|
|
9. NEVER include any closing remarks, sign-offs, or follow-up offers`;
|
|
|
|
|
|
|
|
|
|
const userInstructions = settings?.journalPrompt;
|
|
|
|
|
const systemPromptWithJson = userInstructions
|
|
|
|
|
? `${defaultPrompt}\n\n${jsonInstruction}\n\nCUSTOM USER INSTRUCTIONS:\n${userInstructions}`
|
|
|
|
|
: `${defaultPrompt}\n\n${jsonInstruction}`;
|
|
|
|
|
|
|
|
|
|
let userPrompt = `${previousJournalsText}EVENTS FROM TODAY (${date}):\n${eventsText}`;
|
|
|
|
|
|
|
|
|
|
if (additionalInstructions) {
|
|
|
|
|
userPrompt = `${userPrompt}\n\nADDITIONAL USER INSTRUCTIONS:\n${additionalInstructions}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[Journal Generate] Date: ${date}, Context days: ${contextDays}, Events: ${events.length}`);
|
|
|
|
|
|
|
|
|
|
// Create placeholder journal and task
|
|
|
|
|
const placeholderJournal = await prisma.journal.create({
|
|
|
|
|
data: { userId, date, content: 'Generating...', eventCount: events.length },
|
|
|
|
|
// Check if journal already exists for this date
|
|
|
|
|
const existingJournal = await prisma.journal.findUnique({
|
|
|
|
|
where: { userId_date: { userId, date } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Use upsert to handle both create and regenerate cases
|
|
|
|
|
const placeholderJournal = await prisma.journal.upsert({
|
|
|
|
|
where: { userId_date: { userId, date } },
|
|
|
|
|
create: { userId, date, content: 'Generating...', eventCount: events.length },
|
|
|
|
|
update: { content: 'Generating...', eventCount: events.length, generatedAt: new Date() },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const model = providerConfig.model || settings?.aiModel;
|
|
|
|
|
|
|
|
|
|
const task = await prisma.task.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId,
|
|
|
|
|
@@ -379,23 +434,16 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
|
|
|
|
type: 'journal_generate',
|
|
|
|
|
status: 'pending',
|
|
|
|
|
provider,
|
|
|
|
|
model: settings?.aiModel,
|
|
|
|
|
model: model,
|
|
|
|
|
prompt: `${systemPrompt}\n\n---\n\n${userPrompt}`,
|
|
|
|
|
request: '',
|
|
|
|
|
response: '',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update journal with taskId
|
|
|
|
|
await prisma.journal.update({
|
|
|
|
|
where: { id: placeholderJournal.id },
|
|
|
|
|
data: { id: placeholderJournal.id },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
console.log(`[Journal Generate] Using provider: ${provider}`);
|
|
|
|
|
|
|
|
|
|
const model = providerConfig.model || settings?.aiModel;
|
|
|
|
|
console.log(`[Journal Generate] Using model: ${model}`);
|
|
|
|
|
const baseUrl = providerConfig.baseUrl || settings?.aiBaseUrl;
|
|
|
|
|
|
|
|
|
|
const aiProvider = createAIProvider({
|
|
|
|
|
@@ -407,13 +455,13 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
|
|
|
|
|
|
|
|
|
console.log(`[Journal Generate] AI Provider created: ${aiProvider.provider}`);
|
|
|
|
|
|
|
|
|
|
const result = await aiProvider.generate(userPrompt, systemPrompt);
|
|
|
|
|
const result = await aiProvider.generate(userPrompt, systemPromptWithJson, { jsonMode: true });
|
|
|
|
|
|
|
|
|
|
if (!result.content) {
|
|
|
|
|
throw new Error('No content generated from AI');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[Journal Generate] Success! Content length: ${result.content.length}`);
|
|
|
|
|
console.log(`[Journal Generate] Success! Content length: ${result.content.length}, Title: ${result.title || 'N/A'}`);
|
|
|
|
|
|
|
|
|
|
// Update task with success - store full request and response JSON
|
|
|
|
|
await prisma.task.update({
|
|
|
|
|
@@ -422,14 +470,19 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
|
|
|
|
status: 'completed',
|
|
|
|
|
request: JSON.stringify(result.request, null, 2),
|
|
|
|
|
response: JSON.stringify(result.response, null, 2),
|
|
|
|
|
title: result.title,
|
|
|
|
|
completedAt: new Date(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update journal with content
|
|
|
|
|
// Update journal with content and title
|
|
|
|
|
const journal = await prisma.journal.update({
|
|
|
|
|
where: { id: placeholderJournal.id },
|
|
|
|
|
data: { content: result.content, generatedAt: new Date() },
|
|
|
|
|
data: {
|
|
|
|
|
content: result.content,
|
|
|
|
|
title: result.title,
|
|
|
|
|
generatedAt: new Date()
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: { journal, task }, error: null });
|
|
|
|
|
@@ -448,8 +501,16 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Delete placeholder journal
|
|
|
|
|
await prisma.journal.delete({ where: { id: placeholderJournal.id } });
|
|
|
|
|
// Only delete journal if it was newly created (not an existing one)
|
|
|
|
|
if (!existingJournal) {
|
|
|
|
|
await prisma.journal.delete({ where: { id: placeholderJournal.id } });
|
|
|
|
|
} else {
|
|
|
|
|
// Restore the existing content
|
|
|
|
|
await prisma.journal.update({
|
|
|
|
|
where: { id: placeholderJournal.id },
|
|
|
|
|
data: { content: existingJournal.content },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.json({ data: null, error: { code: 'AI_ERROR', message: `Failed to generate journal: ${errorMessage}` } }, 500);
|
|
|
|
|
}
|
|
|
|
|
@@ -469,9 +530,10 @@ app.get('/api/v1/journal/:date/tasks', async (c) => {
|
|
|
|
|
const tasks = await prisma.task.findMany({
|
|
|
|
|
where: { journalId: journal.id },
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
include: { journal: { select: { title: true } } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: tasks, error: null });
|
|
|
|
|
return c.json({ data: tasks.map(t => ({ ...t, journalTitle: t.journal?.title || null })), error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get('/api/v1/tasks/:id', async (c) => {
|
|
|
|
|
@@ -502,6 +564,30 @@ app.get('/api/v1/journal/:date', async (c) => {
|
|
|
|
|
return c.json({ data: journal, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get('/api/v1/journals', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const page = parseInt(c.req.query('page') || '1');
|
|
|
|
|
const limit = parseInt(c.req.query('limit') || '10');
|
|
|
|
|
const offset = (page - 1) * limit;
|
|
|
|
|
|
|
|
|
|
const [journals, total] = await Promise.all([
|
|
|
|
|
prisma.journal.findMany({
|
|
|
|
|
where: { userId },
|
|
|
|
|
orderBy: { date: 'desc' },
|
|
|
|
|
skip: offset,
|
|
|
|
|
take: limit,
|
|
|
|
|
}),
|
|
|
|
|
prisma.journal.count({ where: { userId } }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return c.json({
|
|
|
|
|
data: { journals, total, page, limit, totalPages: Math.ceil(total / limit) },
|
|
|
|
|
error: null,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Settings routes
|
|
|
|
|
app.get('/api/v1/settings', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
@@ -512,6 +598,11 @@ app.get('/api/v1/settings', async (c) => {
|
|
|
|
|
settings = await prisma.settings.create({ data: { userId } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const oldDefaultLength = 400;
|
|
|
|
|
if (settings.journalPrompt && settings.journalPrompt.length > oldDefaultLength) {
|
|
|
|
|
settings.journalPrompt = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.json({ data: settings, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -527,7 +618,7 @@ app.put('/api/v1/settings', async (c) => {
|
|
|
|
|
if (aiApiKey !== undefined) data.aiApiKey = aiApiKey;
|
|
|
|
|
if (aiModel !== undefined) data.aiModel = aiModel;
|
|
|
|
|
if (aiBaseUrl !== undefined) data.aiBaseUrl = aiBaseUrl;
|
|
|
|
|
if (journalPrompt !== undefined) data.journalPrompt = journalPrompt;
|
|
|
|
|
if (journalPrompt !== undefined) data.journalPrompt = journalPrompt === '' ? null : journalPrompt;
|
|
|
|
|
if (language !== undefined) data.language = language;
|
|
|
|
|
if (providerSettings !== undefined) data.providerSettings = JSON.stringify(providerSettings);
|
|
|
|
|
if (journalContextDays !== undefined) data.journalContextDays = journalContextDays;
|
|
|
|
|
@@ -589,12 +680,489 @@ app.post('/api/v1/ai/test', async (c) => {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const DEARDIARY_VERSION = '0.1.0';
|
|
|
|
|
const MIN_IMPORT_VERSION = '0.0.3';
|
|
|
|
|
|
|
|
|
|
interface ExportData {
|
|
|
|
|
version: string;
|
|
|
|
|
exportedAt: string;
|
|
|
|
|
settings: {
|
|
|
|
|
aiProvider: string;
|
|
|
|
|
aiApiKey?: string;
|
|
|
|
|
aiModel?: string;
|
|
|
|
|
aiBaseUrl?: string;
|
|
|
|
|
journalPrompt?: string;
|
|
|
|
|
language?: string;
|
|
|
|
|
timezone?: string;
|
|
|
|
|
providerSettings?: string;
|
|
|
|
|
journalContextDays?: number;
|
|
|
|
|
};
|
|
|
|
|
events: Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
date: string;
|
|
|
|
|
type: string;
|
|
|
|
|
content: string;
|
|
|
|
|
mediaPath?: string;
|
|
|
|
|
metadata?: string;
|
|
|
|
|
latitude?: number;
|
|
|
|
|
longitude?: number;
|
|
|
|
|
placeName?: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}>;
|
|
|
|
|
journals: Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
date: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
content: string;
|
|
|
|
|
eventCount: number;
|
|
|
|
|
generatedAt: string;
|
|
|
|
|
}>;
|
|
|
|
|
tasks: Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
journalId: string;
|
|
|
|
|
date: string;
|
|
|
|
|
type: string;
|
|
|
|
|
status: string;
|
|
|
|
|
provider: string;
|
|
|
|
|
model?: string;
|
|
|
|
|
prompt?: string;
|
|
|
|
|
request?: string;
|
|
|
|
|
response?: string;
|
|
|
|
|
error?: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
completedAt?: string;
|
|
|
|
|
}>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.get('/api/v1/export', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const [settings, events, journals, tasks] = await Promise.all([
|
|
|
|
|
prisma.settings.findUnique({ where: { userId } }),
|
|
|
|
|
prisma.event.findMany({ where: { userId }, orderBy: { createdAt: 'asc' } }),
|
|
|
|
|
prisma.journal.findMany({ where: { userId }, orderBy: { date: 'asc' } }),
|
|
|
|
|
prisma.task.findMany({ where: { userId }, orderBy: { createdAt: 'asc' } }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const journalMap = new Map(journals.map(j => [j.id, j]));
|
|
|
|
|
|
|
|
|
|
const exportData: ExportData = {
|
|
|
|
|
version: DEARDIARY_VERSION,
|
|
|
|
|
exportedAt: new Date().toISOString(),
|
|
|
|
|
settings: {
|
|
|
|
|
aiProvider: settings?.aiProvider || 'groq',
|
|
|
|
|
aiApiKey: settings?.aiApiKey,
|
|
|
|
|
aiModel: settings?.aiModel,
|
|
|
|
|
aiBaseUrl: settings?.aiBaseUrl,
|
|
|
|
|
journalPrompt: settings?.journalPrompt,
|
|
|
|
|
language: settings?.language,
|
|
|
|
|
timezone: settings?.timezone,
|
|
|
|
|
providerSettings: settings?.providerSettings,
|
|
|
|
|
journalContextDays: settings?.journalContextDays,
|
|
|
|
|
},
|
|
|
|
|
events: events.map(e => ({
|
|
|
|
|
id: e.id,
|
|
|
|
|
date: e.date,
|
|
|
|
|
type: e.type,
|
|
|
|
|
content: e.content,
|
|
|
|
|
mediaPath: e.mediaPath || undefined,
|
|
|
|
|
metadata: e.metadata || undefined,
|
|
|
|
|
latitude: e.latitude ?? undefined,
|
|
|
|
|
longitude: e.longitude ?? undefined,
|
|
|
|
|
placeName: e.placeName ?? undefined,
|
|
|
|
|
createdAt: e.createdAt.toISOString(),
|
|
|
|
|
})),
|
|
|
|
|
journals: journals.map(j => ({
|
|
|
|
|
id: j.id,
|
|
|
|
|
date: j.date,
|
|
|
|
|
title: j.title || undefined,
|
|
|
|
|
content: j.content,
|
|
|
|
|
eventCount: j.eventCount,
|
|
|
|
|
generatedAt: j.generatedAt.toISOString(),
|
|
|
|
|
})),
|
|
|
|
|
tasks: tasks.map(t => {
|
|
|
|
|
const journal = journalMap.get(t.journalId);
|
|
|
|
|
return {
|
|
|
|
|
id: t.id,
|
|
|
|
|
journalId: t.journalId,
|
|
|
|
|
date: journal?.date || '',
|
|
|
|
|
type: t.type,
|
|
|
|
|
status: t.status,
|
|
|
|
|
provider: t.provider,
|
|
|
|
|
model: t.model || undefined,
|
|
|
|
|
prompt: t.prompt || undefined,
|
|
|
|
|
request: t.request || undefined,
|
|
|
|
|
response: t.response || undefined,
|
|
|
|
|
error: t.error || undefined,
|
|
|
|
|
title: t.title || undefined,
|
|
|
|
|
createdAt: t.createdAt.toISOString(),
|
|
|
|
|
completedAt: t.completedAt?.toISOString() || undefined,
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return c.json({ data: exportData, error: null });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post('/api/v1/import', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
let importData: ExportData;
|
|
|
|
|
try {
|
|
|
|
|
importData = await c.req.json();
|
|
|
|
|
} catch {
|
|
|
|
|
return c.json({ data: null, error: { code: 'INVALID_JSON', message: 'Invalid JSON body' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!importData.version || !importData.events || !importData.journals) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'INVALID_FORMAT', message: 'Invalid export format: missing required fields' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const versionParts = importData.version.split('.');
|
|
|
|
|
const minParts = MIN_IMPORT_VERSION.split('.');
|
|
|
|
|
const versionNum = parseInt(versionParts[0]) * 10000 + parseInt(versionParts[1]) * 100 + parseInt(versionParts[2] || '0');
|
|
|
|
|
const minNum = parseInt(minParts[0]) * 10000 + parseInt(minParts[1]) * 100 + parseInt(minParts[2]);
|
|
|
|
|
|
|
|
|
|
const compatible = versionNum >= minNum;
|
|
|
|
|
|
|
|
|
|
if (!compatible) {
|
|
|
|
|
return c.json({
|
|
|
|
|
data: {
|
|
|
|
|
compatible: false,
|
|
|
|
|
importVersion: importData.version,
|
|
|
|
|
currentVersion: DEARDIARY_VERSION,
|
|
|
|
|
warning: `Import version ${importData.version} is older than minimum supported version ${MIN_IMPORT_VERSION}. Import may fail or lose data.`,
|
|
|
|
|
},
|
|
|
|
|
error: { code: 'VERSION_INCOMPATIBLE', message: `Import version ${importData.version} is not compatible with current version ${DEARDIARY_VERSION}` }
|
|
|
|
|
}, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let importedEvents = 0;
|
|
|
|
|
let importedJournals = 0;
|
|
|
|
|
let importedTasks = 0;
|
|
|
|
|
let skippedEvents = 0;
|
|
|
|
|
let skippedJournals = 0;
|
|
|
|
|
|
|
|
|
|
const idMapping = {
|
|
|
|
|
events: new Map<string, string>(),
|
|
|
|
|
journals: new Map<string, string>(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const event of importData.events) {
|
|
|
|
|
const existing = await prisma.event.findFirst({
|
|
|
|
|
where: { userId, date: event.date, content: event.content, createdAt: new Date(event.createdAt) }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
skippedEvents++;
|
|
|
|
|
idMapping.events.set(event.id, existing.id);
|
|
|
|
|
} else {
|
|
|
|
|
const created = await prisma.event.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId,
|
|
|
|
|
date: event.date,
|
|
|
|
|
type: event.type,
|
|
|
|
|
content: event.content,
|
|
|
|
|
mediaPath: event.mediaPath,
|
|
|
|
|
metadata: event.metadata,
|
|
|
|
|
latitude: event.latitude,
|
|
|
|
|
longitude: event.longitude,
|
|
|
|
|
placeName: event.placeName,
|
|
|
|
|
createdAt: new Date(event.createdAt),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
idMapping.events.set(event.id, created.id);
|
|
|
|
|
importedEvents++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const journal of importData.journals) {
|
|
|
|
|
const existing = await prisma.journal.findFirst({
|
|
|
|
|
where: { userId, date: journal.date }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
skippedJournals++;
|
|
|
|
|
} else {
|
|
|
|
|
await prisma.journal.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId,
|
|
|
|
|
date: journal.date,
|
|
|
|
|
title: journal.title,
|
|
|
|
|
content: journal.content,
|
|
|
|
|
eventCount: journal.eventCount,
|
|
|
|
|
generatedAt: new Date(journal.generatedAt),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
importedJournals++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const journalDateMap = new Map<string, string>();
|
|
|
|
|
const journals = await prisma.journal.findMany({ where: { userId }, select: { id: true, date: true } });
|
|
|
|
|
for (const j of journals) {
|
|
|
|
|
journalDateMap.set(j.date, j.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const task of importData.tasks) {
|
|
|
|
|
const newJournalId = journalDateMap.get(task.date);
|
|
|
|
|
if (!newJournalId) continue;
|
|
|
|
|
|
|
|
|
|
const existing = await prisma.task.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
userId,
|
|
|
|
|
journalId: newJournalId,
|
|
|
|
|
provider: task.provider,
|
|
|
|
|
createdAt: new Date(task.createdAt),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
|
await prisma.task.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId,
|
|
|
|
|
journalId: newJournalId,
|
|
|
|
|
type: task.type,
|
|
|
|
|
status: task.status,
|
|
|
|
|
provider: task.provider,
|
|
|
|
|
model: task.model,
|
|
|
|
|
prompt: task.prompt,
|
|
|
|
|
request: task.request,
|
|
|
|
|
response: task.response,
|
|
|
|
|
error: task.error,
|
|
|
|
|
title: task.title,
|
|
|
|
|
createdAt: new Date(task.createdAt),
|
|
|
|
|
completedAt: task.completedAt ? new Date(task.completedAt) : null,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
importedTasks++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (importData.settings) {
|
|
|
|
|
await prisma.settings.update({
|
|
|
|
|
where: { userId },
|
|
|
|
|
data: {
|
|
|
|
|
aiProvider: importData.settings.aiProvider || 'groq',
|
|
|
|
|
aiApiKey: importData.settings.aiApiKey,
|
|
|
|
|
aiModel: importData.settings.aiModel,
|
|
|
|
|
aiBaseUrl: importData.settings.aiBaseUrl,
|
|
|
|
|
journalPrompt: importData.settings.journalPrompt,
|
|
|
|
|
language: importData.settings.language,
|
|
|
|
|
timezone: importData.settings.timezone,
|
|
|
|
|
providerSettings: importData.settings.providerSettings,
|
|
|
|
|
journalContextDays: importData.settings.journalContextDays,
|
|
|
|
|
},
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.json({
|
|
|
|
|
data: {
|
|
|
|
|
compatible: true,
|
|
|
|
|
importedEvents,
|
|
|
|
|
importedJournals,
|
|
|
|
|
importedTasks,
|
|
|
|
|
skippedEvents,
|
|
|
|
|
skippedJournals,
|
|
|
|
|
totalEvents: importData.events.length,
|
|
|
|
|
totalJournals: importData.journals.length,
|
|
|
|
|
totalTasks: importData.tasks.length,
|
|
|
|
|
},
|
|
|
|
|
error: null
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : 'Import failed';
|
|
|
|
|
return c.json({ data: null, error: { code: 'IMPORT_FAILED', message } }, 500);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.delete('/api/v1/account', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await prisma.user.delete({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
api.clearApiKey();
|
|
|
|
|
|
|
|
|
|
return c.json({ data: { deleted: true }, error: null });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : 'Delete failed';
|
|
|
|
|
return c.json({ data: null, error: { code: 'DELETE_FAILED', message } }, 500);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post('/api/v1/account/reset', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await prisma.event.deleteMany({ where: { userId } });
|
|
|
|
|
await prisma.task.deleteMany({ where: { userId } });
|
|
|
|
|
await prisma.journal.deleteMany({ where: { userId } });
|
|
|
|
|
await prisma.settings.update({
|
|
|
|
|
where: { userId },
|
|
|
|
|
data: {
|
|
|
|
|
aiProvider: 'groq',
|
|
|
|
|
aiApiKey: null,
|
|
|
|
|
aiModel: 'llama-3.3-70b-versatile',
|
|
|
|
|
aiBaseUrl: null,
|
|
|
|
|
journalPrompt: null,
|
|
|
|
|
language: 'en',
|
|
|
|
|
timezone: 'UTC',
|
|
|
|
|
providerSettings: null,
|
|
|
|
|
journalContextDays: 10,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: { reset: true }, error: null });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : 'Reset failed';
|
|
|
|
|
return c.json({ data: null, error: { code: 'RESET_FAILED', message } }, 500);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post('/api/v1/account/password', async (c) => {
|
|
|
|
|
const userId = await getUserId(c);
|
|
|
|
|
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
|
|
|
|
|
|
|
|
|
const body = await c.req.json();
|
|
|
|
|
const { currentPassword, newPassword } = body;
|
|
|
|
|
|
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'currentPassword and newPassword are required' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newPassword.length < 6) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'New password must be at least 6 characters' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'User not found' } }, 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const valid = await bcrypt.compare(currentPassword, user.passwordHash);
|
|
|
|
|
if (!valid) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'INVALID_PASSWORD', message: 'Current password is incorrect' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newHash = await bcrypt.hash(newPassword, 12);
|
|
|
|
|
await prisma.user.update({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
data: { passwordHash: newHash },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: { changed: true }, error: null });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : 'Password change failed';
|
|
|
|
|
return c.json({ data: null, error: { code: 'CHANGE_FAILED', message } }, 500);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post('/api/v1/auth/register', async (c) => {
|
|
|
|
|
const registrationEnabled = envVars.REGISTRATION_ENABLED !== 'false';
|
|
|
|
|
if (!registrationEnabled) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'REGISTRATION_DISABLED', message: 'Registration is currently disabled' } }, 403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const body = await c.req.json();
|
|
|
|
|
const { email, password } = body;
|
|
|
|
|
|
|
|
|
|
if (!email || !password) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'email and password are required' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (password.length < 6) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'Password must be at least 6 characters' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const existing = await prisma.user.findUnique({ where: { email } });
|
|
|
|
|
if (existing) {
|
|
|
|
|
return c.json({ data: null, error: { code: 'EMAIL_EXISTS', message: 'Email already registered' } }, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const passwordHash = await bcrypt.hash(password, 12);
|
|
|
|
|
const user = await prisma.user.create({
|
|
|
|
|
data: {
|
|
|
|
|
email,
|
|
|
|
|
passwordHash,
|
|
|
|
|
settings: { create: {} },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const apiKey = randomBytes(32).toString('hex');
|
|
|
|
|
const keyHash = createHash('sha256').update(apiKey).digest('hex');
|
|
|
|
|
await prisma.apiKey.create({
|
|
|
|
|
data: { userId: user.id, keyHash, name: 'Default' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return c.json({ data: { apiKey, userId: user.id }, error: null }, 201);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : 'Registration failed';
|
|
|
|
|
return c.json({ data: null, error: { code: 'REGISTRATION_FAILED', message } }, 500);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.notFound((c) => c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Not found' } }, 404));
|
|
|
|
|
app.onError((err, c) => {
|
|
|
|
|
console.error('Unhandled error:', err);
|
|
|
|
|
return c.json({ data: null, error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } }, 500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function setupFTS() {
|
|
|
|
|
try {
|
|
|
|
|
await prisma.$executeRaw`CREATE VIRTUAL TABLE IF NOT EXISTS journal_fts USING fts5(
|
|
|
|
|
userId UNINDEXED,
|
|
|
|
|
date UNINDEXED,
|
|
|
|
|
title,
|
|
|
|
|
content,
|
|
|
|
|
tokenize='porter unicode61'
|
|
|
|
|
)`;
|
|
|
|
|
|
|
|
|
|
await prisma.$executeRaw`CREATE VIRTUAL TABLE IF NOT EXISTS event_fts USING fts5(
|
|
|
|
|
userId UNINDEXED,
|
|
|
|
|
date UNINDEXED,
|
|
|
|
|
type UNINDEXED,
|
|
|
|
|
content,
|
|
|
|
|
tokenize='porter unicode61'
|
|
|
|
|
)`;
|
|
|
|
|
|
|
|
|
|
console.log('FTS tables ready');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('FTS setup error:', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function rebuildFTS() {
|
|
|
|
|
try {
|
|
|
|
|
const journals = await prisma.journal.findMany();
|
|
|
|
|
await prisma.$executeRaw`DELETE FROM journal_fts`;
|
|
|
|
|
for (const j of journals) {
|
|
|
|
|
await prisma.$executeRaw`INSERT INTO journal_fts(userId, date, title, content) VALUES (${j.userId}, ${j.date}, ${j.title || ''}, ${j.content})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const events = await prisma.event.findMany();
|
|
|
|
|
await prisma.$executeRaw`DELETE FROM event_fts`;
|
|
|
|
|
for (const e of events) {
|
|
|
|
|
await prisma.$executeRaw`INSERT INTO event_fts(userId, date, type, content) VALUES (${e.userId}, ${e.date}, ${e.type}, ${e.content})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('FTS indexes rebuilt');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('FTS rebuild error:', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createDefaultUser() {
|
|
|
|
|
const defaultEmail = envVars.DEFAULT_USER_EMAIL;
|
|
|
|
|
const defaultPassword = envVars.DEFAULT_USER_PASSWORD;
|
|
|
|
|
@@ -647,7 +1215,11 @@ async function createDefaultUser() {
|
|
|
|
|
const port = parseInt(envVars.PORT || '3000', 10);
|
|
|
|
|
console.log(`Starting DearDiary API on port ${port}`);
|
|
|
|
|
|
|
|
|
|
createDefaultUser().then(() => {
|
|
|
|
|
setupFTS().then(() => {
|
|
|
|
|
return rebuildFTS();
|
|
|
|
|
}).then(() => {
|
|
|
|
|
return createDefaultUser();
|
|
|
|
|
}).then(() => {
|
|
|
|
|
console.log('Server ready');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|