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. 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)
|
- [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)
|
||||||
- [ ] 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.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
|
- [x] 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.4 Modify `GET /api/export` to accept `?chartId=` query param and scope exported annotations to that chart
|
||||||
|
|
||||||
## 5. Chart Selector UI Component
|
## 5. Chart Selector UI Component
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { annotations } from '@/lib/db/schema';
|
import { annotations, charts } from '@/lib/db/schema';
|
||||||
import { eq, inArray } from 'drizzle-orm';
|
import { eq, inArray, and, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
// GET all annotations
|
// GET annotations scoped by chartId
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
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
|
// Parse geometry from JSON string
|
||||||
const parsed = allAnnotations.map((annotation) => ({
|
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) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { timestamp, label_type, geometry, color } = body;
|
const { timestamp, label_type, chart_id, geometry, color } = body;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!timestamp || !label_type) {
|
if (!timestamp || !label_type || !chart_id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'timestamp and label_type are required' },
|
{ error: 'timestamp, label_type, and chart_id are required' },
|
||||||
{ status: 400 }
|
{ 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
|
// Serialize geometry to JSON string if present
|
||||||
const geometryString = geometry ? JSON.stringify(geometry) : null;
|
const geometryString = geometry ? JSON.stringify(geometry) : null;
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.insert(annotations)
|
.insert(annotations)
|
||||||
.values({
|
.values({
|
||||||
|
chart_id,
|
||||||
timestamp,
|
timestamp,
|
||||||
label_type,
|
label_type,
|
||||||
geometry: geometryString,
|
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) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = request.nextUrl;
|
const { searchParams } = request.nextUrl;
|
||||||
const type = searchParams.get('type');
|
const type = searchParams.get('type');
|
||||||
const all = searchParams.get('all');
|
const all = searchParams.get('all');
|
||||||
|
const chartId = searchParams.get('chartId');
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
if (all === 'true') {
|
if (all === 'true') {
|
||||||
// Delete all annotations
|
if (chartId) {
|
||||||
|
result = await db.delete(annotations).where(eq(annotations.chart_id, parseInt(chartId, 10))).returning();
|
||||||
|
} else {
|
||||||
result = await db.delete(annotations).returning();
|
result = await db.delete(annotations).returning();
|
||||||
|
}
|
||||||
} else if (type) {
|
} else if (type) {
|
||||||
// Delete by type(s)
|
|
||||||
const types = type.split(',').map((t) => t.trim());
|
const types = type.split(',').map((t) => t.trim());
|
||||||
|
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
|
result = await db
|
||||||
.delete(annotations)
|
.delete(annotations)
|
||||||
.where(inArray(annotations.label_type, types))
|
.where(inArray(annotations.label_type, types))
|
||||||
.returning();
|
.returning();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No filter specified
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Specify type or all parameter for bulk delete' },
|
{ error: 'Specify type or all parameter for bulk delete' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,32 @@
|
||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { candles } from '@/lib/db/schema';
|
import { candles, charts } from '@/lib/db/schema';
|
||||||
import { asc } from 'drizzle-orm';
|
import { asc, desc, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
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
|
const allCandles = await db
|
||||||
.select()
|
.select({
|
||||||
|
time: candles.time,
|
||||||
|
open: candles.open,
|
||||||
|
high: candles.high,
|
||||||
|
low: candles.low,
|
||||||
|
close: candles.close,
|
||||||
|
})
|
||||||
.from(candles)
|
.from(candles)
|
||||||
|
.where(eq(candles.chart_id, parseInt(chartId, 10)))
|
||||||
.orderBy(asc(candles.time));
|
.orderBy(asc(candles.time));
|
||||||
|
|
||||||
return NextResponse.json(allCandles);
|
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 { db } from '@/lib/db';
|
||||||
import { annotations, candles } from '@/lib/db/schema';
|
import { annotations, candles, charts } from '@/lib/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
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
|
// Build CSV content
|
||||||
const csvRows = ['timestamp,label_type,price'];
|
const csvRows = ['timestamp,label_type,price'];
|
||||||
|
|
@ -14,18 +35,16 @@ export async function GET() {
|
||||||
let price: number | null = null;
|
let price: number | null = null;
|
||||||
|
|
||||||
if (annotation.label_type === 'break_up' || annotation.label_type === 'break_down') {
|
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
|
const candleResult = await db
|
||||||
.select()
|
.select()
|
||||||
.from(candles)
|
.from(candles)
|
||||||
.where(eq(candles.time, annotation.timestamp))
|
.where(and(eq(candles.chart_id, chartIdNum), eq(candles.time, annotation.timestamp)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (candleResult.length > 0) {
|
if (candleResult.length > 0) {
|
||||||
price = candleResult[0].close;
|
price = candleResult[0].close;
|
||||||
}
|
}
|
||||||
} else if (annotation.label_type === 'line' && annotation.geometry) {
|
} else if (annotation.label_type === 'line' && annotation.geometry) {
|
||||||
// For line annotations, use startPrice from geometry
|
|
||||||
const geometry = JSON.parse(annotation.geometry);
|
const geometry = JSON.parse(annotation.geometry);
|
||||||
price = geometry.startPrice || null;
|
price = geometry.startPrice || null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue