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 express from "express"
import entries from "./routes/entries.js" import entries from "./routes/entries.js"
import tags from "./routes/tags.js" import tags from "./routes/tags.js"
import search from "./routes/search.js"
import cors from "cors" import cors from "cors"
import { validateApiKey } from "./middlewear/auth.js" import { validateApiKey } from "./middlewear/auth.js"
import morgan from "morgan" import morgan from "morgan"
const app = express() const app = express()
const port = process.env.PORT || 4000 const port = process.env.PORT || 4000
@ -14,12 +16,13 @@ app.use(express.json())
app.use("/", validateApiKey) app.use("/", validateApiKey)
app.use("/entries", entries) app.use("/entries", entries)
app.use("/tags", tags) app.use("/tags", tags)
app.use("/search", search)
app.listen(port, () => { app.listen(port, () => {
console.info(`TB-INFO eolas-api running on NodeJS ${process.version}`) 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 server running at http://localhost:${port}`)
}) })
app.get("/health", (req, res) => { 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 { export default class EntriesService {
database database
constructor(database) { constructor(database) {
this.database = database this.database = database
} }
getEntry = (title) => { getEntry = (title) => {
return this.database.prepare(GET_ENTRY).get(title) return this.database.prepare(GET_ENTRY).get(title)
} }
getAllEntries = (sort, limit) => { getAllEntries = (sort, limit) => {
const entries = this.database.prepare(GET_ALL_ENTRIES).all() const entries = this.database.prepare(GET_ALL_ENTRIES).all()
const sorted = const sorted =
sort === "date" ? this._sortByDate(entries) : this._sortByTitle(entries, "title") 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 { return {
count: sliced.length, count: sliced.length,
data: sliced, data: sliced,
} }
} }
getEntriesForTag = (tag, sort) => { getEntriesForTag = (tag, sort) => {
const entries = this.database.prepare(GET_ENTRIES_FOR_TAG).all(tag) const entries = this.database.prepare(GET_ENTRIES_FOR_TAG).all(tag)
return { return {
count: entries.length, count: entries.length,
data: data:
sort === "date" sort === "date"
? this._sortByDate(entries) ? this._sortByDate(entries)
: this._sortByTitle(entries, "entry_title"), : this._sortByTitle(entries, "entry_title"),
} }
} }
getBacklinksForEntry = (title) => { getBacklinksForEntry = (title) => {
const backlinks = this.database.prepare(GET_BACKLINKS_FOR_ENTRY).all(title) const backlinks = this.database.prepare(GET_BACKLINKS_FOR_ENTRY).all(title)
const sorted = this._sortByTitle(backlinks, "source_entry_title") const sorted = this._sortByTitle(backlinks, "source_entry_title")
const list = sorted.flatMap((i) => i.source_entry_title) const list = sorted.flatMap((i) => i.source_entry_title)
return { return {
count: backlinks.length, count: backlinks.length,
data: list data: list,
} }
} }
getOutlinksForEntry = (title) => { getOutlinksForEntry = (title) => {
const outlinks = this.database.prepare(GET_OUTLINKS_FOR_ENTRY).all(title) const outlinks = this.database.prepare(GET_OUTLINKS_FOR_ENTRY).all(title)
const sorted = this._sortByTitle(outlinks, "target_entry_title") const sorted = this._sortByTitle(outlinks, "target_entry_title")
const list = sorted.flatMap((i) => i.target_entry_title) const list = sorted.flatMap((i) => i.target_entry_title)
return { return {
count: outlinks.length, count: outlinks.length,
data: list data: list,
} }
}
} _sortByTitle = (entries, fieldName) => {
return entries.sort((a, b) => a[fieldName].localeCompare(b[fieldName]))
}
_sortByTitle = (entries, fieldName) => { _sortByDate = (entries, fieldName = "last_modified") => {
return entries.sort((a, b) => a[fieldName].localeCompare(b[fieldName])) 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" import { GET_ALL_TAGS, GET_TAGS_FOR_ENTRY } from "../sql/tags.js"
export default class TagsService { export default class TagsService {
database database
constructor(database) { constructor(database) {
this.database = database this.database = database
} }
getAllTags = () => { getAllTags = () => {
const tags = this.database.prepare(GET_ALL_TAGS).all() const tags = this.database.prepare(GET_ALL_TAGS).all()
const sorted = this._sortTags(tags, "name") const sorted = this._sortTags(tags, "name")
const list = sorted.flatMap((tag) => tag.name) const list = sorted.flatMap((tag) => tag.name)
return { count: tags.length, data: list } return { count: tags.length, data: list }
} }
getTagsForEntry = (entryTitle) => { getTagsForEntry = (entryTitle) => {
const tags = this.database.prepare(GET_TAGS_FOR_ENTRY).all(entryTitle) const tags = this.database.prepare(GET_TAGS_FOR_ENTRY).all(entryTitle)
const sorted = this._sortTags(tags, "tag_name") const sorted = this._sortTags(tags, "tag_name")
const list = sorted.flatMap((tag) => tag.tag_name) const list = sorted.flatMap((tag) => tag.tag_name)
return { count: tags.length, data: list } return { count: tags.length, data: list }
} }
_sortTags = (tags, fieldName) => { _sortTags = (tags, fieldName) => {
return tags.sort((a, b) => a[fieldName].localeCompare(b[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 }