Skip to content

Commit 8244e9f

Browse files
Merge pull request #12 from SinSo-API/dev
feat: search endpoint
2 parents 5c77f5f + c525445 commit 8244e9f

5 files changed

Lines changed: 375 additions & 1 deletion

File tree

doc/openapi.spec.ts

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,138 @@ export const generateOpenAPISpec = () => {
383383
}
384384
}
385385
}
386+
},
387+
'/api/v1/search': {
388+
get: {
389+
tags: ['Search'],
390+
summary: 'Search songs',
391+
description: 'Search for songs across title, artist name, and lyrics content. Supports Sinhala and English text.',
392+
parameters: [
393+
{
394+
name: 'all',
395+
in: 'query',
396+
description: 'Search across song title, artist name, and lyrics (Sinhala or English)',
397+
required: false,
398+
schema: { type: 'string' }
399+
},
400+
{
401+
name: 'artist',
402+
in: 'query',
403+
description: 'Search by artist name (Sinhala or English)',
404+
required: false,
405+
schema: { type: 'string' }
406+
},
407+
{
408+
name: 'title',
409+
in: 'query',
410+
description: 'Search by song title (Sinhala or English)',
411+
required: false,
412+
schema: { type: 'string' }
413+
},
414+
{
415+
name: 'lyrics',
416+
in: 'query',
417+
description: 'Search by lyrics content (Sinhala or English)',
418+
required: false,
419+
schema: { type: 'string' }
420+
},
421+
{
422+
name: 'page',
423+
in: 'query',
424+
description: 'Page number',
425+
required: false,
426+
schema: { type: 'string', example: '1', default: '1' }
427+
},
428+
{
429+
name: 'limit',
430+
in: 'query',
431+
description: 'Items per page (max: 100)',
432+
required: false,
433+
schema: { type: 'string', example: '10', default: '10' }
434+
},
435+
{
436+
name: 'sortBy',
437+
in: 'query',
438+
description: 'Sort field',
439+
required: false,
440+
schema: { type: 'string', default: 'ViewCount' }
441+
},
442+
{
443+
name: 'sortOrder',
444+
in: 'query',
445+
description: 'Sort order (asc/desc)',
446+
required: false,
447+
schema: { type: 'string', enum: ['asc', 'desc'], default: 'desc' }
448+
}
449+
],
450+
responses: {
451+
'200': {
452+
description: 'Search results retrieved successfully',
453+
content: {
454+
'application/json': {
455+
schema: {
456+
type: 'object',
457+
properties: {
458+
success: { type: 'boolean', example: true },
459+
data: {
460+
type: 'array',
461+
items: { $ref: '#/components/schemas/SearchResult' }
462+
},
463+
pagination: {
464+
type: 'object',
465+
properties: {
466+
page: { type: 'number', example: 1 },
467+
limit: { type: 'number', example: 10 },
468+
total: { type: 'number', example: 50 },
469+
totalPages: { type: 'number', example: 5 }
470+
}
471+
}
472+
}
473+
}
474+
}
475+
}
476+
},
477+
'400': {
478+
description: 'No search parameters provided',
479+
content: {
480+
'application/json': {
481+
schema: {
482+
type: 'object',
483+
properties: {
484+
success: { type: 'boolean', example: false },
485+
message: { type: 'string', example: 'No search parameters provided.' }
486+
}
487+
}
488+
}
489+
}
490+
},
491+
'500': {
492+
description: 'Internal server error',
493+
content: {
494+
'application/json': {
495+
schema: { $ref: '#/components/schemas/ErrorResponse' }
496+
}
497+
}
498+
}
499+
}
500+
}
501+
},
502+
'/api/v1/search/health': {
503+
get: {
504+
tags: ['Search'],
505+
summary: 'Check search service health',
506+
description: 'Health check endpoint for the search service',
507+
responses: {
508+
'200': {
509+
description: 'Service is healthy',
510+
content: {
511+
'application/json': {
512+
schema: { $ref: '#/components/schemas/HealthCheck' }
513+
}
514+
}
515+
}
516+
}
517+
}
386518
}
387519
},
388520
components: {
@@ -435,7 +567,7 @@ export const generateOpenAPISpec = () => {
435567
properties: {
436568
ArtistID: { type: 'string', example: 'ART-00001' },
437569
ArtistName: { type: 'string', example: 'John Doe' },
438-
ArtistNameSinhala: { type: 'string', example: 'ජොන් ඩෝ' },
570+
ArtistNameSinhala: { type: 'string', example: 'ජෝන් ඩෝ' },
439571
Songs: {
440572
type: 'array',
441573
items: {
@@ -460,6 +592,82 @@ export const generateOpenAPISpec = () => {
460592
LyricContentSinhala: { type: 'string', example: 'සම්පූර්ණ ගී පද...' }
461593
}
462594
},
595+
SearchResult: {
596+
type: 'object',
597+
properties: {
598+
SongID: {
599+
type: 'string',
600+
example: 'SNG-00001',
601+
description: 'Unique identifier for the song'
602+
},
603+
SongName: {
604+
type: 'string',
605+
example: 'Beautiful Song',
606+
description: 'Song title in English'
607+
},
608+
SongNameSinhala: {
609+
type: 'string',
610+
example: 'ලස්සන ගීතය',
611+
description: 'Song title in Sinhala'
612+
},
613+
Duration: {
614+
type: 'number',
615+
example: 240,
616+
description: 'Song duration in seconds'
617+
},
618+
ReleaseYear: {
619+
type: 'number',
620+
example: 2024,
621+
description: 'Year the song was released'
622+
},
623+
Composer: {
624+
type: 'string',
625+
example: 'John Doe',
626+
description: 'Name of the music composer'
627+
},
628+
Lyricist: {
629+
type: 'string',
630+
example: 'Jane Smith',
631+
description: 'Name of the lyricist'
632+
},
633+
ViewCount: {
634+
type: 'number',
635+
example: 1000,
636+
description: 'Number of times the song has been viewed'
637+
},
638+
ArtistID: {
639+
type: 'string',
640+
example: 'ART-00001',
641+
description: 'Unique identifier for the artist'
642+
},
643+
ArtistName: {
644+
type: 'string',
645+
example: 'John Doe',
646+
description: 'Artist name in English'
647+
},
648+
ArtistNameSinhala: {
649+
type: 'string',
650+
example: 'ජෝන් ඩෝ',
651+
description: 'Artist name in Sinhala'
652+
},
653+
LyricID: {
654+
type: 'string',
655+
example: 'LYR-00001',
656+
description: 'Unique identifier for the lyrics'
657+
},
658+
LyricContent: {
659+
type: 'string',
660+
example: 'Full lyrics in English...',
661+
description: 'Complete lyrics content in English'
662+
},
663+
LyricContentSinhala: {
664+
type: 'string',
665+
example: 'සම්පූර්ණ ගී පද...',
666+
description: 'Complete lyrics content in Sinhala'
667+
}
668+
},
669+
description: 'Search result combining song, artist, and lyrics information'
670+
},
463671
HealthCheck: {
464672
type: 'object',
465673
properties: {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { Context } from 'hono';
2+
import { Env } from '../models/Search';
3+
import { APP_VERSION } from '../metadata';
4+
5+
export class SearchController{
6+
static checkHealth(c: Context<{ Bindings: Env }>){
7+
return c.json({
8+
status: 'OK',
9+
timestamp: new Date().toISOString(),
10+
environment: 'Production',
11+
version: APP_VERSION
12+
});
13+
}
14+
15+
static async searchSongs(c: Context<{ Bindings: Env }>) {
16+
try {
17+
const query = c.req.query();
18+
const all = query.all;
19+
const artist = query.artist;
20+
const title = query.title;
21+
const lyrics = query.lyrics;
22+
const page = parseInt(query.page || "1");
23+
const limit = Math.min(parseInt(query.limit || "10"), 100);
24+
const sortBy = query.sortBy || "ViewCount";
25+
const sortOrder =
26+
query.sortOrder?.toLowerCase() === "asc" ? "ASC" : "DESC";
27+
28+
if (!all && !artist && !title && !lyrics) {
29+
return c.json({
30+
success: false,
31+
message: "No search parameters provided.",
32+
'Available Search Parameters': {
33+
all: "Search by song title, artist name, or lyrics (Sinhala or English). Example: ?all=love",
34+
artist: "Search by artist name (Sinhala or English). Example: ?artist=Sunil",
35+
title: "Search by song title (Sinhala or English). Example: ?title=Amma",
36+
lyrics: "Search by lyrics content (Sinhala or English). Example: ?lyrics=සුන්දර",
37+
pagination: {
38+
page: "Current page number (default: 1)",
39+
limit: "Number of items per page (default: 10, max: 100)",
40+
},
41+
sorting: {
42+
sortBy: "Field to sort by (default: ViewCount)",
43+
sortOrder: "Sort order (asc or desc, default: desc)",
44+
},
45+
exampleQuery: "/api/search?all=love&page=1&limit=10&sortBy=ViewCount&sortOrder=desc",
46+
},
47+
});
48+
}
49+
50+
let baseQuery = `
51+
FROM Songs s
52+
LEFT JOIN Artists a ON s.ArtistID = a.ArtistID
53+
LEFT JOIN Lyrics l ON s.SongID = l.SongID
54+
WHERE 1=1
55+
`;
56+
57+
const params: any[] = [];
58+
59+
if (all) {
60+
baseQuery += `
61+
AND (
62+
s.SongName LIKE ?
63+
OR s.SongNameSinhala LIKE ?
64+
OR a.ArtistName LIKE ?
65+
OR a.ArtistNameSinhala LIKE ?
66+
OR l.LyricContent LIKE ?
67+
OR l.LyricContentSinhala LIKE ?
68+
)
69+
`;
70+
const likeAll = `%${all}%`;
71+
params.push(likeAll, likeAll, likeAll, likeAll, likeAll, likeAll);
72+
}
73+
74+
if (artist) {
75+
baseQuery += `
76+
AND (a.ArtistName LIKE ? OR a.ArtistNameSinhala LIKE ?)
77+
`;
78+
const likeArtist = `%${artist}%`;
79+
params.push(likeArtist, likeArtist);
80+
}
81+
82+
if (title) {
83+
baseQuery += `
84+
AND (s.SongName LIKE ? OR s.SongNameSinhala LIKE ?)
85+
`;
86+
const likeTitle = `%${title}%`;
87+
params.push(likeTitle, likeTitle);
88+
}
89+
90+
if (lyrics) {
91+
baseQuery += `
92+
AND (l.LyricContent LIKE ? OR l.LyricContentSinhala LIKE ?)
93+
`;
94+
const likeLyrics = `%${lyrics}%`;
95+
params.push(likeLyrics, likeLyrics);
96+
}
97+
98+
const countQuery = `SELECT COUNT(*) as total ${baseQuery}`;
99+
const totalResult = await c.env.sinso_api_db
100+
.prepare(countQuery)
101+
.bind(...params)
102+
.first();
103+
const total = totalResult?.total || 0;
104+
const totalPages = Math.ceil(total / limit);
105+
106+
const dataQuery = `
107+
SELECT
108+
s.SongID,
109+
s.SongName,
110+
s.SongNameSinhala,
111+
s.Duration,
112+
s.ReleaseYear,
113+
s.Composer,
114+
s.Lyricist,
115+
s.ViewCount,
116+
a.ArtistID,
117+
a.ArtistName,
118+
a.ArtistNameSinhala,
119+
l.LyricID,
120+
l.LyricContent,
121+
l.LyricContentSinhala
122+
${baseQuery}
123+
ORDER BY s.${sortBy} ${sortOrder}
124+
LIMIT ? OFFSET ?
125+
`;
126+
127+
const offset = (page - 1) * limit;
128+
const results = await c.env.sinso_api_db
129+
.prepare(dataQuery)
130+
.bind(...params, limit, offset)
131+
.all();
132+
133+
return c.json({
134+
success: true,
135+
data: results.results,
136+
pagination: {
137+
page,
138+
limit,
139+
total,
140+
totalPages,
141+
},
142+
});
143+
} catch (error) {
144+
console.error("Error searching songs:", error);
145+
return c.json({ success: false, error: "Internal Server Error" }, 500);
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)