feat: migrate from SQLite to PostgreSQL - complete schema and API updates
- Remove better-sqlite3, add pg driver - Convert schema to PostgreSQL types (serial, timestamp, boolean, jsonb) - Generate fresh PostgreSQL migrations - Update database connection layer with pg.Pool - Fix all API routes: remove JSON.parse/stringify, use native timestamps and booleans - Update drizzle.config.ts and .env.example for PostgreSQL
This commit is contained in:
parent
4605283d2b
commit
5f70f13da3
37 changed files with 1164 additions and 1825 deletions
|
|
@ -30,7 +30,6 @@ export async function POST(request: NextRequest) {
|
|||
return NextResponse.json({ message: 'Types already seeded' });
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const defaultTypes = [
|
||||
{
|
||||
name: 'break_up',
|
||||
|
|
@ -38,8 +37,7 @@ export async function POST(request: NextRequest) {
|
|||
color: '#10b981',
|
||||
category: 'marker',
|
||||
icon: 'arrowUp',
|
||||
is_active: 1,
|
||||
created_at: now,
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
name: 'break_down',
|
||||
|
|
@ -47,8 +45,7 @@ export async function POST(request: NextRequest) {
|
|||
color: '#ef4444',
|
||||
category: 'marker',
|
||||
icon: 'arrowDown',
|
||||
is_active: 1,
|
||||
created_at: now,
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
name: 'line',
|
||||
|
|
@ -56,8 +53,7 @@ export async function POST(request: NextRequest) {
|
|||
color: '#3b82f6',
|
||||
category: 'line',
|
||||
icon: 'line',
|
||||
is_active: 1,
|
||||
created_at: now,
|
||||
is_active: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -83,8 +79,7 @@ export async function POST(request: NextRequest) {
|
|||
color,
|
||||
category,
|
||||
icon: icon || null,
|
||||
is_active: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
is_active: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export async function PATCH(
|
|||
|
||||
const result = await db
|
||||
.update(annotations)
|
||||
.set({ geometry: JSON.stringify(geometry) })
|
||||
.set({ geometry })
|
||||
.where(eq(annotations.id, id))
|
||||
.returning();
|
||||
|
||||
|
|
@ -41,11 +41,7 @@ export async function PATCH(
|
|||
);
|
||||
}
|
||||
|
||||
const updated = result[0];
|
||||
return NextResponse.json({
|
||||
...updated,
|
||||
geometry: updated.geometry ? JSON.parse(updated.geometry as string) : null,
|
||||
});
|
||||
return NextResponse.json(result[0]);
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to update annotation' },
|
||||
|
|
|
|||
|
|
@ -23,13 +23,7 @@ export async function GET(request: NextRequest) {
|
|||
.from(annotations)
|
||||
.where(eq(annotations.chart_id, parseInt(chartId, 10)));
|
||||
|
||||
// Parse geometry from JSON string
|
||||
const parsed = allAnnotations.map((annotation) => ({
|
||||
...annotation,
|
||||
geometry: annotation.geometry ? JSON.parse(annotation.geometry) : null,
|
||||
}));
|
||||
|
||||
return NextResponse.json(parsed);
|
||||
return NextResponse.json(allAnnotations);
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to fetch annotations' },
|
||||
|
|
@ -61,30 +55,18 @@ export async function POST(request: NextRequest) {
|
|||
);
|
||||
}
|
||||
|
||||
// Serialize geometry to JSON string if present
|
||||
const geometryString = geometry ? JSON.stringify(geometry) : null;
|
||||
|
||||
const result = await db
|
||||
.insert(annotations)
|
||||
.values({
|
||||
chart_id,
|
||||
timestamp,
|
||||
label_type,
|
||||
geometry: geometryString,
|
||||
geometry: geometry || null,
|
||||
color: color || '#3b82f6',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
.returning();
|
||||
|
||||
const created = result[0];
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
...created,
|
||||
geometry: created.geometry ? JSON.parse(created.geometry) : null,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
return NextResponse.json(result[0], { status: 201 });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to create annotation' },
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
|
|||
price = candleResult[0].close;
|
||||
}
|
||||
} else if (annotation.label_type === 'line' && annotation.geometry) {
|
||||
const geometry = JSON.parse(annotation.geometry);
|
||||
const geometry = annotation.geometry as any;
|
||||
price = geometry.startPrice || null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export async function GET(request: NextRequest) {
|
|||
confidence: span.confidence,
|
||||
outcome: span.outcome,
|
||||
notes: span.notes,
|
||||
sub_spans: span.sub_spans ? JSON.parse(span.sub_spans as string) : null,
|
||||
sub_spans: span.sub_spans,
|
||||
color: span.color,
|
||||
created_at: span.created_at,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export async function PATCH(
|
|||
if (confidence !== undefined) updates.confidence = confidence;
|
||||
if (outcome !== undefined) updates.outcome = outcome;
|
||||
if (notes !== undefined) updates.notes = notes;
|
||||
if (sub_spans !== undefined) updates.sub_spans = sub_spans ? JSON.stringify(sub_spans) : null;
|
||||
if (sub_spans !== undefined) updates.sub_spans = sub_spans || null;
|
||||
|
||||
const result = await db
|
||||
.update(spanAnnotations)
|
||||
|
|
|
|||
|
|
@ -69,17 +69,17 @@ export async function GET(request: NextRequest) {
|
|||
// Convert to ML pipeline format
|
||||
const annotations = spans.map(span => ({
|
||||
id: span.id,
|
||||
start_time: new Date(span.start_time * 1000).toISOString(),
|
||||
end_time: new Date(span.end_time * 1000).toISOString(),
|
||||
start_time: span.start_time,
|
||||
end_time: span.end_time,
|
||||
label: span.label,
|
||||
confidence: span.confidence,
|
||||
outcome: span.outcome,
|
||||
notes: span.notes,
|
||||
sub_spans: span.sub_spans ? JSON.parse(span.sub_spans) : null,
|
||||
sub_spans: span.sub_spans,
|
||||
color: span.color,
|
||||
source: span.source,
|
||||
model_prediction: span.model_prediction ? JSON.parse(span.model_prediction) : null,
|
||||
created_at: new Date(span.created_at * 1000).toISOString(),
|
||||
model_prediction: span.model_prediction,
|
||||
created_at: span.created_at,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
|
|
@ -93,12 +93,10 @@ export async function GET(request: NextRequest) {
|
|||
const csvRows = ['id,start_time,end_time,label,confidence,outcome,notes'];
|
||||
|
||||
for (const span of spans) {
|
||||
const startTime = new Date(span.start_time * 1000).toISOString();
|
||||
const endTime = new Date(span.end_time * 1000).toISOString();
|
||||
const notes = span.notes ? `"${span.notes.replace(/"/g, '""')}"` : '';
|
||||
|
||||
csvRows.push(
|
||||
`${span.id},${startTime},${endTime},${span.label},${span.confidence || ''},${span.outcome || ''},${notes}`
|
||||
`${span.id},${span.start_time},${span.end_time},${span.label},${span.confidence || ''},${span.outcome || ''},${notes}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,11 +71,10 @@ export async function POST(request: NextRequest) {
|
|||
confidence: confidence || null,
|
||||
outcome: outcome || null,
|
||||
notes: notes || null,
|
||||
sub_spans: sub_spans ? JSON.stringify(sub_spans) : null,
|
||||
sub_spans: sub_spans || null,
|
||||
color: color || '#2196F3',
|
||||
source: source || 'human', // 'human', 'model', or 'human_correction'
|
||||
model_prediction: model_prediction ? JSON.stringify(model_prediction) : null,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
model_prediction: model_prediction || null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export async function GET() {
|
|||
const types = await db
|
||||
.select()
|
||||
.from(spanLabelTypes)
|
||||
.where(eq(spanLabelTypes.is_active, 1))
|
||||
.where(eq(spanLabelTypes.is_active, true))
|
||||
.orderBy(spanLabelTypes.sort_order);
|
||||
|
||||
return NextResponse.json(types);
|
||||
|
|
@ -42,9 +42,8 @@ export async function POST(request: NextRequest) {
|
|||
display_name,
|
||||
color,
|
||||
hotkey: hotkey || null,
|
||||
is_active: 1,
|
||||
is_active: true,
|
||||
sort_order: sort_order ?? 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|||
// Create the chart
|
||||
const [newChart] = await db.insert(charts).values({
|
||||
name: chartName,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
}).returning();
|
||||
|
||||
// Parse and prepare candle data
|
||||
|
|
@ -99,9 +98,10 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|||
if (isNaN(date.getTime())) {
|
||||
throw new Error(`Invalid date format: ${row.time}`);
|
||||
}
|
||||
timestamp = Math.floor(date.getTime() / 1000);
|
||||
timestamp = date; // PostgreSQL timestamp type expects Date object or ISO string
|
||||
} else if (typeof row.time === 'number') {
|
||||
timestamp = row.time;
|
||||
// If Unix timestamp (seconds), convert to Date
|
||||
timestamp = new Date(row.time * 1000);
|
||||
} else {
|
||||
throw new Error(`Invalid time value: ${row.time}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue