Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db1f92cb66 | |||
| 2dadba0cbe | |||
| aa9dcd7979 | |||
| 9d92a20872 | |||
| 588f60391a | |||
| 196eb6988b | |||
| 9c34d5e672 | |||
| 9869ad6721 | |||
| 0db90ca438 | |||
| 31105abd95 | |||
| 230bf67f6c | |||
| 05bf74aca5 | |||
| e123b9e1ec | |||
| 88c3add4ad | |||
| 392535ecdf | |||
| 0ad7105522 | |||
| aee7641ffa | |||
| b7ac09d51b | |||
| 7037b8b501 | |||
| dcce96e67a | |||
| dca61168af | |||
| c79d3e9994 |
15 changed files with 340 additions and 169 deletions
|
|
@ -10,57 +10,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Determine version bump and update package.json
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
||||||
echo "Previous version: $latest_tag"
|
|
||||||
|
|
||||||
commit_msg=$(git log -1 --pretty=%B)
|
|
||||||
|
|
||||||
version=${latest_tag#v}
|
|
||||||
IFS='.' read -r major minor patch <<< "$version"
|
|
||||||
|
|
||||||
if echo "$commit_msg" | grep -qE "^major(\(.*\))?:|BREAKING CHANGE:"; then
|
|
||||||
major=$((major + 1))
|
|
||||||
minor=0
|
|
||||||
patch=0
|
|
||||||
elif echo "$commit_msg" | grep -qE "^feat(\(.*\))?:"; then
|
|
||||||
minor=$((minor + 1))
|
|
||||||
patch=0
|
|
||||||
elif echo "$commit_msg" | grep -qE "^fix(\(.*\))?:"; then
|
|
||||||
patch=$((patch + 1))
|
|
||||||
else
|
|
||||||
echo "No version bump needed"
|
|
||||||
echo "new_tag=" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
new_tag="v${major}.${minor}.${patch}"
|
|
||||||
new_version="${major}.${minor}.${patch}"
|
|
||||||
echo "New version: $new_tag"
|
|
||||||
echo "new_tag=$new_tag" >> $GITHUB_OUTPUT
|
|
||||||
echo "new_version=$new_version" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
echo "$commit_msg" > /tmp/commit_msg.txt
|
|
||||||
|
|
||||||
sed -i "s/\"version\": \".*\"/\"version\": \"$new_version\"/" package.json
|
|
||||||
|
|
||||||
- name: Commit version change
|
|
||||||
if: steps.version.outputs.new_tag != ''
|
|
||||||
run: |
|
|
||||||
git config user.name "forgejo-actions[bot]"
|
|
||||||
git config user.email "forgejo-actions[bot]@noreply"
|
|
||||||
git add package.json
|
|
||||||
git commit --amend --no-edit
|
|
||||||
git push -f origin main
|
|
||||||
|
|
||||||
- name: Create and push tag
|
|
||||||
if: steps.version.outputs.new_tag != ''
|
|
||||||
run: |
|
|
||||||
git tag ${{ steps.version.outputs.new_tag }}
|
|
||||||
git push origin ${{ steps.version.outputs.new_tag }}
|
|
||||||
|
|
||||||
- name: Deploy to VPS
|
- name: Deploy to VPS
|
||||||
run: |
|
run: |
|
||||||
echo "⚡ INFO Installing rsync"
|
echo "⚡ INFO Installing rsync"
|
||||||
|
|
@ -74,13 +23,28 @@ jobs:
|
||||||
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{vars.VPS_DEPLOY_USER}} sudo /usr/bin/systemctl stop eolas-api.service
|
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{vars.VPS_DEPLOY_USER}} sudo /usr/bin/systemctl stop eolas-api.service
|
||||||
|
|
||||||
echo "⚡ INFO Copy updated sourcefile files via rsync"
|
echo "⚡ INFO Copy updated sourcefile files via rsync"
|
||||||
rsync -avz --delete --inplace --exclude='.env' -e "ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no" ./ ${{ vars.VPS_DEPLOY_USER }}:/var/www/eolas-api/
|
rsync -avz --delete --inplace --exclude='.env' --exclude=".git" -e "ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no" ./ ${{ vars.VPS_DEPLOY_USER }}:/var/www/eolas-api/
|
||||||
|
|
||||||
echo "⚡ INFO Run npm install on VPS"
|
echo "⚡ INFO Run npm install on VPS"
|
||||||
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{vars.VPS_DEPLOY_USER}} cd /var/www/eolas-api && npm install --omit=dev
|
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{vars.VPS_DEPLOY_USER}} "bash -l -c 'source ~/.nvm/nvm.sh && cd /var/www/eolas-api && npm install --omit=dev'"
|
||||||
|
|
||||||
echo "⚡ INFO Restarting service"
|
echo "⚡ INFO Restarting service"
|
||||||
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{vars.VPS_DEPLOY_USER}} sudo /usr/bin/systemctl daemon-reload
|
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{vars.VPS_DEPLOY_USER}} "bash -l -c 'sudo /usr/bin/systemctl daemon-reload && sudo /usr/bin/systemctl start eolas-api.service'"
|
||||||
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{vars.VPS_DEPLOY_USER}} sudo /usr/bin/systemctl start eolas-api.service
|
|
||||||
|
|
||||||
rm /tmp/ssh_key
|
rm /tmp/ssh_key
|
||||||
|
|
||||||
|
- name: Notify success
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
curl -u thomas:${{ secrets.NTFY_PASSWORD }} \
|
||||||
|
-H "Tags: Forgejo Runner" \
|
||||||
|
-d "🟩 eolas-api successfully deployed" \
|
||||||
|
https://ntfy.systemsobscure.net/eolas
|
||||||
|
|
||||||
|
- name: Notify failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
curl -u thomas:${{ secrets.NTFY_PASSWORD }} \
|
||||||
|
-H "Tags: Forgejo Runner" \
|
||||||
|
-d "🟥 An error occurred deploying eolas-api. See Forgejo Action logs." \
|
||||||
|
https://ntfy.systemsobscure.net/eolas
|
||||||
|
|
|
||||||
101
README.md
101
README.md
|
|
@ -1,47 +1,80 @@
|
||||||
|
# eolas-api
|
||||||
|
|
||||||
|
API written in NodeJS that queries my Zettelkasten, Eolas.
|
||||||
|
|
||||||
|
It is a constituent part of my knowledge management system comprising [eolas](https://forgejo.systemsobscure.net/thomasabishop/eolas),
|
||||||
|
[eolas-db](https://forgejo.systemsobscure.net/thomasabishop/eolas-db), and [eolas-app](https://forgejo.systemsobscure.net/thomasabishop/eolas-app).
|
||||||
|
|
||||||
|
The application reads from an SQLite database managed via [eolas-db](https://forgejo.systemsobscure.net/thomasabishop/eolas-db).
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the local server at `http://localhost:4000`. It will look for
|
||||||
|
stub database at `/data/eolas.db`.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Deployment is automated via Forgejo action. See `.forgejo` directory. Deployment actions are always executed by the `deploy` user on the VPS.
|
The API is deployed to my remote VPS, residing at `/var/www/eolas-api`. The
|
||||||
|
database that it reads from is located at `/data/sqlite/eolas/eolas.db`.
|
||||||
|
|
||||||
**The application resides at `/var/www/eolas-api` on the VPS. The SQLite database
|
Deployment is automated via [Forgejo action](https://forgejo.systemsobscure.net/thomasabishop/eolas-api/src/branch/main/.forgejo/workflows/deploy.yaml). Deployment actions are always executed by the `deploy` user on the VPS.
|
||||||
file that the API reads from is stored at `/data/sqlite/eolas`.**
|
|
||||||
|
|
||||||
### Automated pipeline
|
The action script transfers the source files to the VPS and installs necessary
|
||||||
|
packages. It also restarts the `eolas-api` systemd service on the VPS.
|
||||||
On pushes to `main`:
|
|
||||||
|
|
||||||
- Bump version depending on commit keyword, create Git tag (if necessary) and
|
|
||||||
automatically update [package.json](./package.json):
|
|
||||||
|
|
||||||
- Install `rsync` on the `ubuntu-latest` runner
|
|
||||||
|
|
||||||
- `rysnc` over SSH to copy changed files from this repository to VPS deployment directory, excluding`.env`.
|
|
||||||
|
|
||||||
- Restart running instance of `eolas-api` on VPS.
|
|
||||||
|
|
||||||
### Commit keywords
|
|
||||||
|
|
||||||
In accordance with SemVer:
|
|
||||||
|
|
||||||
- Major: `major: <commit description>`,
|
|
||||||
- Minor: `feat: <commit description>`,
|
|
||||||
- Patch: `fix: <commit description>`
|
|
||||||
|
|
||||||
Ignored keywords: `chore`, `test`, `refactor`, and anything else.
|
|
||||||
|
|
||||||
### `systemd` service
|
### `systemd` service
|
||||||
|
|
||||||
On the VPS, `eolas-api` runs as a `systemd` service. This service is restarted as part of
|
On the VPS, `eolas-api` runs as a `systemd` service. See the
|
||||||
the deployment. See copy of this file at
|
[unit file](./systemd/eolas-api.service) for details.
|
||||||
[systemd/eolas-api.service](./systemd/eolas-api.service).
|
|
||||||
|
|
||||||
#### Logs
|
|
||||||
|
|
||||||
```sh
|
|
||||||
journalctl -u eolas-api.service --no-pager
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /search/<search_term>
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"entry": "Test_values_in_Bash",
|
||||||
|
"excerpt": "# <mark>Test</mark> values in Bash\n\n`<mark>test</mark>` is a built-in command that is used to compare values or determine whether\nsomething is the case.\n\nWe..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entry": "Testing_Python_code",
|
||||||
|
"excerpt": "...for a module called `lorem`, it will detzect the unit <mark>test</mark>\n files `lorem_<mark>test</mark>.py` and `<mark>test</mark>_lorem.py`.\n- In order to detect tests..."
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
#### Get broken links
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /diagnostics/broken-links
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"source_entry_title": "Reducing_fractions",
|
||||||
|
"broken_link_title": "Equivalent%20fractions"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Entries
|
### Entries
|
||||||
|
|
||||||
#### Get all entries
|
#### Get all entries
|
||||||
|
|
|
||||||
62
package-lock.json
generated
62
package-lock.json
generated
|
|
@ -1,16 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "eolas-api",
|
"name": "eolas-api",
|
||||||
"version": "0.2.1",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "eolas-api",
|
"name": "eolas-api",
|
||||||
"version": "0.2.1",
|
"version": "0.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.2"
|
"express": "^4.21.2",
|
||||||
|
"morgan": "^1.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^9.20.0",
|
"eslint": "^9.20.0",
|
||||||
|
|
@ -392,6 +393,24 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/basic-auth": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/basic-auth/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
|
|
@ -1480,6 +1499,34 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/morgan": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-auth": "~2.0.1",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"on-finished": "~2.3.0",
|
||||||
|
"on-headers": "~1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/morgan/node_modules/on-finished": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
|
@ -1535,6 +1582,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "eolas-api",
|
"name": "eolas-api",
|
||||||
"version": "0.2.2",
|
"version": "0.0.0",
|
||||||
"description": "API for querying eolas-db, my Zettelkasten database",
|
"description": "API for querying eolas-db, my Zettelkasten database",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"author": "Thomas Bishop",
|
"author": "Thomas Bishop",
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.2"
|
"express": "^4.21.2",
|
||||||
|
"morgan": "^1.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^9.20.0",
|
"eslint": "^9.20.0",
|
||||||
|
|
|
||||||
18
src/controllers/DiagnosticsController.js
Normal file
18
src/controllers/DiagnosticsController.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export default class DiagnosticsController {
|
||||||
|
diagnosticsService
|
||||||
|
|
||||||
|
constructor(diagnosticsService) {
|
||||||
|
this.diagnosticsService = diagnosticsService
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrokenLinks = (req, res) => {
|
||||||
|
const brokenLinks = this.diagnosticsService.getBrokenLinks()
|
||||||
|
if (!brokenLinks) {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: `Broken link list could not be retrieved`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(brokenLinks)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/controllers/SearchController.js
Normal file
19
src/controllers/SearchController.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/index.js
16
src/index.js
|
|
@ -1,18 +1,30 @@
|
||||||
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 diagnostics from "./routes/diagnostics.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"
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const port = process.env.PORT || 3000
|
const port = process.env.PORT || 4000
|
||||||
|
|
||||||
|
app.use(morgan("short"))
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(express.json())
|
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.use("/diagnostics", diagnostics)
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.info(`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" })
|
||||||
})
|
})
|
||||||
|
|
|
||||||
11
src/routes/diagnostics.js
Normal file
11
src/routes/diagnostics.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import express from "express"
|
||||||
|
import DiagnosticsService from "../services/DiagnosticsService.js"
|
||||||
|
import DiagnosticsController from "../controllers/DiagnosticsController.js"
|
||||||
|
import database from "../db/connection.js"
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
const diagnosticsService = new DiagnosticsService(database)
|
||||||
|
const diagnosticsContoller = new DiagnosticsController(diagnosticsService)
|
||||||
|
|
||||||
|
router.get("/broken-links", diagnosticsContoller.getBrokenLinks)
|
||||||
|
export default router
|
||||||
12
src/routes/search.js
Normal file
12
src/routes/search.js
Normal 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
|
||||||
17
src/services/DiagnosticsService.js
Normal file
17
src/services/DiagnosticsService.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { GET_BROKEN_LINKS } from "../sql/diagnostics.js"
|
||||||
|
|
||||||
|
export default class DiagnosticsService {
|
||||||
|
database
|
||||||
|
|
||||||
|
constructor(database) {
|
||||||
|
this.database = database
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrokenLinks = () => {
|
||||||
|
const brokenLinks = this.database.prepare(GET_BROKEN_LINKS).all()
|
||||||
|
return {
|
||||||
|
count: brokenLinks.length,
|
||||||
|
data: brokenLinks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
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
|
||||||
|
|
@ -42,7 +48,7 @@ export default class EntriesService {
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,9 +58,8 @@ export default class EntriesService {
|
||||||
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) => {
|
_sortByTitle = (entries, fieldName) => {
|
||||||
|
|
|
||||||
17
src/services/SearchService.js
Normal file
17
src/services/SearchService.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/sql/diagnostics.js
Normal file
3
src/sql/diagnostics.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
const GET_BROKEN_LINKS = `SELECT * from broken_links`
|
||||||
|
|
||||||
|
export { GET_BROKEN_LINKS }
|
||||||
3
src/sql/search.js
Normal file
3
src/sql/search.js
Normal 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 }
|
||||||
Loading…
Add table
Reference in a new issue