separate fe and be, deploy on push to master

This commit is contained in:
Will Freeman
2026-02-02 11:41:29 -07:00
parent ea218998b3
commit db49ac2a98
18 changed files with 525 additions and 420 deletions

View File

@@ -0,0 +1,38 @@
name: DeFlock API Deploy
on:
push:
branches: [master]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun install
working-directory: ./api
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
- name: Sync files
run: |
rsync -az --delete \
--exclude node_modules \
./api/ ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/home/nullplate/deflock/api
- name: Restart services
run: |
ssh ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} "
sudo systemctl restart deflock-api
"

31
api/README.md Normal file
View File

@@ -0,0 +1,31 @@
# DeFlock API
A Fastify-based API service for DeFlock handling non-OSM related backend logic.
## Endpoints
- `/geocode?query=...` — Geocode a location
- `/sponsors/github?username=...` — Get GitHub sponsors
- `/healthcheck` — Health check
## Development
### Prerequisites
- [Bun](https://bun.sh/) installed
### Install dependencies
```sh
bun install
```
### Run locally
```sh
bun server.ts
```
## Deployment
Deployed via GitHub Actions on push to `master`.
## Environment Variables
Create a `.env` file in this directory with:
- `GITHUB_TOKEN` — Required for GitHub Sponsors endpoint

177
api/bun.lock Normal file
View File

@@ -0,0 +1,177 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"@fastify/cors": "^10.0.0",
"@sinclair/typebox": "^0.34.48",
"cache-manager": "^7.2.8",
"cache-manager-fs-hash": "^3.0.0",
"fastify": "^5.7.2",
"pino-pretty": "^13.1.3",
},
"devDependencies": {
"@types/cache-manager-fs-hash": "^0.0.5",
"@types/node": "^25.2.0",
},
},
},
"packages": {
"@cacheable/utils": ["@cacheable/utils@2.3.3", "", { "dependencies": { "hashery": "^1.3.0", "keyv": "^5.5.5" } }, "sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A=="],
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
"@fastify/cors": ["@fastify/cors@10.1.0", "", { "dependencies": { "fastify-plugin": "^5.0.0", "mnemonist": "0.40.0" } }, "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ=="],
"@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
"@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
"@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
"@types/cache-manager": ["@types/cache-manager@3.4.3", "", {}, "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw=="],
"@types/cache-manager-fs-hash": ["@types/cache-manager-fs-hash@0.0.5", "", { "dependencies": { "@types/cache-manager": "<4" } }, "sha512-mSqk9YisfK/NkB/R5SzGeuSIVtwHhM5m6MLB0VrrFteTphKiQ2Fyz88IRtiX+SYEX6Nw2H3kB9qtpfnVSE/mSQ=="],
"@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="],
"cache-manager": ["cache-manager@7.2.8", "", { "dependencies": { "@cacheable/utils": "^2.3.3", "keyv": "^5.5.5" } }, "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ=="],
"cache-manager-fs-hash": ["cache-manager-fs-hash@3.0.0", "", { "dependencies": { "lockfile": "^1.0.4" } }, "sha512-uFl2EOuIdz5bLIjcRbR5cAxt9JdKQ5jQ6r7w5LXs1V+ls9I232nLVfaEHwK5h+isbV7j2z7ceaMe8lvr17BMVA=="],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stringify": ["fast-json-stringify@6.2.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw=="],
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fastify": ["fastify@5.7.2", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-dBJolW+hm6N/yJVf6J5E1BxOBNkuXNl405nrfeR8SpvGWG3aCC2XDHyiFBdow8Win1kj7sjawQc257JlYY6M/A=="],
"fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"find-my-way": ["find-my-way@9.4.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w=="],
"hashery": ["hashery@1.4.0", "", { "dependencies": { "hookified": "^1.14.0" } }, "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ=="],
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
"hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="],
"ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
"lockfile": ["lockfile@1.0.4", "", { "dependencies": { "signal-exit": "^3.0.2" } }, "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"mnemonist": ["mnemonist@0.40.0", "", { "dependencies": { "obliterator": "^2.0.4" } }, "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg=="],
"obliterator": ["obliterator@2.0.5", "", {}, "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="],
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
"pino-pretty": ["pino-pretty@13.1.3", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="],
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="],
"toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
}
}

