feat: scope candles/annotations/export APIs by chartId query param
- GET /api/candles accepts ?chartId= with fallback to most recent chart - GET /api/annotations accepts ?chartId= with fallback to most recent chart - POST /api/annotations now requires chart_id in request body - GET /api/export accepts ?chartId= to scope exported annotations - DELETE /api/annotations supports optional chartId scoping
This commit is contained in:
parent
98e91b047a
commit
90e1e179cc
4 changed files with 107 additions and 35 deletions
|
|
@ -21,10 +21,10 @@
|
|||
|
||||
## 4. Candles & Annotations API Scoping
|
||||
|
||||
- [ ] 4.1 Modify `GET /api/candles` to accept `?chartId=` query param and filter by chart_id (fall back to most recent chart if omitted)
|
||||
- [ ] 4.2 Modify `GET /api/annotations` to accept `?chartId=` query param and filter by chart_id (fall back to most recent chart if omitted)
|
||||
- [ ] 4.3 Modify `POST /api/annotations` to require `chart_id` in request body and store it
|
||||
- [ ] 4.4 Modify `GET /api/export` to accept `?chartId=` query param and scope exported annotations to that chart
|
||||
- [x] 4.1 Modify `GET /api/candles` to accept `?chartId=` query param and filter by chart_id (fall back to most recent chart if omitted)
|
||||
- [x] 4.2 Modify `GET /api/annotations` to accept `?chartId=` query param and filter by chart_id (fall back to most recent chart if omitted)
|
||||
- [x] 4.3 Modify `POST /api/annotations` to require `chart_id` in request body and store it
|
||||
- [x] 4.4 Modify `GET /api/export` to accept `?chartId=` query param and scope exported annotations to that chart
|
||||
|
||||
## 5. Chart Selector UI Component
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { annotations } from '@/lib/db/schema';
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { annotations, charts } from '@/lib/db/schema';
|
||||
import { eq, inArray, and, desc } from 'drizzle-orm';
|
||||
|
||||
// GET all annotations
|
||||
export async function GET() {
|
||||
// GET annotations scoped by chartId
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const allAnnotations = await db.select().from(annotations);
|
||||
const { searchParams } = request.nextUrl;
|
||||
let chartId = searchParams.get('chartId');
|
||||
|
||||
// Fall back to most recent chart if no chartId provided
|
||||
if (!chartId) {
|
||||
const latest = await db.select({ id: charts.id }).from(charts).orderBy(desc(charts.created_at)).limit(1);
|
||||
if (latest.length === 0) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
chartId = String(latest[0].id);
|
||||
}
|
||||
|
||||
const allAnnotations = await db
|
||||
.select()
|
||||
.from(annotations)
|
||||
.where(eq(annotations.chart_id, parseInt(chartId, 10)));
|
||||
|
||||
// Parse geometry from JSON string
|
||||
const parsed = allAnnotations.map((annotation) => ({
|
||||
|
|
@ -23,26 +38,36 @@ export async function GET() {
|
|||
}
|
||||
}
|
||||
|
||||
// POST create new annotation
|
||||
// POST create new annotation (requires chart_id)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { timestamp, label_type, geometry, color } = body;
|
||||
const { timestamp, label_type, chart_id, geometry, color } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!timestamp || !label_type) {
|
||||
if (!timestamp || !label_type || !chart_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'timestamp and label_type are required' },
|
||||
{ error: 'timestamp, label_type, and chart_id are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify chart exists
|
||||
const chartExists = await db.select({ id: charts.id }).from(charts).where(eq(charts.id, chart_id)).limit(1);
|
||||
if (chartExists.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Chart not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
|
@ -68,27 +93,36 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
// DELETE annotations with bulk operations
|
||||
// DELETE annotations with bulk operations (scoped by chartId)
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = request.nextUrl;
|
||||
const type = searchParams.get('type');
|
||||
const all = searchParams.get('all');
|
||||
const chartId = searchParams.get('chartId');
|
||||
|
||||
let result;
|
||||
|
||||
if (all === 'true') {
|
||||
// Delete all annotations
|
||||
result = await db.delete(annotations).returning();
|
||||
if (chartId) {
|
||||
result = await db.delete(annotations).where(eq(annotations.chart_id, parseInt(chartId, 10))).returning();
|
||||
} else {
|
||||
result = await db.delete(annotations).returning();
|
||||
}
|
||||
} else if (type) {
|
||||
// Delete by type(s)
|
||||
const types = type.split(',').map((t) => t.trim());
|
||||
result = await db
|
||||
.delete(annotations)
|
||||
.where(inArray(annotations.label_type, types))
|
||||
.returning();
|
||||
if (chartId) {
|
||||
result = await db
|
||||
.delete(annotations)
|
||||
.where(and(inArray(annotations.label_type, types), eq(annotations.chart_id, parseInt(chartId, 10))))
|
||||
.returning();
|
||||
} else {
|
||||
result = await db
|
||||
.delete(annotations)
|
||||
.where(inArray(annotations.label_type, types))
|
||||
.returning();
|
||||
}
|
||||
} else {
|
||||
// No filter specified
|
||||
return NextResponse.json(
|
||||
{ error: 'Specify type or all parameter for bulk delete' },
|
||||
{ status: 400 }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,32 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { candles } from '@/lib/db/schema';
|
||||
import { asc } from 'drizzle-orm';
|
||||
import { candles, charts } from '@/lib/db/schema';
|
||||
import { asc, desc, eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = request.nextUrl;
|
||||
let chartId = searchParams.get('chartId');
|
||||
|
||||
// Fall back to most recent chart if no chartId provided
|
||||
if (!chartId) {
|
||||
const latest = await db.select({ id: charts.id }).from(charts).orderBy(desc(charts.created_at)).limit(1);
|
||||
if (latest.length === 0) {
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
chartId = String(latest[0].id);
|
||||
}
|
||||
|
||||
const allCandles = await db
|
||||
.select()
|
||||
.select({
|
||||
time: candles.time,
|
||||
open: candles.open,
|
||||
high: candles.high,
|
||||
low: candles.low,
|
||||
close: candles.close,
|
||||
})
|
||||
.from(candles)
|
||||
.where(eq(candles.chart_id, parseInt(chartId, 10)))
|
||||
.orderBy(asc(candles.time));
|
||||
|
||||
return NextResponse.json(allCandles);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,32 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { annotations, candles } from '@/lib/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { annotations, candles, charts } from '@/lib/db/schema';
|
||||
import { eq, and, desc } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const allAnnotations = await db.select().from(annotations);
|
||||
const { searchParams } = request.nextUrl;
|
||||
let chartId = searchParams.get('chartId');
|
||||
|
||||
// Fall back to most recent chart if no chartId provided
|
||||
if (!chartId) {
|
||||
const latest = await db.select({ id: charts.id }).from(charts).orderBy(desc(charts.created_at)).limit(1);
|
||||
if (latest.length === 0) {
|
||||
return new NextResponse('timestamp,label_type,price\n', {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': 'attachment; filename="annotations.csv"',
|
||||
},
|
||||
});
|
||||
}
|
||||
chartId = String(latest[0].id);
|
||||
}
|
||||
|
||||
const chartIdNum = parseInt(chartId, 10);
|
||||
const allAnnotations = await db
|
||||
.select()
|
||||
.from(annotations)
|
||||
.where(eq(annotations.chart_id, chartIdNum));
|
||||
|
||||
// Build CSV content
|
||||
const csvRows = ['timestamp,label_type,price'];
|
||||
|
|
@ -14,18 +35,16 @@ export async function GET() {
|
|||
let price: number | null = null;
|
||||
|
||||
if (annotation.label_type === 'break_up' || annotation.label_type === 'break_down') {
|
||||
// For marker annotations, look up the candle's close price
|
||||
const candleResult = await db
|
||||
.select()
|
||||
.from(candles)
|
||||
.where(eq(candles.time, annotation.timestamp))
|
||||
.where(and(eq(candles.chart_id, chartIdNum), eq(candles.time, annotation.timestamp)))
|
||||
.limit(1);
|
||||
|
||||
if (candleResult.length > 0) {
|
||||
price = candleResult[0].close;
|
||||
}
|
||||
} else if (annotation.label_type === 'line' && annotation.geometry) {
|
||||
// For line annotations, use startPrice from geometry
|
||||
const geometry = JSON.parse(annotation.geometry);
|
||||
price = geometry.startPrice || null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue