feat: add search endpoint
Some checks failed
Deploy eolas-api / deploy (push) Failing after 26s

This commit is contained in:
Thomas Bishop 2025-12-11 19:14:45 +00:00
parent 588f60391a
commit 9d92a20872
7 changed files with 135 additions and 76 deletions

View file

@ -0,0 +1,19 @@
export default class SearchController {
searchService
constructor(searchService) {
this.searchService = searchService
}
search = (req, res) => {
const query = req.params.query
const results = this.searchService.search(query)
if (!results) {
return res.status(404).json({
message: `No matches for query ${query}`,
})
}
return res.json(results)
}
}

View file

@ -1,9 +1,11 @@
import express from "express"
import entries from "./routes/entries.js"
import tags from "./routes/tags.js"
import search from "./routes/search.js"
import cors from "cors"
import { validateApiKey } from "./middlewear/auth.js"
import morgan from "morgan"
const app = express()
const port = process.env.PORT || 4000
@ -14,12 +16,13 @@ app.use(express.json())
app.use("/", validateApiKey)
app.use("/entries", entries)
app.use("/tags", tags)
app.use("/search", search)
app.listen(port, () => {
console.info(`TB-INFO eolas-api running on NodeJS ${process.version}`)
console.info(`TB-INFO eolas-api server running at http://localhost:${port}`)
console.info(`TB-INFO eolas-api running on NodeJS ${process.version}`)
console.info(`TB-INFO eolas-api server running at http://localhost:${port}`)
})
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" })
res.status(200).json({ status: "ok" })
})

12
src/routes/search.js Normal file
View file

@ -0,0 +1,12 @@
import express from "express"
import SearchService from "../services/SearchService.js"
import SearchController from "../controllers/SearchController.js"
import database from "../db/connection.js"
const router = express.Router()
const searchService = new SearchService(database)
const searchController = new SearchController(searchService)
router.get("/:query", searchController.search)
export default router

View file

@ -1,68 +1,73 @@
import { GET_ALL_ENTRIES, GET_ENTRY, GET_ENTRIES_FOR_TAG, GET_BACKLINKS_FOR_ENTRY, GET_OUTLINKS_FOR_ENTRY } from "../sql/entries.js"
import {
GET_ALL_ENTRIES,
GET_ENTRY,
GET_ENTRIES_FOR_TAG,
GET_BACKLINKS_FOR_ENTRY,
GET_OUTLINKS_FOR_ENTRY,
} from "../sql/entries.js"
export default class EntriesService {
database
database
constructor(database) {
this.database = database
}
constructor(database) {
this.database = database
}
getEntry = (title) => {
return this.database.prepare(GET_ENTRY).get(title)
}
getEntry = (title) => {
return this.database.prepare(GET_ENTRY).get(title)
}
getAllEntries = (sort, limit) => {
const entries = this.database.prepare(GET_ALL_ENTRIES).all()
getAllEntries = (sort, limit) => {
const entries = this.database.prepare(GET_ALL_ENTRIES).all()
const sorted =
sort === "date" ? this._sortByDate(entries) : this._sortByTitle(entries, "title")
const sorted =
sort === "date" ? this._sortByDate(entries) : this._sortByTitle(entries, "title")
const sliced = sorted.slice(0, Number(limit) || -1)
const sliced = sorted.slice(0, Number(limit) || -1)
return {
count: sliced.length,
data: sliced,
}
}
return {
count: sliced.length,
data: sliced,
}
}
getEntriesForTag = (tag, sort) => {
const entries = this.database.prepare(GET_ENTRIES_FOR_TAG).all(tag)
return {
count: entries.length,
data:
sort === "date"
? this._sortByDate(entries)
: this._sortByTitle(entries, "entry_title"),
}
}
getEntriesForTag = (tag, sort) => {
const entries = this.database.prepare(GET_ENTRIES_FOR_TAG).all(tag)
return {
count: entries.length,
data:
sort === "date"
? this._sortByDate(entries)
: this._sortByTitle(entries, "entry_title"),
}
}
getBacklinksForEntry = (title) => {
const backlinks = this.database.prepare(GET_BACKLINKS_FOR_ENTRY).all(title)
const sorted = this._sortByTitle(backlinks, "source_entry_title")
const list = sorted.flatMap((i) => i.source_entry_title)
return {
count: backlinks.length,
data: list
}
}
getBacklinksForEntry = (title) => {
const backlinks = this.database.prepare(GET_BACKLINKS_FOR_ENTRY).all(title)
const sorted = this._sortByTitle(backlinks, "source_entry_title")
const list = sorted.flatMap((i) => i.source_entry_title)
return {
count: backlinks.length,
data: list,
}
}
getOutlinksForEntry = (title) => {
const outlinks = this.database.prepare(GET_OUTLINKS_FOR_ENTRY).all(title)
const sorted = this._sortByTitle(outlinks, "target_entry_title")
const list = sorted.flatMap((i) => i.target_entry_title)
return {
count: outlinks.length,
data: list
}
getOutlinksForEntry = (title) => {
const outlinks = this.database.prepare(GET_OUTLINKS_FOR_ENTRY).all(title)
const sorted = this._sortByTitle(outlinks, "target_entry_title")
const list = sorted.flatMap((i) => i.target_entry_title)
return {
count: outlinks.length,
data: list,
}
}
}
_sortByTitle = (entries, fieldName) => {
return entries.sort((a, b) => a[fieldName].localeCompare(b[fieldName]))
}
_sortByTitle = (entries, fieldName) => {
return entries.sort((a, b) => a[fieldName].localeCompare(b[fieldName]))
}
_sortByDate = (entries, fieldName = "last_modified") => {
const sorted = entries.sort((a, b) => new Date(b[fieldName]) - new Date(a[fieldName]))
return sorted
}
_sortByDate = (entries, fieldName = "last_modified") => {
const sorted = entries.sort((a, b) => new Date(b[fieldName]) - new Date(a[fieldName]))
return sorted
}
}

