diff --git a/src/controllers/SearchController.js b/src/controllers/SearchController.js new file mode 100644 index 0000000..70cff71 --- /dev/null +++ b/src/controllers/SearchController.js @@ -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) + } +} diff --git a/src/index.js b/src/index.js index d6ec63a..1f12c31 100644 --- a/src/index.js +++ b/src/index.js @@ -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" }) }) diff --git a/src/routes/search.js b/src/routes/search.js new file mode 100644 index 0000000..f420de3 --- /dev/null +++ b/src/routes/search.js @@ -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 diff --git a/src/services/EntriesService.js b/src/services/EntriesService.js index 3102fbe..5d8c873 100644 --- a/src/services/EntriesService.js +++ b/src/services/EntriesService.js @@ -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 + } } diff --git a/src/services/SearchService.js b/src/services/SearchService.js new file mode 100644 index 0000000..fee132c --- /dev/null +++ b/src/services/SearchService.js @@ -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, + } + } +} diff --git a/src/services/TagsService.js b/src/services/TagsService.js index c0a8c1b..02c1474 100644 --- a/src/services/TagsService.js +++ b/src/services/TagsService.js @@ -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])) + } } diff --git a/src/sql/search.js b/src/sql/search.js new file mode 100644 index 0000000..623f10e --- /dev/null +++ b/src/sql/search.js @@ -0,0 +1,3 @@ +const SEARCH = `SELECT title as entry, snippet(entries_fts, 1, '', '', '...', 24) as excerpt FROM entries_fts WHERE entries_fts MATCH ? ORDER BY rank LIMIT 25` + +export { SEARCH }