Compare commits

..

125 Commits

Author SHA1 Message Date
a39cfd2027 Merge pull request 'BREAKING CHANGE: V2 of thr SDK' (#12) from develop/postgresql into main
Some checks failed
Gitea Build Check / Build (push) Failing after 6m26s
Reviewed-on: #12
Reviewed-by: Lennard Brinkhaus <lennard.brinkhaus@noreply.localhost>
2024-07-01 20:46:26 +00:00
768203ebd7 fix: fixed function to work properly
Some checks failed
Gitea Build Check / Build (pull_request) Failing after 6m32s
Gitea Build Check / Build (push) Failing after 6m28s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 22:40:53 +02:00
2d4102be6a refactor: added getting of tag by specific type
Some checks failed
Gitea Build Check / Build (push) Has been cancelled
Gitea Build Check / Build (pull_request) Successful in 11m44s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 22:29:37 +02:00
a842d64f1c refactor: added deletion of tags
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 22:15:43 +02:00
1b40c6145a refactor: changed CreateTag to align with other functions
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 22:06:36 +02:00
47fda2994b fix: fixed issues in PR
All checks were successful
Gitea Build Check / Build (push) Successful in 11m38s
Gitea Build Check / Build (pull_request) Successful in 11m37s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 21:38:09 +02:00
3dfb345edc fix: make sonarqube happy
All checks were successful
Gitea Build Check / Build (push) Successful in 11m26s
Gitea Build Check / Build (pull_request) Successful in 11m31s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 14:14:17 +02:00
5f9ace9bd3 feat: implemented all functions
All checks were successful
Gitea Build Check / Build (push) Successful in 11m38s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 13:42:53 +02:00
6ccd593d88 refactor: changed code layout
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-07-01 11:36:21 +02:00
d759e072f7 feat(TagGroup): added function and tests
All checks were successful
Gitea Build Check / Build (push) Successful in 11m24s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 22:39:12 +02:00
702870db1e feat(TagAlias): added function and tests
All checks were successful
Gitea Build Check / Build (push) Successful in 11m35s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 22:27:31 +02:00
26178eb64d feat(TagAlias): added function and tests
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 22:14:27 +02:00
bb45658310 feat(TagAlias): added function and tests
All checks were successful
Gitea Build Check / Build (push) Successful in 11m5s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 15:13:40 +02:00
b5437ecc62 tests: fixed tests
All checks were successful
Gitea Build Check / Build (push) Successful in 10m55s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 14:16:28 +02:00
4a0b7d71a9 refactor: reorganized code structure
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 12:15:51 +02:00
5de00e6861 refactor: reorganized code structure
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 10:58:07 +02:00
bc8b5bb4a7 refactor: reorganized code structure
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 10:54:42 +02:00
429f68899d feat(database): new fields
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-28 10:43:55 +02:00
9c8a7c1e16 fix(tests): remove timeout
Some checks failed
Gitea Build Check / Build (push) Failing after 4m49s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-27 22:18:35 +02:00
51aa79e4c2 feat(newAPIs): initial structure for new api endpoints
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-27 22:17:25 +02:00
56bb414f9d feat(PostReference): added the ability to add missing filed in db
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-27 22:05:21 +02:00
da4fda3597 refactor(postgres): a lot of things happened here
Some checks failed
Gitea Build Check / Build (push) Failing after 6m7s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-27 21:37:29 +02:00
a71f759763 feat(functions): fixing bugs
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-27 16:11:16 +02:00
e29cea6e3c feat(functions): enhanced function with RAW SQL
Some checks failed
Gitea Build Check / Build (push) Failing after 4m59s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 16:12:10 +02:00
a9a21581bd test(helper): extend the waiting time to 10 seconds
All checks were successful
Gitea Build Check / Build (push) Successful in 10m40s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 12:40:59 +02:00
97d314ba3e ci: fix go command to show in log
Some checks failed
Gitea Build Check / Build (push) Failing after 5m38s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 12:20:38 +02:00
0a5359d723 ci: fix go command to show in log
Some checks failed
Gitea Build Check / Build (push) Failing after 5m40s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 12:07:55 +02:00
a026563835 fix(errors): added more tests
Some checks failed
Gitea Build Check / Build (push) Failing after 5m41s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 11:48:25 +02:00
615a6b1a25 fix(errors): fix unneeded error handling
Some checks failed
Gitea Build Check / Build (push) Failing after 5m41s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 11:31:42 +02:00
cb04347dd5 fix(errors): more tests
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 11:23:27 +02:00
6e3edfc1fa fix(errors): renamed error package
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 10:02:54 +02:00
4fa936d774 fix(errors): added const
All checks were successful
Gitea Build Check / Build (push) Successful in 10m58s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 09:20:52 +02:00
c550531a35 tests(errors): refactored old tests to reflect new error handling
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-26 09:10:04 +02:00
f66fc6b6d7 tests(errors): fix test cases to reflect the new validation and error handling
Some checks failed
Gitea Build Check / Build (push) Failing after 5m49s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 22:22:05 +02:00
d94ae83c2f feat(errors): implemented custom errors
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 21:55:43 +02:00
63e74219ed feat(error): added first steps for custom errors
All checks were successful
Gitea Build Check / Build (push) Successful in 11m2s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 16:02:08 +02:00
c246f661ff ci: update version of steps
All checks were successful
Gitea Build Check / Build (push) Successful in 11m7s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 15:13:52 +02:00
f34cc637ec ci: readable cache and check for latest
Some checks failed
Gitea Build Check / Build (push) Has been cancelled
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 15:11:47 +02:00
819dc7055a feat(postgres): updated go mod
Some checks failed
Gitea Build Check / Build (push) Failing after 1m19s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 15:09:39 +02:00
07b69f6c18 test(postgres): fixed to use new ORM Generic
Some checks failed
Gitea Build Check / Build (push) Failing after 1m15s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 15:00:44 +02:00
d8f338332b ci: disable cache
Some checks failed
Gitea Build Check / Build (push) Failing after 1m17s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 14:56:08 +02:00
d3c0599263 ci: disable check for latest go
Some checks failed
Gitea Build Check / Build (push) Has been cancelled
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 14:55:25 +02:00
c0cae140f1 test(postgres): finished test migrations to generic orm base
Some checks failed
Gitea Build Check / Build (push) Has been cancelled
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-25 14:50:56 +02:00
5aac1f2916 feat(postgres): start implementing new generic orm model
Some checks failed
Gitea Build Check / Build (push) Failing after 5m46s
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-25 09:09:18 +02:00
31c4249129 refactor(postgres): use generics for IDs
Some checks failed
Gitea Build Check / Build (push) Failing after 4m57s
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-24 22:31:18 +02:00
d90736b1b6 docs(postgres): rearranged
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-24 22:30:57 +02:00
895c4cd436 docs(postgres): rearranged
Some checks failed
Gitea Build Check / Build (push) Has been cancelled
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-24 22:30:05 +02:00
df14171bad docs(postgres): added all the sonarqube badges
Some checks failed
Gitea Build Check / Build (push) Has been cancelled
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-24 22:29:01 +02:00
8ea3369744 docs(postgres): added all the sonarqube badges
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-24 22:18:34 +02:00
97758b180e test(postgres): added more tests and some fixes
All checks were successful
Gitea Build Check / Build (push) Successful in 10m44s
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-24 22:07:34 +02:00
2a752adcac test(postgres): added more tests
All checks were successful
Gitea Build Check / Build (push) Successful in 10m49s
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-24 17:07:41 +02:00
30fa03ef9a feat(postgres): added more tests
All checks were successful
Gitea Build Check / Build (push) Successful in 10m40s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-24 14:28:09 +02:00
57729359d2 feat(postgres): using only gorm
All checks were successful
Gitea Build Check / Build (push) Successful in 10m40s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-24 13:13:32 +02:00
b40d93c993 feat(postgres): config map
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-24 12:59:51 +02:00
bfa074fd9b ci(postgres): fixed trigger
All checks were successful
Gitea Build Check / Build (push) Successful in 10m54s
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-24 12:02:39 +02:00
1ed6b91214 ci(postgres): implemented CI for build check in sonarqube
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-24 11:58:29 +02:00
a5f41e7a2d fix(postgres): function mismatch with new types
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-23 22:39:54 +02:00
b0d98d842f refactor(postgres): renamed function names and fixed tests and other issues
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-23 22:35:46 +02:00
8a0229eb23 test(postgres): renamed functions to be more clear
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-23 21:23:38 +02:00
7842976f4b test(postgres): fixed the tests & also squashed some bugs
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-23 01:18:23 +02:00
bb7cb23c2a refactor(postgres): Refactor post retrieval functions and remove unnecessary code in tag and user creation
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 23:25:28 +02:00
9471941e02 refactor(postgres): fixed any IDE errors
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 23:25:06 +02:00
b28891f383 refactor(postgres): fixed any IDE errors
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 23:23:23 +02:00
55296d9a95 refactor(postgres): renamed functions to be more clear
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 22:30:03 +02:00
78145fe02a refactor(postgres): removed old docs
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 22:28:29 +02:00
2621eea00c refactor(postgres): fixed implementation
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 22:27:38 +02:00
b11cfbaa24 refactor(postgres): removed old graph related code
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 22:07:36 +02:00
58dcf94d49 refactor(postgres): removed old graph related code
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-22 22:06:36 +02:00
536f7c0443 fix(postgres): Update database logger in postgres.go to include slow queries and error handling
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 23:48:18 +02:00
3c17c60439 fix(postgres): Update postgres.go to use silent logger for GORM configuration
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 23:35:41 +02:00
c07fd69f4f fix(postgres): CheckIfAnthrovePostNodeExistsBySourceURL function to use PostReference model
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 23:25:55 +02:00
c359b4636e fix(postgres): prefilled posts with empty
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 23:19:03 +02:00
0b7b6beccd fix(postgres): Update CheckIfAnthrovePostNodeExistsBySourceURL to return pointer instead of value
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 22:58:52 +02:00
5e501b2003 fix(postgres): ** whyy
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 22:57:05 +02:00
b86c667156 fix(postgres): CheckIfAnthrovePostNodeExistsBySourceURL now working as intended
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 22:54:47 +02:00
aea99da484 fix(postgres): fix creating issue
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 17:50:31 +02:00
f5360d2757 fix(migration):remove unique key for url
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 15:52:59 +02:00
8a2803276a fix(migration):add url to primary key
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 15:25:22 +02:00
e09d39dd02 feat(postgres): added copyright as enum
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 15:18:40 +02:00
4690ce135d fix(postgres): added url to SourceLink
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 15:15:20 +02:00
2515fc5081 fix(postgres): check if source exists before creating
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 15:08:42 +02:00
795bcbdac3 feat(postgres): added new each function
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 14:56:11 +02:00
d4bec1d8c7 fix(postgres): removed unneeded code that breaks the thing
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 14:36:38 +02:00
40ce69567a dix(postgres): wrong parameters
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-21 14:16:03 +02:00
84aee5c2dd test(postgres): finished tests for user and fixed bugs
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-20 22:45:03 +02:00
947a77d13f test(postgres): added first tests for user and fixed bugs
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-20 15:24:55 +02:00
83a5c89438 test(postgres): added tag and fixed bugs
Signed-off-by: soxx <soxx@fenpa.ws>
2024-06-20 14:54:06 +02:00
e6931570c3 chore(postgres): removed dead code 2024-06-19 22:16:42 +00:00
e7cf655362 test(postgres): added sources and fixed bugs 2024-06-20 00:13:39 +02:00
03ce8b52ae test(postgres): added first tests and already fixed bugs 2024-06-19 23:32:42 +02:00
5a242c6989 docs(postgres): updated readme 2024-06-14 15:43:40 +02:00
1da21024d7 feat(postgres): added GetUserTagsTroughFavedPosts 2024-06-14 15:16:31 +02:00
8cde1235ad feat(postgres): added GetUserFavoritePostsWithPagination 2024-06-14 15:11:13 +02:00
6268a594cf feat(postgres): added GetAllAnthroveUserIDs 2024-06-14 15:09:11 +02:00
4cdd4450d5 feat(postgres): added GetAnthroveUser 2024-06-14 15:07:23 +02:00
bbd08b9a2f feat(postgres): added GetSpecifiedUserSourceLink 2024-06-14 15:04:45 +02:00
16d5a381df feat(postgres): added GetUserSourceLinks 2024-06-14 15:03:34 +02:00
4f8e54f09a feat(postgres): added GetUserFavoriteCount 2024-06-14 14:50:29 +02:00
c04e382ee5 feat(postgres): added AddUserWithRelationToSource 2024-06-14 14:48:23 +02:00
78777ce7be feat(postgres): partially added Check heckPostNodeExistsBy 2024-06-14 13:57:03 +02:00
bd22ed3f79 feat(postgres): added AddPost 2024-06-14 13:29:53 +02:00
ad3a43366b feat(postgres): added CheckUserPostLink 2024-06-14 13:25:54 +02:00
a430c3b582 feat(postgres): added LinkUserWithPost 2024-06-14 13:22:52 +02:00
8d655e991f feat(postgres): added LinkPostWithSource 2024-06-14 13:19:55 +02:00
011f4d1c2a feat(postgres): added tag create and function to get all tags 2024-06-14 13:05:07 +02:00
aae7fc78e0 feat(postgres): added image urls to the database 2024-06-14 12:24:14 +02:00
a60805fccf feat(postgres): added console debug option 2024-06-14 12:06:58 +02:00
b248cbf0c7 feat(db): start implementing sources & fixing issues that come along the way
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 23:14:02 +02:00
8f87eb6ce8 feat(db): start implementing sources & fixing issues that come along the way
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 15:16:37 +02:00
5aa1542074 feat(db): corrected tables
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 14:00:00 +02:00
ccb7cff4fc refactor(db): removed unused context
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 13:59:38 +02:00
f0bd89759c refactor(db): logging & tracing
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 13:44:15 +02:00
ebd25580fe feat(db): reduce to one migration file
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 13:35:14 +02:00
b65e942f24 feat(db): embed migration files
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 13:28:14 +02:00
549c1ca573 docs(db): removing unused entities
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 13:07:18 +02:00
751f387b86 feat(db): finalizing migration statements
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 13:06:38 +02:00
b2b354c60f feat(db): initial setup for migrations
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 09:33:35 +02:00
5a693063bb feat(sources): finalized model with gorm annotation
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-04 08:49:46 +02:00
21b43333d0 feat(sources): restructure models and first implementation of pgModel
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-03 23:55:30 +02:00
ea2958b27d feat(sources): finished implementing get all sources
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-03 22:18:02 +02:00
ebc7fcc571 feat(sources): get all source nodes
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-03 22:14:50 +02:00
c1df01d113 chore: reorganizing code
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-03 21:45:39 +02:00
0d5274eb27 feat: connecting to postgres now possible
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-03 21:31:44 +02:00
ccc6010466 chore: streamline the naming to indicate more than graph support
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-03 19:32:25 +02:00
5015749f70 chore: streamline the naming to indicate more than graph support
Signed-off-by: SoXX <soxx@fenpa.ws>
2024-06-03 19:31:25 +02:00
59 changed files with 9696 additions and 1218 deletions

View File

@ -0,0 +1,39 @@
name: Gitea Build Check
run-name: ${{ gitea.actor }} is testing the build
on:
push:
branches:
- develop/postgresql
pull_request:
branches: [ "main" ]
jobs:
Build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Setup Go environment
uses: https://github.com/actions/setup-go@v5
with:
# The Go version to download (if necessary) and use. Supports semver spec and ranges.
go-version: 1.22.0 # optional
# Path to the go.mod file.
go-version-file: ./go.mod # optional
# Set this option to true if you want the action to always check for the latest available version that satisfies the version spec
check-latest: true # optional
# Used to specify whether caching is needed. Set to true, if you'd like to enable caching.
cache: true # optional
- name: Execute Go Test files with coverage report
run: TESTCONTAINERS_RYUK_DISABLED=true go test -v ./... -json -coverprofile="coverage.out" | tee "test-report.out"
- uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectKey=Anthrove---OtterSpace-SDK

View File

@ -1,3 +1,19 @@
![Build Check Runner](https://git.dragse.it/anthrove/otter-space-sdk/actions/workflows/build_check.yaml/badge.svg)
[![Bugs](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=bugs&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Code Smells](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=code_smells&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Coverage](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=coverage&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Duplicated Lines (%)](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=duplicated_lines_density&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Lines of Code](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=ncloc&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Maintainability Rating](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=sqale_rating&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Quality Gate Status](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=alert_status&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Reliability Rating](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=reliability_rating&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Security Hotspots](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=security_hotspots&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Security Rating](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=security_rating&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
[![Vulnerabilities](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=vulnerabilities&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK)
# OtterSpace SDK # OtterSpace SDK
The OtterSpace SDK is a Go package for interacting with the OtterSpace API. It provides methods for connecting to the API, adding and linking users, posts, and sources, and retrieving information about users and posts. The OtterSpace SDK is a Go package for interacting with the OtterSpace API. It provides methods for connecting to the API, adding and linking users, posts, and sources, and retrieving information about users and posts.
@ -17,20 +33,24 @@ Here's a simple usage example:
package main package main
import ( import (
"context" "context"
"fmt" "fmt"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models" "git.dragse.it/anthrove/otter-space-sdk/pkg/database"
"git.dragse.it/anthrove/otter-space-sdk/pkg/graph" "git.dragse.it/anthrove/otter-space-sdk/pkg/models"
) )
func main() { func main() {
client := graph.NewGraphConnection() var err error
err := client.Connect(context.Background(), "your-endpoint", "your-username", "your-password") dbDebug := false
if err != nil { ctx := context.Background()
fmt.Println(err)
return pgClient := database.NewPostgresqlConnection(dbDebug)
} err = pgClient.Connect(ctx, "your-endpoint", "your-username", "your-password", "anthrove", 5432, "disable", "Europe/Berlin")
// further usage of the client... if err != nil {
fmt.Println(err)
return
}
// further usage of the client...
} }
``` ```

67
go.mod
View File

@ -3,8 +3,71 @@ module git.dragse.it/anthrove/otter-space-sdk
go 1.22.0 go 1.22.0
require ( require (
github.com/neo4j/neo4j-go-driver/v5 v5.17.0 github.com/lib/pq v1.10.9
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/rubenv/sql-migrate v1.6.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/testcontainers/testcontainers-go v0.31.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.25.10
) )
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.15 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/docker v25.0.5+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)

222
go.sum
View File

@ -1,17 +1,229 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes=
github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/neo4j/neo4j-go-driver/v5 v5.17.0 h1:Bdqg1Y8Hd3uLYToXtBjysDYXTdMiP7zeUNUEwfbJkSo= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/neo4j/neo4j-go-driver/v5 v5.17.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos=
github.com/rubenv/sql-migrate v1.6.1/go.mod h1:tPzespupJS0jacLfhbwto/UjSX+8h2FdWB7ar+QlHa0=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U=
github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI=
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E=
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d h1:pgIUhmqwKOUlnKna4r6amKdUngdL8DrkpFeV8+VBElY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

View File

@ -1,46 +0,0 @@
package internal
import (
"fmt"
neo4jLog "github.com/neo4j/neo4j-go-driver/v5/neo4j/log"
log "github.com/sirupsen/logrus"
)
type graphLogger struct {
graphDebug bool
}
func NewGraphLogger(graphDebug bool) neo4jLog.Logger {
return &graphLogger{graphDebug: graphDebug}
}
func (n graphLogger) Error(name string, id string, err error) {
log.WithFields(log.Fields{
"name": name,
"id": id,
}).Errorf("graph: %s", err)
}
func (n graphLogger) Warnf(name string, id string, msg string, args ...any) {
log.WithFields(log.Fields{
"name": name,
"id": id,
}).Warnf("graph: %v", fmt.Sprintf(msg, args...))
}
func (n graphLogger) Infof(name string, id string, msg string, args ...any) {
log.WithFields(log.Fields{
"name": name,
"id": id,
}).Infof("graph: %v", fmt.Sprintf(msg, args...))
}
func (n graphLogger) Debugf(name string, id string, msg string, args ...any) {
if n.graphDebug {
log.WithFields(log.Fields{
"name": name,
"id": id,
}).Debugf("graph: %v", fmt.Sprintf(msg, args...))
}
}

View File

@ -1,116 +0,0 @@
package internal
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
log "github.com/sirupsen/logrus"
)
func CreateAnthrovePostNode(ctx context.Context, driver neo4j.DriverWithContext, anthrovePost *models.AnthrovePost) error {
query := `
CREATE (newPostNode:AnthrovePost {post_id: $anthrove_post_id, rating: $anthrove_rating})
`
params := map[string]any{
"anthrove_post_id": anthrovePost.PostID,
"anthrove_rating": anthrovePost.Rating,
}
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return err
}
log.WithFields(log.Fields{
"anthrove_post_id": anthrovePost.PostID,
"anthrove_post_rating": anthrovePost.Rating,
}).Trace("graph: created anthrove post")
return nil
}
func CheckIfAnthrovePostNodeExistsByAnthroveID(ctx context.Context, driver neo4j.DriverWithContext, anthrovePost *models.AnthrovePost) (*models.AnthrovePost, bool, error) {
query := `
OPTIONAL MATCH (postNode:AnthrovePost {post_id: $anthrove_post_id})
RETURN postNode.post_id AS AnthrovePostID
`
params := map[string]any{
"anthrove_post_id": anthrovePost.PostID,
}
anthrovePost, exists, err := executeCheckQuery(ctx, driver, query, params)
if err != nil {
return nil, false, err
}
return anthrovePost, exists, nil
}
func CheckIfAnthrovePostNodeExistsBySourceURl(ctx context.Context, driver neo4j.DriverWithContext, sourceUrl string) (*models.AnthrovePost, bool, error) {
query := `
OPTIONAL MATCH (postNode:AnthrovePost)<-[:REFERENCE {url: $source_url}]-()
RETURN postNode.post_id AS AnthrovePostID
`
params := map[string]any{
"source_url": sourceUrl,
}
anthrovePost, exists, err := executeCheckQuery(ctx, driver, query, params)
if err != nil {
return nil, false, err
}
return anthrovePost, exists, nil
}
func CheckIfAnthrovePostNodeExistsBySourceID(ctx context.Context, driver neo4j.DriverWithContext, sourcePostID string) (*models.AnthrovePost, bool, error) {
query := `
OPTIONAL MATCH (postNode:AnthrovePost)<-[:REFERENCE {source_post_id: $source_post_id}]-()
RETURN postNode.post_id AS AnthrovePostID
`
params := map[string]any{
"source_post_id": sourcePostID,
}
anthrovePost, exists, err := executeCheckQuery(ctx, driver, query, params)
if err != nil {
return nil, false, err
}
return anthrovePost, exists, nil
}
func executeCheckQuery(ctx context.Context, driver neo4j.DriverWithContext, query string, params map[string]any) (*models.AnthrovePost, bool, error) {
var anthrovePost models.AnthrovePost
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return &anthrovePost, false, err
}
record := result.Records
anthrovePostID, isNil, err := neo4j.GetRecordValue[string](record[0], "AnthrovePostID")
exists := !isNil
if err != nil {
return &anthrovePost, exists, err
}
anthrovePost.PostID = models.AnthrovePostID(anthrovePostID)
log.WithFields(log.Fields{
"anthrove_post_id": anthrovePost.PostID,
"anthrove_post_exists": exists,
}).Trace("graph: checked if post exists")
if !exists {
return nil, exists, nil
}
return &anthrovePost, exists, nil
}