View file

@ -0,0 +1,17 @@
import { SEARCH } from "../sql/search.js"
export default class SearchService {
database
constructor(database) {
this.database = database
}
search = (query) => {
const results = this.database.prepare(SEARCH).all(query.trim())
return {
count: results.length,
data: results,
}
}
}

View file

@ -1,27 +1,27 @@
import { GET_ALL_TAGS, GET_TAGS_FOR_ENTRY } from "../sql/tags.js"
export default class TagsService {
database
database
constructor(database) {
this.database = database
}
constructor(database) {
this.database = database
}
getAllTags = () => {
const tags = this.database.prepare(GET_ALL_TAGS).all()
const sorted = this._sortTags(tags, "name")
const list = sorted.flatMap((tag) => tag.name)
return { count: tags.length, data: list }
}
getAllTags = () => {
const tags = this.database.prepare(GET_ALL_TAGS).all()
const sorted = this._sortTags(tags, "name")
const list = sorted.flatMap((tag) => tag.name)
return { count: tags.length, data: list }
}
getTagsForEntry = (entryTitle) => {
const tags = this.database.prepare(GET_TAGS_FOR_ENTRY).all(entryTitle)
const sorted = this._sortTags(tags, "tag_name")
const list = sorted.flatMap((tag) => tag.tag_name)
return { count: tags.length, data: list }
}
getTagsForEntry = (entryTitle) => {
const tags = this.database.prepare(GET_TAGS_FOR_ENTRY).all(entryTitle)
const sorted = this._sortTags(tags, "tag_name")
const list = sorted.flatMap((tag) => tag.tag_name)
return { count: tags.length, data: list }
}
_sortTags = (tags, fieldName) => {
return tags.sort((a, b) => a[fieldName].localeCompare(b[fieldName]))
}
_sortTags = (tags, fieldName) => {
return tags.sort((a, b) => a[fieldName].localeCompare(b[fieldName]))
}
}

3
src/sql/search.js Normal file
View file

@ -0,0 +1,3 @@
const SEARCH = `SELECT title as entry, snippet(entries_fts, 1, '<mark>', '</mark>', '...', 24) as excerpt FROM entries_fts WHERE entries_fts MATCH ? ORDER BY rank LIMIT 25`
export { SEARCH }