21
api/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "deflock-api",
"version": "1.0.0",
"main": "server.ts",
"type": "module",
"scripts": {
"start": "bun server.ts"
},
"dependencies": {
"@fastify/cors": "^10.0.0",
"@sinclair/typebox": "^0.34.48",
"cache-manager": "^7.2.8",
"cache-manager-fs-hash": "^3.0.0",
"fastify": "^5.7.2",
"pino-pretty": "^13.1.3"
},
"devDependencies": {
"@types/cache-manager-fs-hash": "^0.0.5",
"@types/node": "^25.2.0"
}
}

127
api/server.ts Normal file
View File

@@ -0,0 +1,127 @@
import Fastify, { FastifyInstance, FastifyError } from 'fastify';
import cors from '@fastify/cors';
import { NominatimClient, NominatimResultSchema } from './services/NominatimClient';
import { GithubClient, SponsorsResponseSchema } from './services/GithubClient';
const start = async () => {
const server: FastifyInstance = Fastify({
logger: {
level: 'error',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
trustProxy: true,
});
// Global error handler
server.setErrorHandler((error: FastifyError, request, reply) => {
server.log.error({
url: request.url,
method: request.method,
error: error.message,
stack: error.stack,
}, 'Request error');
reply.status(error.statusCode || 500).send({
error: 'Internal Server Error',
});
});
// Coors Light Config
await server.register(cors, {
origin: (origin, cb) => {
const allowedOrigins = [
'http://localhost:5173', // vite dev server
'https://deflock.me',
'https://www.deflock.me',
'https://deflock.org', // will migrate
'https://www.deflock.org', // will migrate
];
if (!origin || allowedOrigins.includes(origin) || /^https:\/\/.*\.deflock\.pages\.dev$/.test(origin)) {
cb(null, true);
} else {
cb(null, false);
}
},
methods: ['GET', 'HEAD'],
});
const nominatim = new NominatimClient();
const githubClient = new GithubClient();
const shutdown = async () => {
server.log.info("Shutting down");
await server.close();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
server.get('/geocode', {
schema: {
querystring: {
type: 'object',
properties: {
query: { type: 'string' },
},
required: ['query'],
},
response: {
200: NominatimResultSchema,
500: { type: 'object', properties: { error: { type: 'string' } } },
},
},
}, async (request, reply) => {
const { query } = request.query as { query: string };
reply.header('Cache-Control', 'public, max-age=300, s-maxage=86400');
const result = await nominatim.geocodeSingleResult(query);
return result;
});
server.get('/sponsors/github', {
schema: {
querystring: {
type: 'object',
properties: {
username: { type: 'string', default: 'frillweeman' },
},
},
response: {
200: SponsorsResponseSchema,
500: { type: 'object', properties: { error: { type: 'string' } } },
},
},
}, async (request, reply) => {
const { username } = request.query as { username?: string };
reply.header('Cache-Control', 'public, max-age=60, s-maxage=600');
const result = await githubClient.getSponsors(username || 'frillweeman');
return result;
});
server.head('/healthcheck', async (request, reply) => {
reply.status(200).send();
});
try {
await server.listen({ host: '0.0.0.0', port: 3000 });
console.log('Server listening on port 3000');
} catch (err) {
console.error('Failed to start server:', err);
server.log.error(err);
process.exit(1);
}
};
start().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,39 @@
import { Type, Static } from '@sinclair/typebox';
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
const graphQLEndpoint = 'https://api.github.com/graphql';
export const SponsorSchema = Type.Object({
sponsor: Type.Object({
avatarUrl: Type.String(),
login: Type.String(),
name: Type.Union([Type.String(), Type.Null()]),
url: Type.String(),
}),
});
export type Sponsor = Static<typeof SponsorSchema>;
export const SponsorsResponseSchema = Type.Array(SponsorSchema);
export class GithubClient {
async getSponsors(username: string): Promise<Sponsor[]> {
const query = `query { user(login: \"${username}\") { sponsorshipsAsMaintainer(first: 100) { nodes { sponsor { login name avatarUrl url } } } } }`;
const body = JSON.stringify({ query, variables: '' });
const response = await fetch(graphQLEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'User-Agent': 'Shotgun',
'Content-Type': 'application/json',
},
body,
});
if (!response.ok) {
throw new Error(`Failed to get sponsors: ${response.status}`);
}
const json = await response.json();
return json?.data?.user?.sponsorshipsAsMaintainer?.nodes || [];
}
}

