Compare commits

..

59 commits
v0.1.0 ... main

Author SHA1 Message Date
db1f92cb66 feat: add diagnostics route and broken link method
All checks were successful
Deploy eolas-api / deploy (push) Successful in 46s
2025-12-22 17:16:42 +00:00
2dadba0cbe infra: exclude .git rsync deploy
All checks were successful
Deploy eolas-api / deploy (push) Successful in 34s
2025-12-12 18:08:46 +00:00
aa9dcd7979 chore: add notification to deploy script
Some checks failed
Deploy eolas-api / deploy (push) Failing after 35s
2025-12-12 17:46:21 +00:00
9d92a20872 feat: add search endpoint
Some checks failed
Deploy eolas-api / deploy (push) Failing after 26s
2025-12-11 19:14:45 +00:00
588f60391a chore: update readme
All checks were successful
Deploy eolas-api / deploy (push) Successful in 1m28s
2025-12-08 17:42:14 +00:00
196eb6988b feat: add morgan for logging
All checks were successful
Deploy eolas-api / deploy (push) Successful in 29s
2025-10-19 12:31:11 +01:00
9c34d5e672 Merge branch 'main' of https://forgejo.systemsobscure.net/thomasabishop/eolas-api
All checks were successful
Deploy eolas-api / deploy (push) Successful in 29s
2025-10-19 12:11:46 +01:00
9869ad6721 infra: remove git tag subtask and auto version bump 2025-10-19 12:11:00 +01:00
0db90ca438 fix: health endpoint null ref 2025-10-18 15:23:54 +00:00
31105abd95 fix: health endpoint null ref
All checks were successful
Deploy eolas-api / deploy (push) Successful in 47s
2025-10-18 16:23:12 +01:00
230bf67f6c Merge branch 'main' of https://forgejo.systemsobscure.net/thomasabishop/eolas-api
All checks were successful
Deploy eolas-api / deploy (push) Successful in 33s
2025-10-17 19:14:06 +01:00
05bf74aca5 refactor: change default port to 4000 to avoid clashes on server 2025-10-17 19:14:01 +01:00
e123b9e1ec feat: add /health endpoint to check API is up 2025-10-17 17:52:37 +00:00
88c3add4ad feat: add /health endpoint to check API is up
All checks were successful
Deploy eolas-api / deploy (push) Successful in 40s
2025-10-17 18:52:06 +01:00
392535ecdf chore: formatting
All checks were successful
Deploy eolas-api / deploy (push) Successful in 42s
2025-10-17 18:24:33 +01:00
0ad7105522 infra: add NodeJS runtime to initial server log 2025-10-17 18:22:45 +01:00
aee7641ffa infra: debug - source nvm before running Node commands
All checks were successful
Deploy eolas-api / deploy (push) Successful in 38s
2025-10-16 15:04:10 +01:00
b7ac09d51b infra: debug - use NPM binary in npm install command
Some checks failed
Deploy eolas-api / deploy (push) Failing after 30s
2025-10-16 14:57:08 +01:00
7037b8b501 infra: fix ssh syntax
Some checks failed
Deploy eolas-api / deploy (push) Failing after 38s
2025-10-16 14:50:35 +01:00
dcce96e67a Merge branch 'main' of https://forgejo.systemsobscure.net/thomasabishop/eolas-api
Some checks failed
Deploy eolas-api / deploy (push) Failing after 29s
2025-10-16 14:45:20 +01:00
dca61168af infra: use correct && commands with ssh 2025-10-16 14:44:09 +01:00
8d77d813cf fix: use separate ssh commands 2025-10-16 13:40:11 +00:00
c79d3e9994 fix: use separate ssh commands
All checks were successful
Deploy eolas-api / deploy (push) Successful in 43s
2025-10-16 14:39:38 +01:00
86bd3220fc infra: add remaining ssh commands
Some checks failed
Deploy eolas-api / deploy (push) Failing after 36s
2025-10-16 14:37:03 +01:00
c27f342864 infra: remove strict host key checking
All checks were successful
Deploy eolas-api / deploy (push) Successful in 26s
2025-10-16 14:20:14 +01:00
eea16096c3 infra: test SSH command
Some checks failed
Deploy eolas-api / deploy (push) Failing after 26s
2025-10-16 14:17:27 +01:00
cde7d5feb4 infra: add Node env vars to systemd service
All checks were successful
Deploy eolas-api / deploy (push) Successful in 31s
2025-10-16 13:45:48 +01:00
128eb4f79a chore: add logging command to readme 2025-10-15 17:14:08 +01:00
2d7ff15ebe infra: run systemd exec with node not npm
All checks were successful
Deploy eolas-api / deploy (push) Successful in 25s
2025-10-15 17:00:52 +01:00
5ba2c68fd5 infra: add better logs in deploy script 2025-10-15 17:00:07 +01:00
49ea3e2554 infra: add npm install command forgejo action
All checks were successful
Deploy eolas-api / deploy (push) Successful in 29s
2025-10-15 15:53:04 +01:00
af297be06f chore: add separate prod start command 2025-10-15 15:50:04 +01:00
df04b1ba7e fix: systemd unit
- add missing working directory
- use npm binary as exec
2025-10-15 15:49:37 +01:00
0edd977cd4 infra: run rsync inplace to preserve systemd symlink 2025-10-15 15:21:05 +01:00
3f18776685 chore: update deployment details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 24s
2025-10-15 15:01:32 +01:00
3f3e83f968 chore: update README with deploy details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 24s
2025-10-15 14:58:02 +01:00
d5c1763b2a chore: update README with further deployment details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 22s
2025-10-15 14:55:33 +01:00
645be686be chore: update README with further deployment details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 20s
2025-10-15 14:52:25 +01:00
20bda8825a chore: update README with further deployment details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 19s
2025-10-15 14:51:10 +01:00
f18ad382c7 chore: update README with further deployment details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 23s
2025-10-15 14:49:56 +01:00
9d7610df01 infra: add systemd service for service restart
All checks were successful
Deploy eolas-api / deploy (push) Successful in 24s
2025-10-14 17:09:18 +01:00
f5a1e3f1a8 chore: update README with API endpoint docs
All checks were successful
Deploy eolas-api / deploy (push) Successful in 22s
2025-10-13 18:50:38 +01:00
d9463b4a40 infra: try rsync in deploy script with sudo
All checks were successful
Deploy eolas-api / deploy (push) Successful in 22s
2025-10-10 15:24:16 +01:00
73b1df9a10 infra: install rsync on deploy runnter
Some checks failed
Deploy eolas-api / deploy (push) Failing after 12s
2025-10-10 15:21:59 +01:00
55f77f208c infra: use rsync instead of ssh+scp to copy files at deploy
Some checks failed
Deploy eolas-api / deploy (push) Failing after 14s
2025-10-10 15:16:46 +01:00
80d1247333 chore: delete dummy file
All checks were successful
Deploy eolas-api / deploy (push) Successful in 17s
2025-10-10 14:59:43 +01:00
2e281eae7d chore: remove old script from deploy
All checks were successful
Deploy eolas-api / deploy (push) Successful in 13s
2025-10-09 18:55:57 +01:00
6adfeb3f9a chore: update README deploy details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 14s
2025-10-09 18:53:31 +01:00
548e6a8095 infra: remove Forgejo release from deploy script 2025-10-09 18:51:57 +01:00
f049e4cd36 chore: update README with deploy details
All checks were successful
Deploy eolas-api / deploy (push) Successful in 11s
2025-10-08 19:24:22 +01:00
bac1d38cf8 Merge branch 'main' of https://forgejo.systemsobscure.net/thomasabishop/eolas-api 2025-10-08 19:18:11 +01:00
9f449ba49a fix: syntax in subtask deploy script 2025-10-08 18:17:33 +00:00
d3b02757c3 fix: syntax in subtask deploy script
All checks were successful
Deploy eolas-api / deploy (push) Successful in 20s
2025-10-08 19:17:13 +01:00
bb49bac558 feat: test versioning script without jq dep
Some checks failed
Deploy eolas-api / deploy (push) Failing after 0s
2025-10-08 19:15:32 +01:00
0e73b6a01f Merge branch 'main' of https://forgejo.systemsobscure.net/thomasabishop/eolas-api 2025-10-08 19:14:09 +01:00
47e08f7855 feat: test versioning script again 2025-10-08 18:13:12 +00:00
354ccfacf9 feat: test versioning script again
Some checks failed
Deploy eolas-api / deploy (push) Failing after 17s
2025-10-08 19:12:53 +01:00
38b51e05a2 Merge branch 'main' of https://forgejo.systemsobscure.net/thomasabishop/eolas-api
All checks were successful
Deploy eolas-api / deploy (push) Successful in 10s
2025-10-08 19:10:21 +01:00
c5dd762580 feat: test versioning script 2025-10-08 19:10:05 +01:00
16 changed files with 507 additions and 168 deletions