98
internal/postgres/post.go Normal file
View File

@ -0,0 +1,98 @@
package postgres
import (
"context"
"errors"
otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func CreatePost(ctx context.Context, db *gorm.DB, anthrovePost *models.Post) error {
if anthrovePost == nil {
return &otterError.EntityValidationFailed{Reason: "anthrovePost is nil"}
}
result := db.WithContext(ctx).Create(&anthrovePost)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
if result.RowsAffected == 0 {
return &otterError.NoDataWritten{}
}
log.WithFields(log.Fields{
"anthrove_post_id": anthrovePost.ID,
"anthrove_post_rating": anthrovePost.Rating,
}).Trace("database: created anthrove post")
return nil
}
func GetPostByAnthroveID(ctx context.Context, db *gorm.DB, anthrovePostID models.AnthrovePostID) (*models.Post, error) {
if anthrovePostID == "" {
return nil, &otterError.EntityValidationFailed{Reason: "anthrovePostID is not set"}
}
if len(anthrovePostID) != 25 {
return nil, &otterError.EntityValidationFailed{Reason: "anthrovePostID needs to be 25 characters long"}
}
var post models.Post
result := db.WithContext(ctx).First(&post, "id = ?", anthrovePostID)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
return &post, nil
}
func GetPostBySourceURL(ctx context.Context, db *gorm.DB, sourceURL string) (*models.Post, error) {
if sourceURL == "" {
return nil, &otterError.EntityValidationFailed{Reason: "sourceURL is not set"}
}
var post models.Post
result := db.WithContext(ctx).Raw(`SELECT p.id AS id, p.rating as rating FROM "Post" AS p INNER JOIN "PostReference" AS pr ON p.id = pr.post_id AND pr.url = $1 LIMIT 1`, sourceURL).First(&post)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
return &post, nil
}
func GetPostBySourceID(ctx context.Context, db *gorm.DB, sourceID models.AnthroveSourceID) (*models.Post, error) {
if sourceID == "" {
return nil, &otterError.EntityValidationFailed{Reason: "sourceID is not set"}
}
var post models.Post
result := db.WithContext(ctx).Raw(`SELECT p.id AS id, p.rating as rating FROM "Post" AS p INNER JOIN "PostReference" AS pr ON p.id = pr.post_id AND pr.source_id = $1 LIMIT 1`, sourceID).First(&post)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
return &post, nil
}

View File

@ -0,0 +1,381 @@
package postgres
import (
"context"
"fmt"
"testing"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"git.dragse.it/anthrove/otter-space-sdk/test"
_ "github.com/lib/pq"
"gorm.io/gorm"
)
func TestCreateAnthrovePostNode(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Tests
validPost := &models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")),
},
Rating: "safe",
}
invalidPost := &models.Post{
Rating: "error",
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
anthrovePost *models.Post
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Test 1: Valid AnthrovePostID and Rating",
args: args{
ctx: context.Background(),
db: gormDB,
anthrovePost: validPost,
},
wantErr: false,
},
{
name: "Test 2: Invalid Rating",
args: args{
ctx: context.Background(),
db: gormDB,
anthrovePost: invalidPost,
},
wantErr: true,
},
{
name: "Test 3: Nill",
args: args{
ctx: context.Background(),
db: gormDB,
anthrovePost: nil,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CreatePost(tt.args.ctx, tt.args.db, tt.args.anthrovePost); (err != nil) != tt.wantErr {
t.Errorf("CreatePost() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGetPostByAnthroveID(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Tests
post := &models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")),
},
Rating: "safe",
}
err = CreatePost(ctx, gormDB, post)
if err != nil {
t.Fatal("Could not create post", err)
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
anthrovePostID models.AnthrovePostID
}
tests := []struct {
name string
args args
want *models.Post
wantErr bool
}{
{
name: "Test 1: Valid anthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthrovePostID: post.ID,
},
want: post,
wantErr: false,
},
{
name: "Test 2: Invalid anthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthrovePostID: "1234",
},
want: nil,
wantErr: true,
},
{
name: "Test 3: No anthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthrovePostID: "",
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetPostByAnthroveID(tt.args.ctx, tt.args.db, tt.args.anthrovePostID)
if (err != nil) != tt.wantErr {
t.Errorf("GetPostByAnthroveID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !checkPost(got, tt.want) {
t.Errorf("GetPostByAnthroveID() got = %v, want %v", got, tt.want)
}
})
}
}
func TestGetPostBySourceURL(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Tests
post := &models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")),
},
Rating: "safe",
}
err = CreatePost(ctx, gormDB, post)
if err != nil {
t.Fatal("Could not create post", err)
}
source := models.Source{
BaseModel: models.BaseModel[models.AnthroveSourceID]{
ID: models.AnthroveSourceID(fmt.Sprintf("%025s", "1")),
},
DisplayName: "e621",
Domain: "e621.net",
Icon: "https://e621.net/icon.ico",
}
err = CreateSource(ctx, gormDB, &source)
if err != nil {
t.Fatal("Could not create source", err)
}
err = CreateReferenceBetweenPostAndSource(ctx, gormDB, post.ID, models.AnthroveSourceDomain(source.Domain), "http://test.org", models.PostReferenceConfig{})
if err != nil {
t.Fatal("Could not create source reference", err)
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
sourceURL string
}
tests := []struct {
name string
args args
want *models.Post
wantErr bool
}{
{
name: "Test 1: Valid sourceURL",
args: args{
ctx: ctx,
db: gormDB,
sourceURL: "http://test.org",
},
want: post,
wantErr: false,
},
{
name: "Test 2: Invalid sourceURL",
args: args{
ctx: ctx,
db: gormDB,
sourceURL: "1234",
},
want: nil,
wantErr: true,
},
{
name: "Test 3: No sourceURL",
args: args{
ctx: ctx,
db: gormDB,
sourceURL: "",
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetPostBySourceURL(tt.args.ctx, tt.args.db, tt.args.sourceURL)
if (err != nil) != tt.wantErr {
t.Errorf("GetPostBySourceURL() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !checkPost(got, tt.want) {
t.Errorf("GetPostBySourceURL() got = %v, want %v", got, tt.want)
}
})
}
}
func TestGetPostBySourceID(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Tests
post := &models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")),
},
Rating: "safe",
}
err = CreatePost(ctx, gormDB, post)
if err != nil {
t.Fatal("Could not create post", err)
}
source := models.Source{
BaseModel: models.BaseModel[models.AnthroveSourceID]{
ID: models.AnthroveSourceID(fmt.Sprintf("%025s", "1")),
},
DisplayName: "e621",
Domain: "e621.net",
Icon: "https://e621.net/icon.ico",
}
err = CreateSource(ctx, gormDB, &source)
if err != nil {
t.Fatal("Could not create source", err)
}
err = CreateReferenceBetweenPostAndSource(ctx, gormDB, post.ID, models.AnthroveSourceDomain(source.Domain), "http://test.otg", models.PostReferenceConfig{})
if err != nil {
t.Fatal("Could not create source reference", err)
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
sourceID models.AnthroveSourceID
}
tests := []struct {
name string
args args
want *models.Post
wantErr bool
}{
{
name: "Test 1: Valid sourceID",
args: args{
ctx: ctx,
db: gormDB,
sourceID: source.ID,
},
want: post,
wantErr: false,
},
{
name: "Test 2: Invalid sourceID",
args: args{
ctx: ctx,
db: gormDB,
sourceID: "1234",
},
want: nil,
wantErr: true,
},
{
name: "Test 3: No sourceID",
args: args{
ctx: ctx,
db: gormDB,
sourceID: "",
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetPostBySourceID(tt.args.ctx, tt.args.db, tt.args.sourceID)
if (err != nil) != tt.wantErr {
t.Errorf("GetPostBySourceID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !checkPost(got, tt.want) {
t.Errorf("GetPostBySourceID() got = %v, want %v", got, tt.want)
}
})
}
}
func checkPost(got *models.Post, want *models.Post) bool {
if got == nil && want == nil {
return true
} else if got == nil || want == nil {
return false
}
if got.ID != want.ID {
return false
}
if got.Rating != want.Rating {
return false
}
return true
}

View File

@ -0,0 +1,126 @@
package postgres
import (
"context"
"errors"
otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func CreateReferenceBetweenPostAndSource(ctx context.Context, db *gorm.DB, anthrovePostID models.AnthrovePostID, sourceDomain models.AnthroveSourceDomain, postURL models.AnthrovePostURL, config models.PostReferenceConfig) error {
if anthrovePostID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthrovePostID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if sourceDomain == "" {
return &otterError.EntityValidationFailed{Reason: "sourceDomain cannot be empty"}
}
result := db.WithContext(ctx).Exec(`INSERT INTO "PostReference" (post_id, source_id, url, full_file_url, preview_file_url, sample_file_url, source_post_id) SELECT $1, source.id, $2, $4, $5, $6, $7 FROM "Source" AS source WHERE domain = $3;`, anthrovePostID, postURL, sourceDomain, config.FullFileURL, config.PreviewFileURL, config.SampleFileURL, config.SourcePostID)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return &otterError.NoDataFound{}
}
if errors.Is(result.Error, gorm.ErrCheckConstraintViolated) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
if result.RowsAffected == 0 {
return &otterError.NoDataWritten{}
}
log.WithFields(log.Fields{
"anthrove_post_id": anthrovePostID,
"anthrove_source_domain": sourceDomain,
}).Trace("database: created anthrove post to source link")
return nil
}
func CreateReferenceBetweenUserAndPost(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) error {
if anthrovePostID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthrovePostID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if anthroveUserID == "" {
return &otterError.EntityValidationFailed{Reason: "anthroveUserID cannot be empty"}
}
userFavorite := models.UserFavorites{
UserID: string(anthroveUserID),
PostID: string(anthrovePostID),
}
result := db.WithContext(ctx).Create(&userFavorite)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
if result.RowsAffected == 0 {
return &otterError.NoDataWritten{}
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"anthrove_post_id": anthrovePostID,
}).Trace("database: created user to post link")
return nil
}
func CheckReferenceBetweenUserAndPost(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) (bool, error) {
var count int64
if anthrovePostID == "" {
return false, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthrovePostID) != 25 {
return false, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if anthroveUserID == "" {
return false, &otterError.EntityValidationFailed{Reason: "anthroveUserID cannot be empty"}
}
if len(anthroveUserID) != 25 {
return false, &otterError.EntityValidationFailed{Reason: "anthroveUserID needs to be 25 characters long"}
}
result := db.WithContext(ctx).Model(&models.UserFavorites{}).Where("user_id = ? AND post_id = ?", string(anthroveUserID), string(anthrovePostID)).Count(&count)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, &otterError.NoDataFound{}
}
return false, result.Error
}
exists := count > 0
log.WithFields(log.Fields{
"relationship_exists": exists,
"relationship_anthrove_user_id": anthroveUserID,
"relationship_anthrove_post_id": anthrovePostID,
}).Trace("database: checked user post relationship")
return exists, nil
}

View File

@ -0,0 +1,392 @@
package postgres
import (
"context"
"fmt"
"testing"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"git.dragse.it/anthrove/otter-space-sdk/test"
"gorm.io/gorm"
)
func TestCheckUserToPostLink(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Test
validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1"))
invalidUserID := models.AnthroveUserID("XXX")
validPostID := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1"))
err = CreateUser(ctx, gormDB, validUserID)
if err != nil {
t.Fatal(err)
}
post := &models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: validPostID,
},
Rating: "safe",
}
err = CreatePost(ctx, gormDB, post)
if err != nil {
t.Fatal(err)
}
err = CreateReferenceBetweenUserAndPost(ctx, gormDB, validUserID, post.ID)
if err != nil {
t.Fatal(err)
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
anthroveUserID models.AnthroveUserID
anthrovePostID models.AnthrovePostID
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "Test 1: Valid AnthroveUserID and AnthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: validUserID,
anthrovePostID: post.ID,
},
want: true,
wantErr: false,
},
{
name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: validUserID,
anthrovePostID: "qadw",
},
want: false,
wantErr: true,
},
{
name: "Test 3: Valid AnthrovePostID and invalid AnthroveUserID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: post.ID,
},
want: false,
wantErr: true,
},
{
name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: "123456",
},
want: false,
wantErr: true,
},
{
name: "Test 5: No AnthrovePostID given",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: "",
anthrovePostID: "123456",
},
want: false,
wantErr: true,
},
{
name: "Test 6: No anthrovePostID given",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: "",
},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CheckReferenceBetweenUserAndPost(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.anthrovePostID)
if (err != nil) != tt.wantErr {
t.Errorf("CheckIfUserHasPostAsFavorite() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CheckIfUserHasPostAsFavorite() got = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckUserToPostLinkWithNoData(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Test
validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1"))
invalidUserID := models.AnthroveUserID("XXX")
validPostID := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1"))
err = CreateUser(ctx, gormDB, validUserID)
if err != nil {
t.Fatal(err)
}
post := &models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: validPostID,
},
Rating: "safe",
}
err = CreatePost(ctx, gormDB, post)
if err != nil {
t.Fatal(err)
}
err = CreateReferenceBetweenUserAndPost(ctx, gormDB, validUserID, post.ID)
if err != nil {
t.Fatal(err)
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
anthroveUserID models.AnthroveUserID
anthrovePostID models.AnthrovePostID
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "Test 1: Valid AnthroveUserID and AnthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: validUserID,
anthrovePostID: post.ID,
},
want: true,
wantErr: false,
},
{
name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: validUserID,
anthrovePostID: "qadw",
},
want: false,
wantErr: true,
},
{
name: "Test 3: Valid AnthrovePostID and invalid AnthroveUserID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: post.ID,
},
want: false,
wantErr: true,
},
{
name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: "123456",
},
want: false,
wantErr: true,
},
{
name: "Test 5: No AnthrovePostID given",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: "",
anthrovePostID: "123456",
},
want: false,
wantErr: true,
},
{
name: "Test 6: No anthrovePostID given",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: "",
},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CheckReferenceBetweenUserAndPost(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.anthrovePostID)
if (err != nil) != tt.wantErr {
t.Errorf("CheckIfUserHasPostAsFavorite() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CheckIfUserHasPostAsFavorite() got = %v, want %v", got, tt.want)
}
})
}
}
func TestEstablishUserToPostLink(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Test
validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1"))
invalidUserID := models.AnthroveUserID("XXX")
validPostID := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1"))
err = CreateUser(ctx, gormDB, validUserID)
if err != nil {
t.Fatal(err)
}
post := &models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: validPostID,
},
Rating: "safe",
}
err = CreatePost(ctx, gormDB, post)
if err != nil {
t.Fatal(err)
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
anthroveUserID models.AnthroveUserID
anthrovePostID models.AnthrovePostID
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Test 1: Valid AnthroveUserID and AnthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: validUserID,
anthrovePostID: post.ID,
},
wantErr: false,
},
{
name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: validUserID,
anthrovePostID: "123456",
},
wantErr: true,
},
{
name: "Test 3: invalid AnthroveUserID and valid AnthrovePostID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: post.ID,
},
wantErr: true,
},
{
name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: "123456",
},
wantErr: true,
},
{
name: "Test 5: AnthrovePostID is empty",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: invalidUserID,
anthrovePostID: "",
},
wantErr: true,
},
{
name: "Test 6: anthroveUserID is empty",
args: args{
ctx: ctx,
db: gormDB,
anthroveUserID: "",
anthrovePostID: validPostID,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CreateReferenceBetweenUserAndPost(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.anthrovePostID); (err != nil) != tt.wantErr {
t.Errorf("CreateReferenceBetweenUserAndPost() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -0,0 +1,85 @@
package postgres
import (
"context"
"errors"
otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// CreateSource creates a pgModels.Source
func CreateSource(ctx context.Context, db *gorm.DB, anthroveSource *models.Source) error {
if anthroveSource.Domain == "" {
return &otterError.EntityValidationFailed{Reason: "Domain is required"}
}
result := db.WithContext(ctx).Where(models.Source{Domain: anthroveSource.Domain}).FirstOrCreate(anthroveSource)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
if result.RowsAffected == 0 {
return &otterError.NoDataWritten{}
}
log.WithFields(log.Fields{
"node_source_url": anthroveSource.Domain,
"node_source_displayName": anthroveSource.DisplayName,
"node_source_icon": anthroveSource.Icon,
}).Trace("database: created source node")
return nil
}
// GetAllSource returns a list of all pgModels.Source
func GetAllSource(ctx context.Context, db *gorm.DB) ([]models.Source, error) {
var sources []models.Source
result := db.WithContext(ctx).Find(&sources)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tag_amount": result.RowsAffected,
}).Trace("database: get all source nodes")
return sources, nil
}
// GetSourceByDomain returns the first source it finds based on the domain
func GetSourceByDomain(ctx context.Context, db *gorm.DB, sourceDomain models.AnthroveSourceDomain) (*models.Source, error) {
var sources models.Source
if sourceDomain == "" {
return nil, &otterError.EntityValidationFailed{Reason: "AnthroveSourceDomain is not set"}
}
result := db.WithContext(ctx).Where("domain = ?", sourceDomain).First(&sources)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tag_amount": result.RowsAffected,
}).Trace("database: get all source nodes")
return &sources, nil
}

View File

@ -0,0 +1,270 @@
package postgres
import (
"context"
"fmt"
"testing"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"git.dragse.it/anthrove/otter-space-sdk/test"
"gorm.io/gorm"
)
func TestCreateSourceNode(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Test
validPostID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Post1"))
validSource := &models.Source{
BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validPostID},
DisplayName: "e621",
Domain: "e621.net",
Icon: "icon.e621.net",
}
invalidSource := &models.Source{
BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validPostID},
Domain: "notfound.intern",
}
invalidSourceDomain := &models.Source{
BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validPostID},
Domain: "",
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
anthroveSource *models.Source
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Test 1: Valid anthroveSource",
args: args{
ctx: ctx,
db: gormDB,
anthroveSource: validSource,
},
wantErr: false,
},
{
name: "Test 2: inValid anthroveSource",
args: args{
ctx: ctx,
db: gormDB,
anthroveSource: invalidSourceDomain,
},
wantErr: true,
},
{
name: "Test 3: unique anthroveSource",
args: args{
ctx: ctx,
db: gormDB,
anthroveSource: invalidSource,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CreateSource(tt.args.ctx, tt.args.db, tt.args.anthroveSource); (err != nil) != tt.wantErr {
t.Errorf("CreateSource() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGetAllSourceNodes(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Test
sources := []models.Source{
{
DisplayName: "e621",
Domain: "e621.net",
Icon: "icon.e621.net",
},
{
DisplayName: "furaffinity",
Domain: "furaffinity.net",
Icon: "icon.furaffinity.net",
},
{
DisplayName: "fenpaws",
Domain: "fenpa.ws",
Icon: "icon.fenpa.ws",
},
}
for _, source := range sources {
err = CreateSource(ctx, gormDB, &source)
if err != nil {
t.Fatal(err)
}
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
}
tests := []struct {
name string
args args
want []models.Source
wantErr bool
}{
{
name: "Test 1: Get all entries",
args: args{
ctx: ctx,
db: gormDB,
},
want: sources,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetAllSource(tt.args.ctx, tt.args.db)
if (err != nil) != tt.wantErr {
t.Errorf("GetAllSource() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !checkSourcesNode(got, tt.want) {
t.Errorf("GetAllSource() got = %v, want %v", got, tt.want)
}
})
}
}
func TestGetSourceNodesByURL(t *testing.T) {
// Setup trow away container
ctx := context.Background()
container, gormDB, err := test.StartPostgresContainer(ctx)
if err != nil {
t.Fatalf("Could not start PostgreSQL container: %v", err)
}
defer container.Terminate(ctx)
// Setup Test
source := &models.Source{
DisplayName: "e621",
Domain: "e621.net",
Icon: "icon.e621.net",
}
err = CreateSource(ctx, gormDB, source)
if err != nil {
t.Fatal(err)
}
// Test
type args struct {
ctx context.Context
db *gorm.DB
domain models.AnthroveSourceDomain
}
tests := []struct {
name string
args args
want *models.Source
wantErr bool
}{
{
name: "Test 1: Valid URL",
args: args{
ctx: ctx,
db: gormDB,
domain: "e621.net",
},
want: source,
wantErr: false,
},
{
name: "Test 2: Invalid URL",
args: args{
ctx: ctx,
db: gormDB,
domain: "eeeee.net",
},
want: nil,
wantErr: true,
},
{
name: "Test 2: No URL",
args: args{
ctx: ctx,
db: gormDB,
domain: "",
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetSourceByDomain(tt.args.ctx, tt.args.db, tt.args.domain)
if (err != nil) != tt.wantErr {
t.Errorf("GetSourceByDomain() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !checkSourceNode(got, tt.want) {
t.Errorf("GetSourceByDomain() got = %v, want %v", got, tt.want)
}
})
}
}
func checkSourcesNode(got []models.Source, want []models.Source) bool {
for i, source := range want {
if source.DisplayName != got[i].DisplayName {
return false
}
if source.Domain != got[i].Domain {
return false
}
if source.Icon != got[i].Icon {
return false
}
}
return true
}
func checkSourceNode(got *models.Source, want *models.Source) bool {
if want == nil && got == nil {
return true
}
if got.Domain != want.Domain {
return false
}
return true
}

329
internal/postgres/tag.go Normal file
View File

@ -0,0 +1,329 @@
package postgres
import (
"context"
"errors"
otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func CreateTag(ctx context.Context, db *gorm.DB, tagName models.AnthroveTagName, tagType models.TagType) error {
if tagName == "" {
return &otterError.EntityValidationFailed{Reason: "tagName cannot be empty"}
}
if tagType == "" {
return &otterError.EntityValidationFailed{Reason: "tagType cannot be empty"}
}
result := db.WithContext(ctx).Create(&models.Tag{Name: string(tagName), Type: tagType})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
if result.RowsAffected == 0 {
return &otterError.NoDataWritten{}
}
log.WithFields(log.Fields{
"tag_name": tagName,
"tag_type": tagType,
}).Trace("database: created tag node")
return nil
}
func DeleteTag(ctx context.Context, db *gorm.DB, tagName models.AnthroveTagName) error {
if tagName == "" {
return &otterError.EntityValidationFailed{Reason: "tagName cannot be empty"}
}
result := db.WithContext(ctx).Delete(&models.Tag{Name: string(tagName)})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return &otterError.NoDataFound{}
}
return result.Error
}
log.WithFields(log.Fields{
"tag_name": tagName,
}).Trace("database: deleted tag")
return nil
}
func GetAllTagByTagsType(ctx context.Context, db *gorm.DB, tagType models.TagType) ([]models.Tag, error) {
var tags []models.Tag
if tagType == "" {
return nil, &otterError.EntityValidationFailed{Reason: "tagType cannot be empty"}
}
result := db.WithContext(ctx).Model(&models.Tag{}).Where("tag_type = ?", tagType).Scan(&tags)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tags_length": len(tags),
}).Trace("database: got tag")
return tags, nil
}
func CreateTagAndReferenceToPost(ctx context.Context, db *gorm.DB, anthrovePostID models.AnthrovePostID, tag *models.Tag) error {
if anthrovePostID == "" {
return &otterError.EntityValidationFailed{Reason: "anthrovePostID cannot be empty"}
}
if len(anthrovePostID) != 25 {
return &otterError.EntityValidationFailed{Reason: "anthrovePostID needs to be 25 characters long"}
}
if tag == nil {
return &otterError.EntityValidationFailed{Reason: "Tag is nil"}
}
pgPost := models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{
ID: anthrovePostID,
},
}
err := db.WithContext(ctx).Model(&pgPost).Association("Tags").Append(tag)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &otterError.NoDataFound{}
}
return errors.Join(err, &otterError.NoRelationCreated{})
}
log.WithFields(log.Fields{
"anthrove_post_id": anthrovePostID,
"tag_name": tag.Name,
"tag_type": tag.Type,
}).Trace("database: created tag node")
return nil
}
func GetTags(ctx context.Context, db *gorm.DB) ([]models.Tag, error) {
var tags []models.Tag
result := db.WithContext(ctx).Find(&tags)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tag_amount": len(tags),
}).Trace("database: got tags")
return tags, nil
}
func CreateTagAlias(ctx context.Context, db *gorm.DB, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error {
if tagAliasName == "" {
return &otterError.EntityValidationFailed{Reason: "tagAliasName cannot be empty"}
}
if tagID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty}
}
result := db.WithContext(ctx).Create(&models.TagAlias{
Name: string(tagAliasName),
TagID: string(tagID),
})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
log.WithFields(log.Fields{
"tag_alias_name": tagAliasName,
"tag_alias_tag_id": tagID,
}).Trace("database: created tagAlias")
return nil
}
func GetAllTagAlias(ctx context.Context, db *gorm.DB) ([]models.TagAlias, error) {
var tagAliases []models.TagAlias
result := db.WithContext(ctx).Find(&tagAliases)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tag_alias_length": len(tagAliases),
}).Trace("database: created tagAlias")
return tagAliases, nil
}
func GetAllTagAliasByTag(ctx context.Context, db *gorm.DB, tagID models.AnthroveTagID) ([]models.TagAlias, error) {
var tagAliases []models.TagAlias
if tagID == "" {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty}
}
result := db.WithContext(ctx).Where("tag_id = ?", tagID).Find(&tagAliases)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tag_alias_length": len(tagAliases),
"tag_alias_tag_id": tagID,
}).Trace("database: get specific tagAlias")
return tagAliases, nil
}
func DeleteTagAlias(ctx context.Context, db *gorm.DB, tagAliasName models.AnthroveTagAliasName) error {
if tagAliasName == "" {
return &otterError.EntityValidationFailed{Reason: "tagAliasName cannot be empty"}
}
result := db.WithContext(ctx).Delete(&models.TagAlias{Name: string(tagAliasName)})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return &otterError.NoDataFound{}
}
return result.Error
}
log.WithFields(log.Fields{
"tag_alias_name": tagAliasName,
}).Trace("database: deleted tagAlias")
return nil
}
func CreateTagGroup(ctx context.Context, db *gorm.DB, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error {
if tagGroupName == "" {
return &otterError.EntityValidationFailed{Reason: "tagGroupName cannot be empty"}
}
if tagID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty}
}
result := db.WithContext(ctx).Create(&models.TagGroup{
Name: string(tagGroupName),
TagID: string(tagID),
})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
log.WithFields(log.Fields{
"tag_group_name": tagGroupName,
"tag_group_tag_id": tagID,
}).Trace("database: created tagGroup")
return nil
}
func GetAllTagGroup(ctx context.Context, db *gorm.DB) ([]models.TagGroup, error) {
var tagGroups []models.TagGroup
result := db.WithContext(ctx).Find(&tagGroups)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tag_alias_length": len(tagGroups),
}).Trace("database: created tagGroup")
return tagGroups, nil
}
func GetAllTagGroupByTag(ctx context.Context, db *gorm.DB, tagID models.AnthroveTagID) ([]models.TagGroup, error) {
var tagGroups []models.TagGroup
if tagID == "" {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty}
}
result := db.WithContext(ctx).Where("tag_id = ?", tagID).Find(&tagGroups)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"tag_alias_length": len(tagGroups),
"tag_alias_tag_id": tagID,
}).Trace("database: get specific tagGroup")
return tagGroups, nil
}
func DeleteTagGroup(ctx context.Context, db *gorm.DB, tagGroupName models.AnthroveTagGroupName) error {
if tagGroupName == "" {
return &otterError.EntityValidationFailed{Reason: "tagGroupName cannot be empty"}
}
result := db.WithContext(ctx).Delete(&models.TagGroup{Name: string(tagGroupName)})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return &otterError.NoDataFound{}
}
return result.Error
}
log.WithFields(log.Fields{
"tag_alias_name": tagGroupName,
}).Trace("database: deleted tagAlias")
return nil
}