View File

@@ -0,0 +1,88 @@
import { createCache, Cache } from 'cache-manager';
import { Type, Static } from '@sinclair/typebox';
const { DiskStore } = require('cache-manager-fs-hash');
export const NominatimResultSchema = Type.Object({
addresstype: Type.String(),
boundingbox: Type.Tuple([Type.String(), Type.String(), Type.String(), Type.String()]),
class: Type.String(),
display_name: Type.String(),
geojson: Type.Object({
coordinates: Type.Any(),
type: Type.String(),
}),
importance: Type.Number(),
lat: Type.String(),
licence: Type.String(),
lon: Type.String(),
name: Type.String(),
osm_id: Type.Number(),
osm_type: Type.String(),
place_id: Type.Number(),
place_rank: Type.Number(),
type: Type.String(),
});
export type NominatimResult = Static<typeof NominatimResultSchema>;
const cache: Cache = createCache({
stores: [new DiskStore({
path: '/tmp/nominatim-cache',
ttl: 3600 * 24, // 24 hours
maxsize: 1000 * 1000 * 100, // 100MB
subdirs: true,
zip: false,
})]
});
export class NominatimClient {
baseUrl = 'https://nominatim.openstreetmap.org/search';
async geocodePhrase(query: string): Promise<NominatimResult[]> {
const cacheKey = `geocode:${query}`;
const cached = await cache.get(cacheKey);
if (cached) {
return cached as NominatimResult[];
}
const url = `${this.baseUrl}?q=${encodeURIComponent(query)}&polygon_geojson=1&format=json`;
const response = await fetch(url, {
headers: {
'User-Agent': 'DeFlock/1.1',
},
});
if (!response.ok) {
throw new Error(`Failed to geocode phrase: ${response.status}`);
}
const json = await response.json();
await cache.set(cacheKey, json);
return json;
}
async geocodeSingleResult(query: string): Promise<NominatimResult | null> {
const results = await this.geocodePhrase(query);
if (!results.length) return null;
const cityStatePattern = /(.+),\s*(\w{2})/;
const postalCodePattern = /\d{5}/;
if (cityStatePattern.test(query)) {
const cityStateResults = results.filter((result: NominatimResult) =>
["city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", "borough"].includes(result.addresstype)
);
if (cityStateResults.length) {
return cityStateResults[0];
}
}
if (postalCodePattern.test(query)) {
const postalCodeResults = results.filter((result: NominatimResult) => result.addresstype === "postcode");
if (postalCodeResults.length) {
return postalCodeResults[0];
}
}
return results[0];
}
}

70
shotgun/.gitignore vendored
View File