View file

@ -1,20 +1,3 @@
# name: Deploy eolas-api
# on:
# push:
# branches: [main]
# jobs:
# deploy:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - run: |
# echo "${{ secrets.VPS_DEPLOY_USER_SSH_KEY }}" > /tmp/ssh_key
# chmod 600 /tmp/ssh_key
# ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{ vars.VPS_DEPLOY_USER }} "bash -c 'cd /var/www/eolas-api && rm -rf * .[^.]*'"
# scp -i /tmp/ssh_key -o StrictHostKeyChecking=no -r ./* ${{ vars.VPS_DEPLOY_USER }}:/var/www/eolas-api/
# rm /tmp/ssh_key
#
#
name: Deploy eolas-api
on:
push:
@ -27,72 +10,41 @@ jobs:
with:
fetch-depth: 0
- name: Determine version bump
id: version
- name: Deploy to VPS
run: |
latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Previous version: $latest_tag"
echo "⚡ INFO Installing rsync"
apt-get update && apt-get install -y rsync
commit_msg=$(git log -1 --pretty=%B)
version=${latest_tag#v}
IFS='.' read -r major minor patch <<< "$version"
if echo "$commit_msg" | grep -q "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
- name: Update package.json version
if: steps.version.outputs.new_tag != ''
run: |
# Update version in package.json
sed -i "s/\"version\": \".*\"/\"version\": \"${{ steps.version.outputs.new_version }}\"/" package.json
git config user.name "forgejo-actions[bot]"
git config user.email "forgejo-actions[bot]@noreply"
git add package.json
git commit -m "chore: bump version to ${{ steps.version.outputs.new_tag }}" || true
git push origin main || true
- 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: Create Forgejo Release
if: steps.version.outputs.new_tag != ''
run: |
curl -X POST \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"${{ steps.version.outputs.new_tag }}\",
\"name\": \"${{ steps.version.outputs.new_tag }}\",
\"body\": \"Release ${{ steps.version.outputs.new_tag }}\"
}"
- run: |
echo "⚡ INFO Retrieving SSH key for deploy user"
echo "${{ secrets.VPS_DEPLOY_USER_SSH_KEY }}" > /tmp/ssh_key
chmod 600 /tmp/ssh_key
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{ vars.VPS_DEPLOY_USER }} "bash -c 'cd /var/www/eolas-api && rm -rf * .[^.]*'"
scp -i /tmp/ssh_key -o StrictHostKeyChecking=no -r ./* ${{ vars.VPS_DEPLOY_USER }}:/var/www/eolas-api/
echo "⚡ INFO Stopping service on VPS"
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"
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"
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"
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'"
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

214
README.md
View file

@ -1,9 +1,207 @@
TBC
# eolas-api
| HTTP Method | Path | Parameter | Returns |
| ----------- | --------------- | ------------- | -------------------------------- |
| GET | `entry` | `entry_title` | Body text, tags, title |
| GET | `tag` | `tag_name` | List of entries for supplied tag |
| GET | `refs` | `entry_title` | List of linked entries |
| GET | `full_metadata` | `entry_title` | Tags, links, last modified |
| GET | `graph` | `null` | Full network graph |
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
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`.
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.
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.
### `systemd` service
On the VPS, `eolas-api` runs as a `systemd` service. See the
[unit file](./systemd/eolas-api.service) for details.
## 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
#### Get all entries
Return all entries. Optionally limit by length and/or date.
```
GET /entries?limit=2&sort=date
```
```json
{
"count": 5,
"data": [
{
"title": "SSH",
"last_modified": "2025-07-10 14:26:04"
},
{
"title": "List_largest_files_bash",
"last_modified": "2025-07-07 16:49:12"
}
]
}
```
#### Get specific entry
```
GET /entries/Memory_versus_processor
```
```json
{
"title": "Memory_versus_processor",
"last_modified": "2024-10-18 19:17:01",
"size": 270,
"body": "# Memory versus processor\n\n Would a more powerful processor with average or reduced memory capacity..."
}
```
#### Get backlinks for an entry
Defaults to alphabetic list.
```
GET /entries/backlinks/The_kernel
```
```json
{
"count": 3,
"data": ["Boot_process", "Containerization", "CPU_architecture"]
}
```
#### Get outlinks for an entry
Defaults to alphabetic list.
```
GET /entries/outlinks/The_kernel
```
```json
{
"count": 3,
"data": ["Basic_model_of_the_operating_system", "Processes", "User_Space"]
}
```
#### Get entries associated with a specified tag
Optionally sort chronologically.
```
GET /entries/tag/memory?sort=date
```
```json
{
"count": 3,
"data": [
{
"entry_title": "Memory_addresses"
},
{
"entry_title": "Call_stack"
},
{
"entry_title": "The_memory_hierarchy"
}
]
}
```
### Tags
#### Get all tags
Sorted alphabetically.
```
GET /tags
```
```json
{
"count": 119,
"data": ["algebra", "algorithms", "analogue", "android", "..."]
}
```
#### Get tags for specified entry
```
GET /tags/The_kernel
```
```json
{
"count": 4,
"data": [
"computer-architecture",
"memory",
"operating-systems",
"systems-programming"
]
}
```

58
package-lock.json generated
View file

@ -10,7 +10,8 @@
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.2"
"express": "^4.21.2",
"morgan": "^1.10.1"
},
"devDependencies": {
"eslint": "^9.20.0",
@ -392,6 +393,24 @@
"dev": true,
"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": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -1480,6 +1499,34 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -1535,6 +1582,15 @@
"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": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "eolas-api",
"version": "0.1.0",
"version": "0.0.0",
"description": "API for querying eolas-db, my Zettelkasten database",
"license": "ISC",
"author": "Thomas Bishop",
@ -8,12 +8,14 @@
"imports": {},
"main": "index.js",
"scripts": {
"start": "NODE_OPTIONS='--experimental-sqlite' node --watch --env-file=.env src/index.js",
"dev": "NODE_OPTIONS='--experimental-sqlite' node --watch --env-file=.env src/index.js",
"start": "NODE_OPTIONS='--experimental-sqlite' node --env-file=.env src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.2"
"express": "^4.21.2",
"morgan": "^1.10.1"
},
"devDependencies": {
"eslint": "^9.20.0",

View 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)
}
}

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,18 +1,30 @@
import express from "express"
import entries from "./routes/entries.js"
import tags from "./routes/tags.js"
import search from "./routes/search.js"
import diagnostics from "./routes/diagnostics.js"
import cors from "cors"
import { validateApiKey } from "./middlewear/auth.js"
import morgan from "morgan"
const app = express()
const port = process.env.PORT || 3000
const port = process.env.PORT || 4000
app.use(morgan("short"))
app.use(cors())
app.use(express.json())
app.use("/", validateApiKey)
app.use("/entries", entries)
app.use("/tags", tags)
app.use("/search", search)
app.use("/diagnostics", diagnostics)
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
View 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
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

@ -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,
}
}
}

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/diagnostics.js Normal file
View file

@ -0,0 +1,3 @@
const GET_BROKEN_LINKS = `SELECT * from broken_links`
export { GET_BROKEN_LINKS }

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 }

14
systemd/eolas-api.service Normal file
View file

@ -0,0 +1,14 @@
[Unit]
Description=eolas-api
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/eolas-api
Environment="NODE_OPTIONS=--experimental-sqlite"
ExecStart=/home/deploy/.nvm/versions/node/v24.10.0/bin/node --env-file=.env /var/www/eolas-api/src/index.js
Restart=on-failure
[Install]
WantedBy=multi-user.target