File diff suppressed because it is too large Load Diff

421
internal/postgres/user.go Normal file
View File

@ -0,0 +1,421 @@
package postgres
import (
"context"
"errors"
"time"
otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
gonanoid "github.com/matoous/go-nanoid/v2"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// Workaround, should be changed later maybe, but its not that bad right now
type selectFrequencyTag struct {
tagName string `gorm:"tag_name"`
count int64 `gorm:"count"`
tagType models.TagType `gorm:"tag_type"`
}
func CreateUser(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) error {
if anthroveUserID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
user := models.User{
BaseModel: models.BaseModel[models.AnthroveUserID]{
ID: anthroveUserID,
},
}
result := db.WithContext(ctx).FirstOrCreate(&user)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
return nil
}
func CreateUserWithRelationToSource(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, accountId string, accountUsername string) error {
if anthroveUserID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if accountId == "" {
return &otterError.EntityValidationFailed{Reason: "accountID cannot be empty"}
}
if accountUsername == "" {
return &otterError.EntityValidationFailed{Reason: "accountUsername cannot be empty"}
}
validationCode, err := gonanoid.New(25)
if err != nil {
return err
}
result := db.WithContext(ctx).Exec(`WITH userObj AS (
INSERT INTO "User" (id)
VALUES ($1)
ON CONFLICT (id) DO NOTHING
)
INSERT INTO "UserSource" (user_id, source_id, account_username, account_id, account_validate, account_validation_key)
SELECT $2, source.id, $3, $4, false, $5
FROM "Source" AS source
WHERE source.id = $6;`, anthroveUserID, anthroveUserID, accountUsername, accountId, validationCode, sourceID)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return &otterError.EntityAlreadyExists{}
}
return result.Error
}
if result.RowsAffected == 0 {
return &otterError.NoDataWritten{}
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"source_id": sourceID,
"account_username": accountUsername,
"account_id": accountId,
}).Info("database: created user-source relationship")
return nil
}
func GetUserFavoritesCount(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) (int64, error) {
var count int64
if anthroveUserID == "" {
return 0, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return 0, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
result := db.WithContext(ctx).Model(&models.UserFavorites{}).Where("user_id = ?", string(anthroveUserID)).Count(&count)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return 0, &otterError.NoDataFound{}
}
return 0, result.Error
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"anthrove_user_fav_count": count,
}).Trace("database: got user favorite count")
return count, nil
}
func GetUserSourceLinks(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) (map[string]models.UserSource, error) {
var userSources []models.UserSource
userSourceMap := make(map[string]models.UserSource)
if anthroveUserID == "" {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
result := db.WithContext(ctx).Model(&models.UserSource{}).Where("user_id = ?", string(anthroveUserID)).Find(&userSources)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
for _, userSource := range userSources {
var source models.Source
result = db.WithContext(ctx).Model(&models.Source{}).Where("id = ?", userSource.SourceID).First(&source)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
userSourceMap[source.DisplayName] = models.UserSource{
UserID: userSource.AccountID,
AccountUsername: userSource.AccountUsername,
Source: models.Source{
DisplayName: source.DisplayName,
Domain: source.Domain,
Icon: source.Icon,
},
}
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
}).Trace("database: got user source link")
return userSourceMap, nil
}
func GetUserSourceBySourceID(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID) (*models.UserSource, error) {
var userSource models.UserSource
if anthroveUserID == "" {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if sourceID == "" {
return nil, &otterError.EntityValidationFailed{Reason: "sourceID cannot be empty"}
}
if len(sourceID) != 25 {
return nil, &otterError.EntityValidationFailed{Reason: "sourceID needs to be 25 characters long"}
}
result := db.WithContext(ctx).Model(&models.UserSource{}).InnerJoins("Source", db.Where("id = ?", sourceID)).Where("user_id = ?", string(anthroveUserID)).First(&userSource)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"source_id": sourceID,
}).Trace("database: got specified user source link")
return &userSource, nil
}
func GetAllUsers(ctx context.Context, db *gorm.DB) ([]models.User, error) {
var users []models.User
result := db.WithContext(ctx).Model(&models.User{}).Find(&users)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, result.Error
}
log.WithFields(log.Fields{
"anthrove_user_id_count": len(users),
}).Trace("database: got all anthrove user IDs")
return users, nil
}
func GetUserFavoriteWithPagination(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) {
var userFavorites []models.UserFavorites
var favoritePosts []models.Post
if anthroveUserID == "" {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
err := db.WithContext(ctx).Model(&models.UserFavorites{}).Where("user_id = ?", string(anthroveUserID)).Offset(skip).Limit(limit).Find(&userFavorites).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, err
}
for _, userFavorite := range userFavorites {
var post models.Post
err = db.WithContext(ctx).Model(&models.Post{}).Where("id = ?", userFavorite.PostID).First(&post).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, err
}
favoritePosts = append(favoritePosts,
models.Post{
BaseModel: models.BaseModel[models.AnthrovePostID]{ID: post.ID},
Rating: post.Rating,
})
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"anthrove_user_fav_count": len(favoritePosts),
}).Trace("database: got all anthrove user favorites")
return &models.FavoriteList{Posts: favoritePosts}, nil
}
func GetUserTagWitRelationToFavedPosts(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) {
var queryUserFavorites []selectFrequencyTag
if anthroveUserID == "" {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
rows, err := db.WithContext(ctx).Raw(
`WITH user_posts AS (
SELECT post_id FROM "UserFavorites" WHERE user_id = $1
)
SELECT post_tags.tag_name AS tag_name, count(*) AS count, (SELECT tag_type FROM "Tag" WHERE "Tag".name = post_tags.tag_name LIMIT 1) AS tag_type FROM post_tags, user_posts WHERE post_tags.post_id IN (user_posts.post_id) GROUP BY post_tags.tag_name ORDER BY tag_type DESC, tag_name DESC`, anthroveUserID).Rows()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, &otterError.NoDataFound{}
}
return nil, err
}
var userFavoritesFrequency = make([]models.TagsWithFrequency, 0)
defer rows.Close()
for rows.Next() {
var tagName string
var count int64
var tagType string
rows.Scan(&tagName, &count, &tagType)
userFavoritesFrequency = append(userFavoritesFrequency, models.TagsWithFrequency{
Frequency: count,
Tags: models.Tag{
Name: tagName,
Type: models.TagType(tagType),
},
})
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"tag_amount": len(queryUserFavorites),
}).Trace("database: got user tag node with relation to faved posts")
return userFavoritesFrequency, nil
}
func UpdateUserSourceScrapeTimeInterval(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, scrapeTime models.AnthroveScrapeTimeInterval) error {
if anthroveUserID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if sourceID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDEmpty}
}
if len(sourceID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDToShort}
}
if scrapeTime == 0 {
return &otterError.EntityValidationFailed{Reason: "ScrapeTimeInterval cannot be empty"}
}
userSource := &models.UserSource{
UserID: string(anthroveUserID),
}
result := db.WithContext(ctx).Model(&userSource).Update("scrape_time_interval", scrapeTime)
if result.Error != nil {
return result.Error
}
return nil
}
func UpdateUserSourceLastScrapeTime(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, lastScrapeTime models.AnthroveUserLastScrapeTime) error {
if anthroveUserID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if sourceID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDEmpty}
}
if len(sourceID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDToShort}
}
if time.Time.IsZero(time.Time(lastScrapeTime)) {
return &otterError.EntityValidationFailed{Reason: "LastScrapeTime cannot be empty"}
}
userSource := &models.UserSource{
UserID: string(anthroveUserID),
}
result := db.WithContext(ctx).Model(&userSource).Update("last_scrape_time", time.Time(lastScrapeTime))
if result.Error != nil {
return result.Error
}
return nil
}
func UpdateUserSourceValidation(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, valid bool) error {
if anthroveUserID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
if sourceID == "" {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDEmpty}
}
if len(sourceID) != 25 {
return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDToShort}
}
userSource := &models.UserSource{
UserID: string(anthroveUserID),
}
result := db.WithContext(ctx).Model(&userSource).Update("account_validate", valid)
if result.Error != nil {
return result.Error
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +0,0 @@
package internal
import (
"context"
"fmt"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
log "github.com/sirupsen/logrus"
)
func EstablishAnthrovePostToSourceLink(ctx context.Context, driver neo4j.DriverWithContext, anthrovePostID models.AnthrovePostID, anthroveSourceDomain string, anthrovePostRelationship *models.AnthrovePostRelationship) error {
query := `
MATCH (sourceNode:Source {domain: $source_url})
MATCH (postNode:AnthrovePost {post_id: $anthrove_post_id})
MERGE (sourceNode)-[:REFERENCE {url: $source_post_url, source_post_id: $source_post_id}]->(postNode)
`
params := map[string]any{
"source_url": anthroveSourceDomain,
"anthrove_post_id": anthrovePostID,
"source_post_url": anthrovePostRelationship.Url,
"source_post_id": anthrovePostRelationship.PostID,
}
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return err
}
log.WithFields(log.Fields{
"source_url": anthroveSourceDomain,
"anthrove_post_id": anthrovePostID,
"source_post_url": anthrovePostRelationship.Url,
"source_post_id": anthrovePostRelationship.PostID,
}).Trace("graph: creating anthrove post to source link")
return nil
}
func EstablishUserToPostLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUser *models.AnthroveUser, anthrovePost *models.AnthrovePost) error {
query := `
MATCH (user:User {user_id: $anthrove_user_id})
MATCH (anthrovePost:AnthrovePost {post_id: $anthrove_post_id})
MERGE (user)-[:FAV]->(anthrovePost)
`
params := map[string]any{
"anthrove_post_id": anthrovePost.PostID,
"anthrove_user_id": anthroveUser.UserID,
}
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return err
}
log.WithFields(log.Fields{
"anthrove_post_id": anthrovePost.PostID,
"anthrove_user_id": anthroveUser.UserID,
}).Trace("graph: created user to post link")
return nil
}
func CheckUserToPostLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, sourcePostID string, sourceUrl string) (bool, error) {
query := `
OPTIONAL MATCH (:User {user_id: $anthrove_user_id})-[f:FAV]->(:AnthrovePost)<-[:REFERENCE{source_post_id: $source_post_id}]-(:Source{domain: $source_domain})
RETURN COUNT(f) > 0 AS hasRelationship
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
"source_post_id": sourcePostID,
"source_domain": sourceUrl,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return false, err
}
if len(result.Records) == 0 {
return false, fmt.Errorf("no records found")
}
exists, _, err := neo4j.GetRecordValue[bool](result.Records[0], "hasRelationship")
if err != nil {
return false, err
}
log.WithFields(log.Fields{
"relationship_exists": exists,
"relationship_anthrove_user_id": anthroveUserID,
"relationship_e621_post_id": "",
}).Trace("graph: checked user post relationship")
return exists, nil
}

View File

@ -1,113 +0,0 @@
package internal
import (
"context"
"fmt"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
log "github.com/sirupsen/logrus"
)
func CreateSourceNode(ctx context.Context, driver neo4j.DriverWithContext, anthroveSource *models.AnthroveSource) error {
query := `
MERGE (sourceNode:Source {domain: $source_url})
ON CREATE SET sourceNode.domain = $source_url, sourceNode.display_name = $source_display_name, sourceNode.icon = $source_icon
`
params := map[string]any{
"source_url": anthroveSource.Domain,
"source_display_name": anthroveSource.DisplayName,
"source_icon": anthroveSource.Icon,
}
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return fmt.Errorf("graph: %w", err)
}
log.WithFields(log.Fields{
"node_source_url": anthroveSource.Domain,
"node_source_displayName": anthroveSource.DisplayName,
"node_source_icon": anthroveSource.Icon,
}).Trace("graph: created source node")
return nil
}
func GetAllSourceNodes(ctx context.Context, driver neo4j.DriverWithContext) ([]models.AnthroveSource, error) {
var sources []models.AnthroveSource
query := `
MATCH (s:Source)
RETURN s as source
`
params := map[string]any{}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, nil
}
for i := range result.Records {
record := result.Records[i]
source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "source")
if err != nil {
return nil, err
}
sources = append(sources, models.AnthroveSource{
DisplayName: source.Props["display_name"].(string),
Domain: source.Props["domain"].(string),
Icon: source.Props["icon"].(string),
})
}
log.WithFields(log.Fields{
"tag_amount": len(sources),
}).Trace("graph: created tag node")
return sources, nil
}
func GetSourceNodesByURL(ctx context.Context, driver neo4j.DriverWithContext, sourceUrl string) (*models.AnthroveSource, error) {
var source models.AnthroveSource
query := `
MATCH (s:Source {domain: $source_url})
RETURN s as source
`
params := map[string]any{
"source_url": sourceUrl,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, fmt.Errorf("source not found")
}
record, _, err := neo4j.GetRecordValue[neo4j.Node](result.Records[0], "source")
if err != nil {
return nil, err
}
source.DisplayName = record.Props["display_name"].(string)
source.Domain = record.Props["domain"].(string)
source.Icon = record.Props["icon"].(string)
log.WithFields(log.Fields{
"source_url": sourceUrl,
}).Trace("graph: got source node")
return &source, nil
}

View File

@ -1,84 +0,0 @@
package internal
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
log "github.com/sirupsen/logrus"
)
func CreateTagNodeWitRelation(ctx context.Context, driver neo4j.DriverWithContext, anthrovePostID models.AnthrovePostID, anthroveTag *models.AnthroveTag) error {
query := `
MATCH (anthrovePost:AnthrovePost {post_id: $anthrove_post_id})
MERGE (tagNode:Tag {name: $tag_name, type: $tag_type})
MERGE (anthrovePost)-[:HAS]->(tagNode)
`
params := map[string]interface{}{
"tag_name": anthroveTag.Name,
"tag_type": anthroveTag.Type,
"anthrove_post_id": anthrovePostID,
}
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return err
}
log.WithFields(log.Fields{
"anthrove_post_id": anthrovePostID,
"tag_name": anthroveTag.Name,
"tag_type": anthroveTag.Type,
}).Trace("graph: created tag node")
return nil
}
func GetTags(ctx context.Context, driver neo4j.DriverWithContext) ([]models.TagsWithFrequency, error) {
var userTags []models.TagsWithFrequency
query := `
MATCH (:AnthrovePost)-[:HAS]->(t:Tag)
RETURN t as tag, COUNT(t) AS frequency
ORDER BY frequency DESC
`
params := map[string]any{}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, nil
}
for i := range result.Records {
record := result.Records[i]
tag, _, err := neo4j.GetRecordValue[neo4j.Node](record, "tag")
if err != nil {
return nil, err
}
frequency, _, err := neo4j.GetRecordValue[int64](record, "frequency")
if err != nil {
return nil, err
}
userTags = append(userTags, models.TagsWithFrequency{
Frequency: frequency,
Tags: models.AnthroveTag{
Name: tag.Props["name"].(string),
Type: tag.Props["type"].(string),
},
})
}
log.WithFields(log.Fields{
"tag_amount": len(userTags),
}).Trace("graph: created tag node")
return userTags, nil
}

View File

@ -1,454 +0,0 @@
package internal
import (
"context"
"fmt"
"git.dragse.it/anthrove/otter-space-sdk/internal/utils"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
log "github.com/sirupsen/logrus"
)
func CreateUserNodeWithSourceRelation(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, sourceDomain string, userID string, username string) error {
query := `
MATCH (userNode:User {user_id: $anthrove_user_id})
MATCH (sourceNode:Source {domain: $source_domain})
MERGE (userNode)-[r:HAS_ACCOUNT_AT {username: $source_user_name, user_id: $source_user_id}]->(sourceNode)
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
"source_user_id": userID,
"source_user_name": username,
"source_domain": sourceDomain,
}
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return err
}
var anthroveUserRelationship []models.AnthroveUserRelationship
anthroveUserRelationship = append(anthroveUserRelationship, models.AnthroveUserRelationship{
UserID: userID,
Username: username,
ScrapeTimeInterval: "",
Source: models.AnthroveSource{
DisplayName: "",
Domain: sourceDomain,
Icon: "",
},
})
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"source_user_id": userID,
"source_user_name": username,
"source_domain": sourceDomain,
}).Trace("graph: crated user with relationship")
return nil
}
func GetUserFavoritesCount(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) (int64, error) {
var userFavoriteCount int64
query := `
MATCH (userNode:User {user_id: $anthrove_user_id})
MATCH (userNode)-[:FAV]->(favPost:AnthrovePost)
MATCH (sourceNode)-[:REFERENCE]->(favPost)
RETURN count( DISTINCT favPost) AS FavoritePostsCount
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return 0, err
}
if len(result.Records) == 0 {
// no matches -> user does not exist, return count 0
return userFavoriteCount, err
}
record := result.Records[0]
userFavoriteCount, _, err = neo4j.GetRecordValue[int64](record, "FavoritePostsCount")
if err != nil {
return userFavoriteCount, err
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"anthrove_user_fav_count": userFavoriteCount,
}).Trace("graph: got user favorite count")
return userFavoriteCount, nil
}
func GetUserSourceLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) (map[string]models.AnthroveUserRelationship, error) {
userSource := make(map[string]models.AnthroveUserRelationship)
query := `
MATCH (user:User{user_id: $anthrove_user_id})-[r:HAS_ACCOUNT_AT]->(s:Source)
RETURN toString(r.user_id) AS sourceUserID, toString(r.username) AS sourceUsername, s as source;
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, fmt.Errorf("user has no relations")
}
for i := range result.Records {
record := result.Records[i]
source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "source")
if err != nil {
return nil, err
}
sourceUserID, _, err := neo4j.GetRecordValue[string](record, "sourceUserID")
if err != nil {
return nil, err
}
sourceUsername, _, err := neo4j.GetRecordValue[string](record, "sourceUsername")
if err != nil {
return nil, err
}
displayName := source.Props["display_name"].(string)
domain := source.Props["domain"].(string)
icon := source.Props["icon"].(string)
anthroveSourceUser := models.AnthroveUserRelationship{
UserID: sourceUserID,
Username: sourceUsername,
Source: models.AnthroveSource{
DisplayName: displayName,
Domain: domain,
Icon: icon,
},
}
userSource[displayName] = anthroveSourceUser
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"anthrove_data": userSource,
}).Trace("graph: got user favorite count")
return userSource, nil
}
func GetSpecifiedUserSourceLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, sourceDisplayName string) (map[string]models.AnthroveUserRelationship, error) {
userSource := make(map[string]models.AnthroveUserRelationship)
query := `
MATCH (user:User{user_id: $anthrove_user_id})-[r:HAS_ACCOUNT_AT]->(s:Source{display_name: $source_display_name})
RETURN toString(r.user_id) AS sourceUserID, toString(r.username) AS sourceUsername, s as source;
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
"source_display_name": sourceDisplayName,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, fmt.Errorf("user has no relations with the source %s", sourceDisplayName)
}
for i := range result.Records {
record := result.Records[i]
source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "source")
if err != nil {
return nil, err
}
sourceUserID, _, err := neo4j.GetRecordValue[string](record, "sourceUserID")
if err != nil {
return nil, err
}
sourceUsername, _, err := neo4j.GetRecordValue[string](record, "sourceUsername")
if err != nil {
return nil, err
}
displayName := source.Props["display_name"].(string)
domain := source.Props["domain"].(string)
icon := source.Props["icon"].(string)
anthroveSourceUser := models.AnthroveUserRelationship{
UserID: sourceUserID,
Username: sourceUsername,
Source: models.AnthroveSource{
DisplayName: displayName,
Domain: domain,
Icon: icon,
},
}
userSource[displayName] = anthroveSourceUser
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
"anthrove_data": userSource,
}).Trace("graph: got user favorite count")
return userSource, nil
}
func GetAnthroveUser(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) (*models.AnthroveUser, error) {
var err error
var anthroveUser models.AnthroveUser
var userSources models.AnthroveSource
userRelationships := make([]models.AnthroveUserRelationship, 0)
query := `
MATCH (user:User{user_id: $anthrove_user_id})-[relation:HAS_ACCOUNT_AT]->(source:Source)
RETURN user as User, relation as Relation, source as Source;
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, fmt.Errorf("user has no relations")
}
for i := range result.Records {
record := result.Records[i]
user, _, err := neo4j.GetRecordValue[neo4j.Node](record, "User")
if err != nil {
return nil, err
}
relation, _, err := neo4j.GetRecordValue[neo4j.Relationship](record, "Relation")
if err != nil {
return nil, err
}
source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "Source")
if err != nil {
return nil, err
}
userRelationships = append(userRelationships, models.AnthroveUserRelationship{
UserID: fmt.Sprintf("%v", utils.GetOrDefault(relation.Props, "user_id", "")),
Username: utils.GetOrDefault(relation.Props, "username", "").(string),
ScrapeTimeInterval: utils.GetOrDefault(relation.Props, "scrape_time_interval", "").(string),
})
userSources = models.AnthroveSource{
DisplayName: utils.GetOrDefault(source.Props, "display_name", "").(string),
Domain: utils.GetOrDefault(source.Props, "domain", "").(string),
Icon: utils.GetOrDefault(source.Props, "icon", "").(string),
}
anthroveUser.UserID = models.AnthroveUserID(utils.GetOrDefault(user.Props, "user_id", "").(string))
anthroveUser.Relationship = userRelationships
for j := range userRelationships {
anthroveUser.Relationship[j].Source = userSources
}
}
log.WithFields(log.Fields{
"anthrove_user_id": anthroveUserID,
}).Trace("graph: got anthrove user")
return &anthroveUser, nil
}
func GetAllAnthroveUserIDs(ctx context.Context, driver neo4j.DriverWithContext) ([]models.AnthroveUserID, error) {
var err error
var anthroveUsers []models.AnthroveUserID
query := `
MATCH (anthroveUser:User)
RETURN anthroveUser
`
result, err := neo4j.ExecuteQuery(ctx, driver, query, nil, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
log.Warnf("No users found, this should not be happening!")
return []models.AnthroveUserID{}, nil
}
for i := range result.Records {
record := result.Records[i]
user, _, err := neo4j.GetRecordValue[neo4j.Node](record, "anthroveUser")
if err != nil {
return nil, err
}
anthroveUsers = append(anthroveUsers, models.AnthroveUserID(fmt.Sprintf(user.Props["user_id"].(string))))
}
log.WithFields(log.Fields{
"anthrove_user_id_count": len(anthroveUsers),
}).Trace("graph: got al anthrove user IDs")
return anthroveUsers, nil
}
func GetUserFavoriteNodeWithPagination(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) {
var err error
var favoritePosts []models.FavoritePost
query := `
CALL {
MATCH (user:User{user_id: $anthrove_user_id})-[r:FAV]->(p:AnthrovePost)
RETURN p.post_id AS post_id
ORDER BY id(p) ASC
SKIP $skip
LIMIT $limit
}
WITH collect(post_id) AS faves
MATCH (a:AnthrovePost)<-[r:REFERENCE]-(s:Source)
WHERE a.post_id in faves
RETURN a AS anthrovePost, r AS postRelation, s AS Source
ORDER BY id(a) ASC
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
"limit": limit,
"skip": skip,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, nil
}
for i := range result.Records {
record := result.Records[i]
anthrovePost, _, err := neo4j.GetRecordValue[neo4j.Node](record, "anthrovePost")
if err != nil {
return nil, err
}
postRelation, _, err := neo4j.GetRecordValue[neo4j.Relationship](record, "postRelation")
if err != nil {
return nil, err
}
source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "Source")
if err != nil {
return nil, err
}
if len(favoritePosts) != 0 && favoritePosts[len(favoritePosts)-1].AnthrovePost.PostID == models.AnthrovePostID(anthrovePost.Props["post_id"].(string)) {
favoritePosts[len(favoritePosts)-1].Relations = append(favoritePosts[len(favoritePosts)-1].Relations, models.FavoriteRelations{
SourcesID: source.Props["display_name"].(string),
Relations: models.AnthrovePostRelationship{
PostID: postRelation.Props["source_post_id"].(string),
Url: postRelation.Props["url"].(string),
},
})
} else {
favoritePosts = append(favoritePosts, models.FavoritePost{
AnthrovePost: models.AnthrovePost{
PostID: models.AnthrovePostID(anthrovePost.Props["post_id"].(string)),
Rating: models.AnthroveRating(anthrovePost.Props["rating"].(string)),
},
Relations: []models.FavoriteRelations{{
SourcesID: source.Props["display_name"].(string),
Relations: models.AnthrovePostRelationship{
PostID: postRelation.Props["source_post_id"].(string),
Url: postRelation.Props["url"].(string),
},
}},
})
}
}
log.WithFields(log.Fields{
"anthrove_user_fav_count": len(favoritePosts),
}).Trace("graph: got al anthrove user favorites")
return &models.FavoriteList{Posts: favoritePosts}, nil
}
func GetUserTagNodeWitRelationToFavedPosts(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) {
var userTags []models.TagsWithFrequency
query := `
MATCH (u:User {user_id: $anthrove_user_id})-[:FAV]->(:AnthrovePost)-[:HAS]->(t:Tag)
RETURN t as tag, COUNT(t) AS frequency
ORDER BY frequency DESC
`
params := map[string]any{
"anthrove_user_id": anthroveUserID,
}
result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer)
if err != nil {
return nil, err
}
if len(result.Records) == 0 {
return nil, nil
}
for i := range result.Records {
record := result.Records[i]
tag, _, err := neo4j.GetRecordValue[neo4j.Node](record, "tag")
if err != nil {
return nil, err
}
frequency, _, err := neo4j.GetRecordValue[int64](record, "frequency")
if err != nil {
return nil, err
}
userTags = append(userTags, models.TagsWithFrequency{
Frequency: frequency,
Tags: models.AnthroveTag{
Name: tag.Props["name"].(string),
Type: tag.Props["type"].(string),
},
})
}
log.WithFields(log.Fields{
"tag_amount": len(userTags),
}).Trace("graph: created tag node")
return userTags, nil
}

View File

@ -0,0 +1,60 @@
package utils
import (
"reflect"
"testing"
)
func TestGetOrDefault(t *testing.T) {
type args struct {
data map[string]any
key string
defaultVal any
}
tests := []struct {
name string
args args
want any
}{
{
name: "Test 1: Nil map",
args: args{
data: nil,
key: "key1",
defaultVal: "default",
},
want: "default",
},
{
name: "Test 2: Existing key",
args: args{
data: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
key: "key1",
defaultVal: "default",
},
want: "value1",
},
{
name: "Test 3: Non-existing key",
args: args{
data: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
key: "key3",
defaultVal: "default",
},
want: "default",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetOrDefault(tt.args.data, tt.args.key, tt.args.defaultVal); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetOrDefault() = %v, want %v", got, tt.want)
}
})
}
}

30
pkg/database/database.go Normal file
View File

@ -0,0 +1,30 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
type OtterSpace interface {
// Connect establishes a connection to the database.
Connect(ctx context.Context, config models.DatabaseConfig) error
// Post contains all function that are needed to manage Posts
Post
// User contains all function that are needed to manage the AnthroveUser
User
// Source contains all function that are needed to manage the Source
Source
// Tag contains all functions that are used to manage Tag
Tag
// TagAlias contains all function that are needed to manage the TagAlias
TagAlias
// TagGroup contains all function that are needed to manage the TagGroup
TagGroup
}

View File

@ -0,0 +1,109 @@
-- +migrate Up
CREATE TYPE Rating AS ENUM (
'safe',
'questionable',
'explicit'
);
CREATE TYPE TagType AS ENUM (
'general',
'species',
'character',
'artist',
'lore',
'meta',
'invalid',
'copyright'
);
CREATE TABLE "Post"
(
id CHAR(25) PRIMARY KEY,
rating Rating,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL NULL
);
CREATE TABLE "Source"
(
id CHAR(25) PRIMARY KEY,
display_name TEXT NULL,
icon TEXT NULL,
domain TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL
);
CREATE TABLE "Tag"
(
name TEXT PRIMARY KEY,
tag_type TagType,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL
);
CREATE TABLE "User"
(
id TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL
);
CREATE TABLE "PostReference"
(
post_id TEXT REFERENCES "Post" (id),
source_id TEXT REFERENCES "Source" (id),
url TEXT NOT NULL,
full_file_url TEXT,
preview_file_url TEXT,
sample_file_url TEXT,
source_post_id TEXT,
PRIMARY KEY (post_id, source_id, url)
);
CREATE TABLE "TagAlias"
(
name TEXT PRIMARY KEY,
tag_id TEXT REFERENCES "Tag" (name),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE "TagGroup"
(
name TEXT PRIMARY KEY,
tag_id TEXT REFERENCES "Tag" (name),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE "UserFavorites"
(
user_id TEXT REFERENCES "User" (id),
post_id TEXT REFERENCES "Post" (id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, post_id)
);
CREATE TABLE "UserSource"
(
user_id TEXT REFERENCES "User" (id),
source_id TEXT REFERENCES "Source" (id),
scrape_time_interval INT,
account_username TEXT,
account_id TEXT,
last_scrape_time TIMESTAMP,
account_validate BOOL DEFAULT FALSE,
account_validation_key CHAR(25),
PRIMARY KEY (user_id, source_id),
UNIQUE (source_id, account_username, account_id)
);
CREATE TABLE "post_tags"
(
post_id TEXT REFERENCES "Post" (id),
tag_name TEXT REFERENCES "Tag" (name),
PRIMARY KEY (post_id, tag_name)
);

28
pkg/database/post.go Normal file
View File

@ -0,0 +1,28 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
type Post interface {
// CreatePost adds a new post to the database.
CreatePost(ctx context.Context, anthrovePost *models.Post) error
// GetPostByAnthroveID retrieves a post by its Anthrove ID.
GetPostByAnthroveID(ctx context.Context, anthrovePostID models.AnthrovePostID) (*models.Post, error)
// GetPostByURL retrieves a post by its source URL.
GetPostByURL(ctx context.Context, postURL string) (*models.Post, error)
// GetPostBySourceID retrieves a post by its source ID.
GetPostBySourceID(ctx context.Context, sourceID models.AnthroveSourceID) (*models.Post, error)
// CreatePostWithReferenceToTagAnd adds a tag with a relation to a post.
CreatePostWithReferenceToTagAnd(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.Tag) error
// CreatePostReference links a post with a source.
CreatePostReference(ctx context.Context, anthrovePostID models.AnthrovePostID, sourceDomain models.AnthroveSourceDomain, postURL models.AnthrovePostURL, config models.PostReferenceConfig) error
}

244
pkg/database/postgres.go Normal file
View File

@ -0,0 +1,244 @@
package database
import (
"context"
"embed"
"fmt"
log2 "log"
"os"
"time"
"git.dragse.it/anthrove/otter-space-sdk/internal/postgres"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
_ "github.com/lib/pq"
migrate "github.com/rubenv/sql-migrate"
log "github.com/sirupsen/logrus"
gormPostgres "gorm.io/driver/postgres"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
)
//go:embed migrations/*.sql
var embedMigrations embed.FS
type postgresqlConnection struct {
db *gorm.DB
debug bool
}
func NewPostgresqlConnection() OtterSpace {
return &postgresqlConnection{
db: nil,
}
}
func (p *postgresqlConnection) Connect(_ context.Context, config models.DatabaseConfig) error {
var localSSL string
var logLevel gormLogger.LogLevel
if config.SSL {
localSSL = "require"
} else {
localSSL = "disable"
}
p.debug = config.Debug
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s", config.Endpoint, config.Username, config.Password, config.Database, config.Port, localSSL, config.Timezone)
var err error
if p.debug {
logLevel = gormLogger.Info
} else {
logLevel = gormLogger.Silent
}
dbLogger := gormLogger.New(log2.New(os.Stdout, "\r\n", log2.LstdFlags), gormLogger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logLevel,
IgnoreRecordNotFoundError: true,
Colorful: true,
})
db, err := gorm.Open(gormPostgres.Open(dsn), &gorm.Config{
Logger: dbLogger,
})
p.db = db
if err != nil {
return err
}
log.Infof("OtterSpace: database connection established")
err = p.migrateDatabase(db)
if err != nil {
return err
}
log.Infof("OtterSpace: migration compleate")
return nil
}
func (p *postgresqlConnection) CreateUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, accountId string, accountUsername string) error {
return postgres.CreateUserWithRelationToSource(ctx, p.db, anthroveUserID, sourceID, accountId, accountUsername)
}
func (p *postgresqlConnection) CreateSource(ctx context.Context, anthroveSource *models.Source) error {
return postgres.CreateSource(ctx, p.db, anthroveSource)
}
func (p *postgresqlConnection) CreatePost(ctx context.Context, anthrovePost *models.Post) error {
return postgres.CreatePost(ctx, p.db, anthrovePost)
}
func (p *postgresqlConnection) CreatePostWithReferenceToTagAnd(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.Tag) error {
return postgres.CreateTagAndReferenceToPost(ctx, p.db, anthrovePostID, anthroveTag)
}
func (p *postgresqlConnection) CreatePostReference(ctx context.Context, anthrovePostID models.AnthrovePostID, sourceDomain models.AnthroveSourceDomain, postURL models.AnthrovePostURL, config models.PostReferenceConfig) error {
return postgres.CreateReferenceBetweenPostAndSource(ctx, p.db, anthrovePostID, sourceDomain, postURL, config)
}
func (p *postgresqlConnection) CreateReferenceBetweenUserAndPost(ctx context.Context, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) error {
return postgres.CreateReferenceBetweenUserAndPost(ctx, p.db, anthroveUserID, anthrovePostID)
}
func (p *postgresqlConnection) CheckIfUserHasPostAsFavorite(ctx context.Context, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) (bool, error) {
return postgres.CheckReferenceBetweenUserAndPost(ctx, p.db, anthroveUserID, anthrovePostID)
}
func (p *postgresqlConnection) GetPostByAnthroveID(ctx context.Context, anthrovePostID models.AnthrovePostID) (*models.Post, error) {
return postgres.GetPostByAnthroveID(ctx, p.db, anthrovePostID)
}
func (p *postgresqlConnection) GetPostByURL(ctx context.Context, sourceUrl string) (*models.Post, error) {
return postgres.GetPostBySourceURL(ctx, p.db, sourceUrl)
}
func (p *postgresqlConnection) GetPostBySourceID(ctx context.Context, sourceID models.AnthroveSourceID) (*models.Post, error) {
return postgres.GetPostBySourceID(ctx, p.db, sourceID)
}
func (p *postgresqlConnection) GetUserFavoritesCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error) {
return postgres.GetUserFavoritesCount(ctx, p.db, anthroveUserID)
}
func (p *postgresqlConnection) GetAllUserSources(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.UserSource, error) {
return postgres.GetUserSourceLinks(ctx, p.db, anthroveUserID)
}
func (p *postgresqlConnection) GetUserSourceBySourceID(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID) (*models.UserSource, error) {
return postgres.GetUserSourceBySourceID(ctx, p.db, anthroveUserID, sourceID)
}
func (p *postgresqlConnection) GetAllUsers(ctx context.Context) ([]models.User, error) {
return postgres.GetAllUsers(ctx, p.db)
}
func (p *postgresqlConnection) GetAllUserFavoritesWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) {
return postgres.GetUserFavoriteWithPagination(ctx, p.db, anthroveUserID, skip, limit)
}
func (p *postgresqlConnection) GetAllTagsFromUser(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) {
return postgres.GetUserTagWitRelationToFavedPosts(ctx, p.db, anthroveUserID)
}
func (p *postgresqlConnection) GetAllTags(ctx context.Context) ([]models.Tag, error) {
return postgres.GetTags(ctx, p.db)
}
func (p *postgresqlConnection) GetAllSources(ctx context.Context) ([]models.Source, error) {
return postgres.GetAllSource(ctx, p.db)
}
func (p *postgresqlConnection) GetSourceByDomain(ctx context.Context, sourceDomain models.AnthroveSourceDomain) (*models.Source, error) {
return postgres.GetSourceByDomain(ctx, p.db, sourceDomain)
}
// NEW FUNCTIONS
func (p *postgresqlConnection) UpdateUserSourceScrapeTimeInterval(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, scrapeTime models.AnthroveScrapeTimeInterval) error {
return postgres.UpdateUserSourceScrapeTimeInterval(ctx, p.db, anthroveUserID, sourceID, scrapeTime)
}
func (p *postgresqlConnection) UpdateUserSourceLastScrapeTime(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, lastScrapeTime models.AnthroveUserLastScrapeTime) error {
return postgres.UpdateUserSourceLastScrapeTime(ctx, p.db, anthroveUserID, sourceID, lastScrapeTime)
}
func (p *postgresqlConnection) UpdateUserSourceValidation(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, valid bool) error {
return postgres.UpdateUserSourceValidation(ctx, p.db, anthroveUserID, sourceID, valid)
}
func (p *postgresqlConnection) CreateTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error {
return postgres.CreateTagAlias(ctx, p.db, tagAliasName, tagID)
}
func (p *postgresqlConnection) GetAllTagAlias(ctx context.Context) ([]models.TagAlias, error) {
return postgres.GetAllTagAlias(ctx, p.db)
}
func (p *postgresqlConnection) GetAllTagAliasByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagAlias, error) {
return postgres.GetAllTagAliasByTag(ctx, p.db, tagID)
}
func (p *postgresqlConnection) DeleteTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName) error {
return postgres.DeleteTagAlias(ctx, p.db, tagAliasName)
}
func (p *postgresqlConnection) CreateTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error {
return postgres.CreateTagGroup(ctx, p.db, tagGroupName, tagID)
}
func (p *postgresqlConnection) GetAllTagGroup(ctx context.Context) ([]models.TagGroup, error) {
return postgres.GetAllTagGroup(ctx, p.db)
}
func (p *postgresqlConnection) GetAllTagGroupByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagGroup, error) {
return postgres.GetAllTagGroupByTag(ctx, p.db, tagID)
}
func (p *postgresqlConnection) DeleteTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName) error {
return postgres.DeleteTagGroup(ctx, p.db, tagGroupName)
}
func (p *postgresqlConnection) CreateTag(ctx context.Context, tagName models.AnthroveTagName, tagType models.TagType) error {
return postgres.CreateTag(ctx, p.db, tagName, tagType)
}
func (p *postgresqlConnection) GetAllTagsByTagType(ctx context.Context, tagType models.TagType) ([]models.Tag, error) {
return postgres.GetAllTagByTagsType(ctx, p.db, tagType)
}
func (p *postgresqlConnection) DeleteTag(ctx context.Context, tagName models.AnthroveTagName) error {
return postgres.DeleteTag(ctx, p.db, tagName)
}
// HELPER
func (p *postgresqlConnection) migrateDatabase(dbPool *gorm.DB) error {
dialect := "postgres"
migrations := &migrate.EmbedFileSystemMigrationSource{FileSystem: embedMigrations, Root: "migrations"}
db, err := dbPool.DB()
if err != nil {
return fmt.Errorf("postgres migration: %v", err)
}
n, err := migrate.Exec(db, dialect, migrations, migrate.Up)
if err != nil {
return fmt.Errorf("postgres migration: %v", err)
}
if p.debug {
if n != 0 {
log.Infof("postgres migration: applied %d migrations!", n)
} else {
log.Info("postgres migration: nothing to migrate")
}
}
return nil
}

File diff suppressed because it is too large Load Diff

19
pkg/database/source.go Normal file
View File

@ -0,0 +1,19 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
type Source interface {
// CreateSource adds a new source to the database.
CreateSource(ctx context.Context, anthroveSource *models.Source) error
// GetAllSources retrieves all sources.
GetAllSources(ctx context.Context) ([]models.Source, error)
// GetSourceByDomain retrieves a source by its URL.
GetSourceByDomain(ctx context.Context, sourceDomain models.AnthroveSourceDomain) (*models.Source, error)
}

18
pkg/database/tag.go Normal file
View File

@ -0,0 +1,18 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
type Tag interface {
CreateTag(ctx context.Context, tagName models.AnthroveTagName, tagType models.TagType) error
// GetAllTags retrieves all tags.
GetAllTags(ctx context.Context) ([]models.Tag, error)
GetAllTagsByTagType(ctx context.Context, tagType models.TagType) ([]models.Tag, error)
DeleteTag(ctx context.Context, tagName models.AnthroveTagName) error
}

17
pkg/database/tagalias.go Normal file
View File

@ -0,0 +1,17 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
type TagAlias interface {
CreateTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error
GetAllTagAlias(ctx context.Context) ([]models.TagAlias, error)
GetAllTagAliasByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagAlias, error)
DeleteTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName) error
}

17
pkg/database/taggroup.go Normal file
View File

@ -0,0 +1,17 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
type TagGroup interface {
CreateTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error
GetAllTagGroup(ctx context.Context) ([]models.TagGroup, error)
GetAllTagGroupByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagGroup, error)
DeleteTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName) error
}

42
pkg/database/user.go Normal file
View File

@ -0,0 +1,42 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
type User interface {
// CreateUserWithRelationToSource adds a user with a relation to a source.
CreateUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, accountId string, accountUsername string) error
// CreateReferenceBetweenUserAndPost links a user with a post.
CreateReferenceBetweenUserAndPost(ctx context.Context, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) error
UpdateUserSourceScrapeTimeInterval(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, scrapeTime models.AnthroveScrapeTimeInterval) error
UpdateUserSourceLastScrapeTime(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, lastScrapeTime models.AnthroveUserLastScrapeTime) error
UpdateUserSourceValidation(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, valid bool) error
GetAllUsers(ctx context.Context) ([]models.User, error)
// GetUserFavoritesCount retrieves the count of a user's favorites.
GetUserFavoritesCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error)
// GetAllUserSources retrieves the source links of a user.
GetAllUserSources(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.UserSource, error)
// GetUserSourceBySourceID retrieves a specified source link of a user.
GetUserSourceBySourceID(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID) (*models.UserSource, error)
// GetAllUserFavoritesWithPagination retrieves a user's favorite posts with pagination.
GetAllUserFavoritesWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error)
// GetAllTagsFromUser retrieves a user's tags through their favorite posts.
GetAllTagsFromUser(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error)
// CheckIfUserHasPostAsFavorite checks if a user-post link exists.
CheckIfUserHasPostAsFavorite(ctx context.Context, anthroveUserID models.AnthroveUserID, sourcePostID models.AnthrovePostID) (bool, error)
}

25
pkg/error/database.go Normal file
View File

@ -0,0 +1,25 @@
package error
type EntityAlreadyExists struct{}
func (e *EntityAlreadyExists) Error() string {
return "EntityAlreadyExists error"
}
type NoDataWritten struct{}
func (e *NoDataWritten) Error() string {
return "NoDataWritten error"
}
type NoDataFound struct{}
func (e *NoDataFound) Error() string {
return "NoDataFound error"
}
type NoRelationCreated struct{}
func (e *NoRelationCreated) Error() string {
return "relationship creation error"
}

View File

@ -0,0 +1,83 @@
package error
import "testing"
func TestEntityAlreadyExists_Error(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "Test : Valid error String",
want: "EntityAlreadyExists error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &EntityAlreadyExists{}
if got := e.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}
func TestNoDataFound_Error(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "Test : Valid error String",
want: "NoDataFound error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &NoDataFound{}
if got := e.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}
func TestNoDataWritten_Error(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "Test : Valid error String",
want: "NoDataWritten error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &NoDataWritten{}
if got := e.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}
func TestNoRelationCreated_Error(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "Test : Valid error String",
want: "relationship creation error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &NoRelationCreated{}
if got := e.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}

19
pkg/error/validation.go Normal file
View File

@ -0,0 +1,19 @@
package error
import "fmt"
const (
AnthroveUserIDIsEmpty = "anthrovePostID cannot be empty"
AnthroveUserIDToShort = "anthrovePostID needs to be 25 characters long"
AnthroveSourceIDEmpty = "anthroveSourceID cannot be empty"
AnthroveSourceIDToShort = "anthroveSourceID needs to be 25 characters long"
AnthroveTagIDEmpty = "tagID cannot be empty"
)
type EntityValidationFailed struct {
Reason string
}
func (e EntityValidationFailed) Error() string {
return fmt.Sprintf("Entity validation failed: %s", e.Reason)
}

View File

@ -0,0 +1,30 @@
package error
import "testing"
func TestEntityValidationFailed_Error(t *testing.T) {
type fields struct {
Reason string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Reason",
fields: fields{Reason: "TEST ERROR"},
want: "Entity validation failed: TEST ERROR",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := EntityValidationFailed{
Reason: tt.fields.Reason,
}
if got := e.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,116 +0,0 @@
// Package graph provides a client for using the OtterSpace API.
//
// This package provides a client to interact with the OtterSpace API. It includes
// methods for all API endpoints, and convenience methods for common tasks.
//
// This is a simple usage example:
//
// package main
//
// import (
// "context"
// "fmt"
// "git.dragse.it/anthrove/otter-space-sdk/pkg/models"
// "git.dragse.it/anthrove/otter-space-sdk/pkg/graph"
// )
//
// func main() {
// client := graph.NewGraphConnection()
// err := client.Connect(context.Background(), "your-endpoint", "your-username", "your-password")
// if err != nil {
// fmt.Println(err)
// return
// }
// // further usage of the client...
// }
package graph
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
)
// OtterSpace provides an interface for interacting with the OtterSpace API.
// It includes methods for connecting to the API, adding and linking users, posts, and sources,
// and retrieving information about users and posts.
type OtterSpace interface {
// Connect sets up a connection to the OtterSpace API endpoint using the provided username and password.
// It returns an error if the connection cannot be established.
Connect(ctx context.Context, endpoint string, username string, password string) error
// AddUserWithRelationToSource adds a new user to the OtterSpace graph and associates them with a source.
// It returns the newly created user and an error if the operation fails.
AddUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDomain string, userID string, username string) error
// AddSource adds a new source to the OtterSpace graph.
// It returns an error if the operation fails.
AddSource(ctx context.Context, anthroveSource *models.AnthroveSource) error
// AddPost adds a new post to the OtterSpace graph.
// It returns an error if the operation fails.
AddPost(ctx context.Context, anthrovePost *models.AnthrovePost) error
// AddTagWithRelationToPost adds a new tag to the OtterSpace graph and associates it with a post.
// It returns an error if the operation fails.
AddTagWithRelationToPost(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.AnthroveTag) error
// LinkPostWithSource establishes a link between a post and a source in the OtterSpace graph.
// It returns an error if the operation fails.
LinkPostWithSource(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveSourceDomain string, anthrovePostRelationship *models.AnthrovePostRelationship) error
// LinkUserWithPost establishes a link between a user and a post in the OtterSpace graph.
// It returns an error if the operation fails.
LinkUserWithPost(ctx context.Context, anthroveUser *models.AnthroveUser, anthrovePost *models.AnthrovePost) error
// CheckUserPostLink checks if a link between a user and a post exists in the OtterSpace graph.
// It returns true if the link exists, false otherwise, and an error if the operation fails.
CheckUserPostLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourcePostID string, sourceUrl string) (bool, error)
// CheckPostNodeExistsByAnthroveID checks if a post node exists in the OtterSpace graph by its Anthrove ID.
// It returns the post if it exists, a boolean indicating whether the post was found, and an error if the operation fails.
CheckPostNodeExistsByAnthroveID(ctx context.Context, anthrovePost *models.AnthrovePost) (*models.AnthrovePost, bool, error)
// CheckPostNodeExistsBySourceURL checks if a post node exists in the OtterSpace graph by its source URL.
// It returns the post if it exists, a boolean indicating whether the post was found, and an error if the operation fails.
CheckPostNodeExistsBySourceURL(ctx context.Context, sourceUrl string) (*models.AnthrovePost, bool, error)
// CheckPostNodeExistsBySourceID checks if a post node exists in the OtterSpace graph by its source ID.
// It returns the post if it exists, a boolean indicating whether the post was found, and an error if the operation fails.
CheckPostNodeExistsBySourceID(ctx context.Context, sourcePostID string) (*models.AnthrovePost, bool, error)
// GetUserFavoriteCount retrieves the count of a user's favorite posts from the OtterSpace graph.
// It returns the count and an error if the operation fails.
GetUserFavoriteCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error)
// GetUserSourceLinks retrieves the links between a user and sources in the OtterSpace graph.
// It returns a map of source domains to user-source relationships, and an error if the operation fails.
GetUserSourceLinks(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.AnthroveUserRelationship, error)
// GetSpecifiedUserSourceLink GetUserSourceLinks retrieves the links between a user and a specific source in the OtterSpace graph.
// It returns a map of source domains to user-source relationships, and an error if the operation fails.
GetSpecifiedUserSourceLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDisplayName string) (map[string]models.AnthroveUserRelationship, error)
// GetAnthroveUser retrieves a user from the OtterSpace graph by their ID.
// It returns the user and an error if the operation fails.
GetAnthroveUser(ctx context.Context, anthroveUserID models.AnthroveUserID) (*models.AnthroveUser, error)
// GetAllAnthroveUserIDs retrieves all user IDs from the OtterSpace graph.
// It returns a slice of user IDs and an error if the operation fails.
GetAllAnthroveUserIDs(ctx context.Context) ([]models.AnthroveUserID, error)
// GetUserFavoritePostsWithPagination gets all user favorites with relation and sources for the given user
GetUserFavoritePostsWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error)
// GetUserTagsTroughFavedPosts returns a list of Tags that the user hs favorites through a post
GetUserTagsTroughFavedPosts(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error)
// GetAllTags returns a list of Tags that the user hs favorites through a post
GetAllTags(ctx context.Context) ([]models.TagsWithFrequency, error)
// GetAllSources returns a list of Sources in the database
GetAllSources(ctx context.Context) ([]models.AnthroveSource, error)
// GetSourceByURL returns the Source Node based on the URL
GetSourceByURL(ctx context.Context, sourceUrl string) (*models.AnthroveSource, error)
}

View File

@ -1,123 +0,0 @@
package graph
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/internal"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/neo4j/neo4j-go-driver/v5/neo4j/config"
)
type graphConnection struct {
driver neo4j.DriverWithContext
graphDebug bool
}
func NewGraphConnection(graphDebug bool) OtterSpace {
return &graphConnection{
driver: nil,
graphDebug: graphDebug,
}
}
func (g *graphConnection) Connect(ctx context.Context, endpoint string, username string, password string) error {
driver, err := neo4j.NewDriverWithContext(endpoint, neo4j.BasicAuth(username, password, ""),
logger(g.graphDebug))
if err != nil {
return err
}
err = driver.VerifyAuthentication(ctx, nil)
if err != nil {
return err
}
g.driver = driver
return nil
}
func (g *graphConnection) AddUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDomain string, userID string, username string) error {
return internal.CreateUserNodeWithSourceRelation(ctx, g.driver, anthroveUserID, sourceDomain, userID, username)
}
func (g *graphConnection) AddSource(ctx context.Context, anthroveSource *models.AnthroveSource) error {
return internal.CreateSourceNode(ctx, g.driver, anthroveSource)
}
func (g *graphConnection) AddPost(ctx context.Context, anthrovePost *models.AnthrovePost) error {
return internal.CreateAnthrovePostNode(ctx, g.driver, anthrovePost)
}
func (g *graphConnection) AddTagWithRelationToPost(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.AnthroveTag) error {
return internal.CreateTagNodeWitRelation(ctx, g.driver, anthrovePostID, anthroveTag)
}
func (g *graphConnection) LinkPostWithSource(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveSourceDomain string, anthrovePostRelationship *models.AnthrovePostRelationship) error {
return internal.EstablishAnthrovePostToSourceLink(ctx, g.driver, anthrovePostID, anthroveSourceDomain, anthrovePostRelationship)
}
func (g *graphConnection) LinkUserWithPost(ctx context.Context, anthroveUser *models.AnthroveUser, anthrovePost *models.AnthrovePost) error {
return internal.EstablishUserToPostLink(ctx, g.driver, anthroveUser, anthrovePost)
}
func (g *graphConnection) CheckUserPostLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourcePostID string, sourceUrl string) (bool, error) {
return internal.CheckUserToPostLink(ctx, g.driver, anthroveUserID, sourcePostID, sourceUrl)
}
func (g *graphConnection) CheckPostNodeExistsByAnthroveID(ctx context.Context, anthrovePost *models.AnthrovePost) (*models.AnthrovePost, bool, error) {
return internal.CheckIfAnthrovePostNodeExistsByAnthroveID(ctx, g.driver, anthrovePost)
}
func (g *graphConnection) CheckPostNodeExistsBySourceURL(ctx context.Context, sourceUrl string) (*models.AnthrovePost, bool, error) {
return internal.CheckIfAnthrovePostNodeExistsBySourceURl(ctx, g.driver, sourceUrl)
}
func (g *graphConnection) CheckPostNodeExistsBySourceID(ctx context.Context, sourcePostID string) (*models.AnthrovePost, bool, error) {
return internal.CheckIfAnthrovePostNodeExistsBySourceID(ctx, g.driver, sourcePostID)
}
func (g *graphConnection) GetUserFavoriteCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error) {
return internal.GetUserFavoritesCount(ctx, g.driver, anthroveUserID)
}
func (g *graphConnection) GetUserSourceLinks(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.AnthroveUserRelationship, error) {
return internal.GetUserSourceLink(ctx, g.driver, anthroveUserID)
}
func (g *graphConnection) GetSpecifiedUserSourceLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDisplayName string) (map[string]models.AnthroveUserRelationship, error) {
return internal.GetSpecifiedUserSourceLink(ctx, g.driver, anthroveUserID, sourceDisplayName)
}
func (g *graphConnection) GetAnthroveUser(ctx context.Context, anthroveUserID models.AnthroveUserID) (*models.AnthroveUser, error) {
return internal.GetAnthroveUser(ctx, g.driver, anthroveUserID)
}
func (g *graphConnection) GetAllAnthroveUserIDs(ctx context.Context) ([]models.AnthroveUserID, error) {
return internal.GetAllAnthroveUserIDs(ctx, g.driver)
}
func (g *graphConnection) GetUserFavoritePostsWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) {
return internal.GetUserFavoriteNodeWithPagination(ctx, g.driver, anthroveUserID, skip, limit)
}
func (g *graphConnection) GetUserTagsTroughFavedPosts(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) {
return internal.GetUserTagNodeWitRelationToFavedPosts(ctx, g.driver, anthroveUserID)
}
func (g *graphConnection) GetAllTags(ctx context.Context) ([]models.TagsWithFrequency, error) {
return internal.GetTags(ctx, g.driver)
}
func (g *graphConnection) GetAllSources(ctx context.Context) ([]models.AnthroveSource, error) {
return internal.GetAllSourceNodes(ctx, g.driver)
}
func (g *graphConnection) GetSourceByURL(ctx context.Context, sourceUrl string) (*models.AnthroveSource, error) {
return internal.GetSourceNodesByURL(ctx, g.driver, sourceUrl)
}
func logger(graphDebug bool) func(config *config.Config) {
return func(config *config.Config) {
config.Log = internal.NewGraphLogger(graphDebug)
}
}

View File

@ -1,20 +1,5 @@
package models package models
type FavoriteRelations struct {
SourcesID string `json:"sources_id"`
Relations AnthrovePostRelationship `json:"relations"`
}
type FavoritePost struct {
AnthrovePost AnthrovePost `json:"anthrove_post"`
Relations []FavoriteRelations `json:"relations"`
}
type FavoriteList struct { type FavoriteList struct {
Posts []FavoritePost `json:"posts,omitempty"` Posts []Post `json:"posts,omitempty"`
}
type TagsWithFrequency struct {
Frequency int64 `json:"frequency"`
Tags AnthroveTag `json:"tags"`
} }

12
pkg/models/config.go Normal file
View File

@ -0,0 +1,12 @@
package models
type DatabaseConfig struct {
Endpoint string `env:"DB_ENDPOINT,required"`
Username string `env:"DB_USERNAME,required"`
Password string `env:"DB_PASSWORD,required,unset"`
Database string `env:"DB_DATABASE,required"`
Port int `env:"DB_PORT,required" envDefault:"5432"`
SSL bool `env:"DB_SSL,required" envDefault:"true"`
Timezone string `env:"DB_TIMEZONE,required" envDefault:"Europe/Berlin"`
Debug bool `env:"DB_DEBUG" envDefault:"false"`
}

View File

@ -1,18 +1,41 @@
package models package models
import "time"
type AnthroveUserID string type AnthroveUserID string
type AnthrovePostID string type AnthrovePostID string
type AnthroveRating string type AnthroveSourceID string
type AnthroveSourceDomain string
type AnthrovePostURL string
type AnthroveTagGroupName string
type AnthroveTagAliasName string
type AnthroveTagID string
type AnthroveScrapeTimeInterval int
type AnthroveUserLastScrapeTime time.Time
type AnthroveTagName string
type Rating string
type TagType string
const ( const (
SFW AnthroveRating = "s" SFW Rating = "safe"
NSFW AnthroveRating = "e" NSFW Rating = "explicit"
Questionable AnthroveRating = "q" Questionable Rating = "questionable"
Unknown AnthroveRating = "unknown" Unknown Rating = "unknown"
) )
func (r *AnthroveRating) Convert(e621Rating string) { const (
General TagType = "general"
Species TagType = "species"
Character TagType = "character"
Artist TagType = "artist"
Lore TagType = "lore"
Meta TagType = "meta"
Invalid TagType = "invalid"
Copyright TagType = "copyright"
)
func (r *Rating) Convert(e621Rating string) {
switch e621Rating { switch e621Rating {
case "e": case "e":
*r = NSFW *r = NSFW
@ -23,5 +46,4 @@ func (r *AnthroveRating) Convert(e621Rating string) {
default: default:
*r = Unknown *r = Unknown
} }
} }

59
pkg/models/const_test.go Normal file
View File

@ -0,0 +1,59 @@
package models
import (
"reflect"
"testing"
)
func TestRating_Convert(t *testing.T) {
type args struct {
e621Rating string
}
tests := []struct {
name string
r *Rating
args args
want Rating
}{
{
name: "Test 1: NSFW Rating",
r: new(Rating),
args: args{
e621Rating: "e",
},
want: NSFW,
},
{
name: "Test 2: Questionable Rating",
r: new(Rating),
args: args{
e621Rating: "q",
},
want: Questionable,
},
{
name: "Test 3: SFW Rating",
r: new(Rating),
args: args{
e621Rating: "s",
},
want: SFW,
},
{
name: "Test 4: Unknown Rating",
r: new(Rating),
args: args{
e621Rating: "x",
},
want: Unknown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.r.Convert(tt.args.e621Rating)
if !reflect.DeepEqual(*tt.r, tt.want) {
t.Errorf("Convert() = %v, want %v", *tt.r, tt.want)
}
})
}
}

33
pkg/models/orm.go Normal file
View File

@ -0,0 +1,33 @@
package models
import (
gonanoid "github.com/matoous/go-nanoid/v2"
"gorm.io/gorm"
"time"
)
type ID interface {
AnthroveUserID | AnthroveSourceID | AnthrovePostID
}
type BaseModel[T ID] struct {
ID T `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (base *BaseModel[T]) BeforeCreate(db *gorm.DB) error {
var defaultVar T
if base.ID == defaultVar {
id, err := gonanoid.New(25)
if err != nil {
return err
}
base.ID = T(id)
}
return nil
}

56
pkg/models/orm_test.go Normal file
View File

@ -0,0 +1,56 @@
package models
import (
"testing"
"time"
"gorm.io/gorm"
)
func TestBaseModel_BeforeCreate(t *testing.T) {
type fields struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
type args struct {
db *gorm.DB
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "Test 1: Prefilled ID",
fields: fields{
ID: "1",
},
args: args{db: nil},
wantErr: false,
},
{
name: "Test 1: Autogenerate ID",
fields: fields{
ID: "",
},
args: args{db: nil},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
base := &BaseModel[AnthrovePostID]{
ID: AnthrovePostID(tt.fields.ID),
CreatedAt: tt.fields.CreatedAt,
UpdatedAt: tt.fields.UpdatedAt,
DeletedAt: tt.fields.DeletedAt,
}
if err := base.BeforeCreate(tt.args.db); (err != nil) != tt.wantErr {
t.Errorf("BeforeCreate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -1,6 +1,14 @@
package models package models
type AnthrovePost struct { // Post model
PostID AnthrovePostID `json:"post_id"` type Post struct {
Rating AnthroveRating `json:"rating"` BaseModel[AnthrovePostID]
Rating Rating `gorm:"type:enum('safe','questionable','explicit')"`
Tags []Tag `gorm:"many2many:post_tags;"`
Favorites []UserFavorites `gorm:"foreignKey:PostID"`
References []PostReference `gorm:"foreignKey:PostID"`
}
func (Post) TableName() string {
return "Post"
} }

View File

@ -0,0 +1,19 @@
package models
type PostReference struct {
PostID string `gorm:"primaryKey"`
SourceID string `gorm:"primaryKey"`
URL string `gorm:"not null;unique"`
PostReferenceConfig
}
type PostReferenceConfig struct {
SourcePostID string
FullFileURL string
PreviewFileURL string
SampleFileURL string
}
func (PostReference) TableName() string {
return "PostReference"
}

View File

@ -0,0 +1,44 @@
package models
import "testing"
func TestPostReference_TableName(t *testing.T) {
type fields struct {
PostID string
SourceID string
URL string
SourcePostID string
FullFileURL string
PreviewFileURL string
SampleFileURL string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: PostReference",
fields: fields{},
want: "PostReference",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
po := PostReference{
PostID: tt.fields.PostID,
SourceID: tt.fields.SourceID,
URL: tt.fields.URL,
PostReferenceConfig: PostReferenceConfig{
SourcePostID: tt.fields.SourcePostID,
FullFileURL: tt.fields.FullFileURL,
PreviewFileURL: tt.fields.PreviewFileURL,
SampleFileURL: tt.fields.SampleFileURL,
},
}
if got := po.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}

38
pkg/models/post_test.go Normal file
View File

@ -0,0 +1,38 @@
package models
import "testing"
func TestPost_TableName(t *testing.T) {
type fields struct {
BaseModel BaseModel[AnthrovePostID]
Rating Rating
Tags []Tag
Favorites []UserFavorites
References []PostReference
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is name Post",
fields: fields{},
want: "Post",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
po := Post{
BaseModel: tt.fields.BaseModel,
Rating: tt.fields.Rating,
Tags: tt.fields.Tags,
Favorites: tt.fields.Favorites,
References: tt.fields.References,
}
if got := po.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,12 +0,0 @@
package models
type AnthroveUserRelationship struct {
UserID string `json:"user_id"`
Username string `json:"username"`
ScrapeTimeInterval string `json:"scrape_time_interval"`
Source AnthroveSource `json:"source"`
}
type AnthrovePostRelationship struct {
PostID string `json:"post_id"`
Url string `json:"url"`
}

View File

@ -1,7 +1,15 @@
package models package models
type AnthroveSource struct { // Source model
DisplayName string `json:"display_name"` type Source struct {
Domain string `json:"domain"` BaseModel[AnthroveSourceID]
Icon string `json:"icon"` DisplayName string
Domain string `gorm:"not null;unique"`
Icon string `gorm:"not null"`
UserSources []UserSource `gorm:"foreignKey:SourceID"`
References []PostReference `gorm:"foreignKey:SourceID"`
}
func (Source) TableName() string {
return "Source"
} }

40
pkg/models/source_test.go Normal file
View File

@ -0,0 +1,40 @@
package models
import "testing"
func TestSource_TableName(t *testing.T) {
type fields struct {
BaseModel BaseModel[AnthroveSourceID]
DisplayName string
Domain string
Icon string
UserSources []UserSource
References []PostReference
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is name Source",
fields: fields{},
want: "Source",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
so := Source{
BaseModel: tt.fields.BaseModel,
DisplayName: tt.fields.DisplayName,
Domain: tt.fields.Domain,
Icon: tt.fields.Icon,
UserSources: tt.fields.UserSources,
References: tt.fields.References,
}
if got := so.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,6 +1,39 @@
package models package models
type AnthroveTag struct { // Tag models
Name string `json:"name"` type Tag struct {
Type string `json:"type"` Name string `gorm:"primaryKey"`
Type TagType `gorm:"column:tag_type"`
Aliases []TagAlias `gorm:"foreignKey:TagID"`
Groups []TagGroup `gorm:"foreignKey:TagID"`
Posts []Post `gorm:"many2many:post_tags;"`
}
func (Tag) TableName() string {
return "Tag"
}
// TagAlias model
type TagAlias struct {
Name string `gorm:"primaryKey"`
TagID string
}
func (TagAlias) TableName() string {
return "TagAlias"
}
// TagGroup model
type TagGroup struct {
Name string `gorm:"primaryKey"`
TagID string
}
func (TagGroup) TableName() string {
return "TagGroup"
}
type TagsWithFrequency struct {
Frequency int64 `json:"frequency"`
Tags Tag `json:"tags"`
} }

96
pkg/models/tag_test.go Normal file
View File

@ -0,0 +1,96 @@
package models
import "testing"
func TestTagAlias_TableName(t *testing.T) {
type fields struct {
Name string
TagID string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is Name TagAlias",
fields: fields{},
want: "TagAlias",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ta := TagAlias{
Name: tt.fields.Name,
TagID: tt.fields.TagID,
}
if got := ta.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}
func TestTagGroup_TableName(t *testing.T) {
type fields struct {
Name string
TagID string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is name TagGroup",
fields: fields{},
want: "TagGroup",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ta := TagGroup{
Name: tt.fields.Name,
TagID: tt.fields.TagID,
}
if got := ta.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}
func TestTag_TableName(t *testing.T) {
type fields struct {
Name string
Type TagType
Aliases []TagAlias
Groups []TagGroup
Posts []Post
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is name Tag",
fields: fields{},
want: "Tag",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ta := Tag{
Name: tt.fields.Name,
Type: tt.fields.Type,
Aliases: tt.fields.Aliases,
Groups: tt.fields.Groups,
Posts: tt.fields.Posts,
}
if got := ta.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,6 +1,12 @@
package models package models
type AnthroveUser struct { // User model
UserID AnthroveUserID `json:"user_id"` type User struct {
Relationship []AnthroveUserRelationship `json:"relationship"` BaseModel[AnthroveUserID]
Favorites []UserFavorites `gorm:"foreignKey:UserID"`
Sources []UserSource `gorm:"foreignKey:UserID"`
}
func (User) TableName() string {
return "User"
} }

View File

@ -0,0 +1,13 @@
package models
import "time"
type UserFavorites struct {
UserID string `gorm:"primaryKey"`
PostID string `gorm:"primaryKey"`
CreatedAt time.Time
}
func (UserFavorites) TableName() string {
return "UserFavorites"
}

View File

@ -0,0 +1,37 @@
package models
import (
"testing"
"time"
)
func TestUserFavorite_TableName(t *testing.T) {
type fields struct {
UserID string
PostID string
CreatedAt time.Time
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is name UserFavorites",
fields: fields{},
want: "UserFavorites",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
us := UserFavorites{
UserID: tt.fields.UserID,
PostID: tt.fields.PostID,
CreatedAt: tt.fields.CreatedAt,
}
if got := us.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}

20
pkg/models/userSource.go Normal file
View File

@ -0,0 +1,20 @@
package models
import "time"
type UserSource struct {
User User `gorm:"foreignKey:ID;references:UserID"`
UserID string `gorm:"primaryKey"`
Source Source `gorm:"foreignKey:ID;references:SourceID"`
SourceID string `gorm:"primaryKey"`
ScrapeTimeInterval string
AccountUsername string
AccountID string
LastScrapeTime time.Time
AccountValidate bool
AccountValidationKey string
}
func (UserSource) TableName() string {
return "UserSource"
}

View File

@ -0,0 +1,42 @@
package models
import "testing"
func TestUserSource_TableName(t *testing.T) {
type fields struct {
User User
UserID string
Source Source
SourceID string
ScrapeTimeInterval string
AccountUsername string
AccountID string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is name UserSource",
fields: fields{},
want: "UserSource",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
us := UserSource{
User: tt.fields.User,
UserID: tt.fields.UserID,
Source: tt.fields.Source,
SourceID: tt.fields.SourceID,
ScrapeTimeInterval: tt.fields.ScrapeTimeInterval,
AccountUsername: tt.fields.AccountUsername,
AccountID: tt.fields.AccountID,
}
if got := us.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}

34
pkg/models/user_test.go Normal file
View File

@ -0,0 +1,34 @@
package models
import "testing"
func TestUser_TableName(t *testing.T) {
type fields struct {
BaseModel BaseModel[AnthroveUserID]
Favorites []UserFavorites
Sources []UserSource
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test 1: Is name User",
fields: fields{},
want: "User",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
us := User{
BaseModel: tt.fields.BaseModel,
Favorites: tt.fields.Favorites,
Sources: tt.fields.Sources,
}
if got := us.TableName(); got != tt.want {
t.Errorf("TableName() = %v, want %v", got, tt.want)
}
})
}
}

123
test/helper.go Normal file
View File

@ -0,0 +1,123 @@
package test
import (
"context"
"database/sql"
"net/url"
"strconv"
"strings"
"time"
"git.dragse.it/anthrove/otter-space-sdk/pkg/models"
migrate "github.com/rubenv/sql-migrate"
postgrescontainer "github.com/testcontainers/testcontainers-go/modules/postgres"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
const (
databaseName = "anthrove"
databaseUser = "anthrove"
databasePassword = "anthrove"
migrationSource = "../../pkg/database/migrations/"
)
func StartPostgresContainer(ctx context.Context) (*postgrescontainer.PostgresContainer, *gorm.DB, error) {
pgContainer, err := postgrescontainer.RunContainer(ctx,
testcontainers.WithImage("postgres:alpine"),
postgrescontainer.WithDatabase(databaseName),
postgrescontainer.WithUsername(databaseUser),
postgrescontainer.WithPassword(databasePassword),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).WithStartupTimeout(60*time.Second)),
)
if err != nil {
return nil, nil, err
}
connectionString, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
if err != nil {
return nil, nil, err
}
err = migrateDatabase(connectionString)
if err != nil {
return nil, nil, err
}
gormDB, err := getGormDB(connectionString)
if err != nil {
return nil, nil, err
}
return pgContainer, gormDB, nil
}
func migrateDatabase(connectionString string) error {
db, err := sql.Open("postgres", connectionString)
if err != nil {
return err
}
migrations := &migrate.FileMigrationSource{
Dir: migrationSource,
}
_, err = migrate.Exec(db, "postgres", migrations, migrate.Up)
if err != nil {
return err
}
return nil
}
func getGormDB(connectionString string) (*gorm.DB, error) {
return gorm.Open(postgres.Open(connectionString), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
}
func DatabaseModesFromConnectionString(ctx context.Context, pgContainer *postgrescontainer.PostgresContainer) (*models.DatabaseConfig, error) {
var err error
connectionString, err := pgContainer.ConnectionString(ctx)
if err != nil {
return nil, err
}
connectionStringUrl, err := url.Parse(connectionString)
if err != nil {
return nil, err
}
split := strings.Split(connectionStringUrl.Host, ":")
host := split[0]
port, err := strconv.Atoi(split[1])
if err != nil {
return nil, err
}
database := strings.TrimPrefix(connectionStringUrl.Path, "/")
username := connectionStringUrl.User.Username()
password, _ := connectionStringUrl.User.Password()
return &models.DatabaseConfig{
Endpoint: host,
Username: username,
Password: password,
Database: database,
Port: port,
SSL: false,
Timezone: "Europe/Berlin",
Debug: true,
}, nil
}