@@ -1,70 +0,0 @@
#
# Are you tempted to edit this file?
#
# First consider if the changes make sense for all,
# or if they are specific to your workflow/system.
# If it is the latter, you can augment this list with
# entries in .git/info/excludes
#
# see also test/files/.gitignore
#
#
# JARs aren't checked in, they are fetched by sbt
#
/lib/*.jar
/test/files/codelib/*.jar
/test/files/lib/*.jar
/test/files/speclib/instrumented.jar
/tools/*.jar
# Developer specific properties
/build.properties
/buildcharacter.properties
# might get generated when testing Jenkins scripts locally
/jenkins.properties
# target directory for build
/build/
# other
/out/
/bin/
/sandbox/
# intellij
/src/intellij*/*.iml
/src/intellij*/*.ipr
/src/intellij*/*.iws
**/.cache
/.idea
/.settings
# vscode
/.vscode
# Standard symbolic link to build/quick/bin
/qbin
# sbt's target directories
/target/
/project/**/target/
/test/macro-annot/target/
/test/files/target/
/test/target/
/build-sbt/
local.sbt
jitwatch.out
# Used by the restarr/restarrFull commands as target directories
/build-restarr/
/target-restarr/
# metals
.metals
.bloop
project/**/metals.sbt
.bsp
.history

View File

@@ -1,32 +0,0 @@
import Dependencies._
ThisBuild / scalaVersion := "2.12.8"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "me.deflock"
ThisBuild / organizationName := "DeFlock"
lazy val root = (project in file("."))
.settings(
name := "shotgun",
libraryDependencies += scalaTest % Test,
)
val PekkoVersion = "1.0.3"
val PekkoHttpVersion = "1.0.1"
libraryDependencies ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.5.6",
"org.slf4j" % "slf4j-api" % "2.0.12",
"org.apache.pekko" %% "pekko-actor-typed" % PekkoVersion,
"org.apache.pekko" %% "pekko-stream" % PekkoVersion,
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,
"org.apache.pekko" %% "pekko-http-spray-json" % PekkoHttpVersion,
"org.apache.pekko" %% "pekko-http-cors" % PekkoHttpVersion,
"org.apache.pekko" %% "pekko-slf4j" % PekkoVersion,
)
assembly / assemblyMergeStrategy := {
case PathList("module-info.class") => MergeStrategy.first
case x =>
val oldStrategy = (assembly / assemblyMergeStrategy).value
oldStrategy(x)
}

View File

@@ -1,5 +0,0 @@
import sbt._
object Dependencies {
lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5"
}

View File

@@ -1 +0,0 @@
sbt.version=1.9.1

View File

@@ -1 +0,0 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -1,123 +0,0 @@
package me.deflock.shotgun
import org.apache.pekko
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.event.Logging
import org.apache.pekko.http.cors.scaladsl.CorsDirectives.cors
import org.apache.pekko.http.cors.scaladsl.model.HttpOriginMatcher
import org.apache.pekko.http.cors.scaladsl.settings.CorsSettings
import org.apache.pekko.http.scaladsl.model.headers.{HttpOrigin, `Access-Control-Allow-Origin`}
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.model._
import pekko.http.scaladsl.server.Directives.{path, _}
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import org.apache.pekko.http.scaladsl.server.RejectionHandler
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import scala.concurrent.ExecutionContextExecutor
import scala.io.StdIn
import org.slf4j.LoggerFactory
object ShotgunServer {
val logger = LoggerFactory.getLogger(getClass)
def main(args: Array[String]): Unit = {
implicit val system: ActorSystem = ActorSystem("my-system")
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
val logging = Logging(system, getClass)
val nominatim = new services.NominatimClient()
val githubClient = new services.GithubClient()
// CORS
val allowedOrigins = List(
"http://localhost:8080",
"http://localhost:5173",
"https://deflock.me",
"https://www.deflock.me",
).map(HttpOrigin(_)) // TODO: make this a config setting
val corsSettings = CorsSettings.default
.withAllowedOrigins(HttpOriginMatcher(allowedOrigins: _*))
.withExposedHeaders(List(`Access-Control-Allow-Origin`.name))
val rejectionHandler = RejectionHandler.newBuilder()
.handleNotFound {
complete((StatusCodes.NotFound, "The requested resource could not be found."))
}
.handle {
case corsRejection: org.apache.pekko.http.cors.scaladsl.CorsRejection =>
complete((StatusCodes.Forbidden, "CORS rejection: Invalid origin"))
}
.result()
val apiRoutes = pathPrefix("api") {
concat (
path("geocode") {
get {
parameters("query".as[String]) { query =>
val encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8.toString)
onSuccess(nominatim.geocodePhrase(encodedQuery)) { json =>
complete(json)
}
}
}
},
path("sponsors" / "github") {
get {
onSuccess(githubClient.getSponsors("frillweeman")) { json =>
complete(json)
}
}
},
path("oauth2" / "callback") {
get {
parameters(Symbol("code").?) { (code) =>
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to Pekko HTTP</h1><p><b>Code: " + code.getOrElse("None") + "</b></p>"))
}
}
},
path("healthcheck") {
get {
complete(HttpResponse(StatusCodes.OK, entity = "Service is healthy"))
}
head {
complete(StatusCodes.OK)
}
}
)
}
val spaRoutes = pathEndOrSingleSlash {
getFromFile("../webapp/dist/index.html")
} ~ getFromDirectory("../webapp/dist") ~
path(Remaining) { _ =>
getFromFile("../webapp/dist/index.html")
}
val routes = handleRejections(rejectionHandler) {
cors(corsSettings) {
concat(apiRoutes, spaRoutes)
}
}
val bindingFuture = Http().newServerAt("0.0.0.0", 8080).bind(routes)
// Handle the binding future properly
bindingFuture.foreach { binding =>
println(s"Server online at http://localhost:${binding.localAddress.getPort}/")
println("Press RETURN to stop...")
}
StdIn.readLine()
bindingFuture
.flatMap(_.unbind())
.onComplete { _ =>
println("Server shutting down...")
system.terminate()
}
}
}

View File

@@ -1,69 +0,0 @@
package services
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.http.javadsl.model.headers.{Authorization, HttpCredentials, UserAgent}
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.model.{HttpMethods, HttpRequest, StatusCodes}
import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal
import spray.json.JsValue
import spray.json._
import org.apache.pekko.http.scaladsl.model.ContentTypes
import org.apache.pekko.http.scaladsl.model.HttpEntity
import org.slf4j.LoggerFactory
import scala.concurrent.{ExecutionContextExecutor, Future}
class GithubClient(implicit val system: ActorSystem, implicit val executionContext: ExecutionContextExecutor) {
val logger = LoggerFactory.getLogger(getClass)
val graphQLEndpoint = "https://api.github.com/graphql"
private val githubApiToken = sys.env("GITHUB_TOKEN")
def getSponsors(username: String): Future[JsArray] = {
val query = s"""
|query {
| user(login: "$username") {
| sponsorshipsAsMaintainer(first: 100) {
| nodes {
| sponsor {
| login
| name
| avatarUrl
| url
| }
| }
| }
| }
|}
|""".stripMargin.replace("\n", " ").replace("\"", "\\\"")
val jsonRequest = s"""{"query": "$query", "variables": ""}"""
val jsonEntity = HttpEntity(ContentTypes.`application/json`, jsonRequest)
val request = HttpRequest(
headers = List(
UserAgent.create("Shotgun"),
Authorization.create(HttpCredentials.create("Bearer", githubApiToken))
),
method = HttpMethods.POST,
uri = graphQLEndpoint,
entity = jsonEntity
)
Http().singleRequest(request).flatMap { response =>
response.status match {
case StatusCodes.OK =>
Unmarshal(response.entity).to[String].map { jsonString =>
jsonString.parseJson.asJsObject
.fields("data").asJsObject
.fields("user").asJsObject
.fields("sponsorshipsAsMaintainer")
.asJsObject.fields("nodes")
.asInstanceOf[JsArray]
}
case _ =>
response.discardEntityBytes()
Future.failed(new Exception(s"Failed to get sponsors: ${response.status}"))
}
}
}
}

View File

@@ -1,53 +0,0 @@
package services
import org.apache.pekko
import org.apache.pekko.actor.ActorSystem
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.model._
import pekko.http.scaladsl.unmarshalling.Unmarshal
import spray.json._
import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future}
class NominatimClient(implicit val system: ActorSystem, implicit val executionContext: ExecutionContextExecutor) {
val baseUrl = "https://nominatim.openstreetmap.org/search"
private val cache: mutable.LinkedHashMap[String, JsValue] = new mutable.LinkedHashMap[String, JsValue]()
private val maxCacheSize = 300
private def cleanUpCache(): Unit = {
if (cache.size > maxCacheSize) {
val oldest = cache.head
cache.remove(oldest._1)
}
}
def geocodePhrase(query: String): Future[JsValue] = {
cleanUpCache()
cache.get(query) match {
case Some(cachedResult) =>
println(s"Cache hit for $query")
Future.successful(cachedResult)
case _ =>
println(s"Cache miss for $query")
val request = HttpRequest(
uri = s"$baseUrl?q=$query&polygon_geojson=1&format=json",
headers = List(headers.`User-Agent`("DeFlock/1.0"))
)
Http().singleRequest(request).flatMap { response =>
response.status match {
case StatusCodes.OK =>
Unmarshal(response.entity).to[String].map { jsonString =>
val json = jsonString.parseJson
cache.put(query, json)
json
}
case _ =>
response.discardEntityBytes()
Future.failed(new Exception(s"Failed to geocode phrase: ${response.status}"))
}
}
}
}
}

View File

@@ -1,9 +0,0 @@
package example
import org.scalatest._
class HelloSpec extends FlatSpec with Matchers {
"The Hello object" should "say hello" in {
Hello.greeting shouldEqual "hello"
}
}

View File

@@ -48,7 +48,7 @@ export class BoundingBox implements BoundingBoxLiteral {
}
const apiService = axios.create({
baseURL: window.location.hostname === "localhost" ? "http://localhost:8080/api" : "/api",
baseURL: window.location.hostname === "localhost" ? "http://localhost:3000" : "https://api.deflock.org",
headers: {
"Content-Type": "application/json",
},
@@ -82,49 +82,8 @@ export const getCities = async () => {
return response.data;
}
export const geocodeQuery = async (query: string, currentLocation: any) => {
export const geocodeQuery = async (query: string) => {
const encodedQuery = encodeURIComponent(query);
const results = (await apiService.get(`/geocode?query=${encodedQuery}`)).data;
function findNearestResult(results: any, currentLocation: any) {
let nearestResult = results[0];
let nearestDistance = Number.MAX_VALUE;
for (const result of results) {
const distance = Math.sqrt(
Math.pow(result.lat - currentLocation.lat, 2) +
Math.pow(result.lon - currentLocation.lng, 2)
);
if (distance < nearestDistance) {
nearestResult = result;
nearestDistance = distance;
}
}
return nearestResult;
}
if (!results.length) return null;
const cityStatePattern = /(.+),\s*(\w{2})/;
const postalCodePattern = /\d{5}/;
if (cityStatePattern.test(query)) {
console.debug("cityStatePattern");
const cityStateResults = results.filter((result: any) =>
["city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", "borough"].includes(result.addresstype)
);
if (cityStateResults.length) {
return findNearestResult(cityStateResults, currentLocation);
}
}
if (postalCodePattern.test(query)) {
console.debug("postalCodePattern");
const postalCodeResults = results.filter((result: any) => result.addresstype === "postcode");
if (postalCodeResults.length) {
return findNearestResult(postalCodeResults, currentLocation);
}
}
console.debug("defaultPattern");
return findNearestResult(results, currentLocation);
const result = (await apiService.get(`/geocode?query=${encodedQuery}`)).data;
return result;
}