migration from old git (no git history)
Some checks failed
Gitea Build Check / Build (push) Failing after 11m27s

This commit is contained in:
SoXX 2024-07-19 10:03:35 +02:00
commit 34c001473b
52 changed files with 10977 additions and 0 deletions

View File

@ -0,0 +1,39 @@
name: Gitea Build Check
run-name: ${{ gitea.actor }} is testing the build
on:
push:
branches:
- main
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

195
.gitignore vendored Normal file
View File

@ -0,0 +1,195 @@
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,goland,go
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,goland,go
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### GoLand ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### GoLand Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,goland,go
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
.idea/*
/.run/*
.env
main.go

57
README.md Normal file
View File

@ -0,0 +1,57 @@
![Build Check Runner](https://git.dragse.it/anthrove/otter-space-sdk/v2/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
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.
## Installation
To install the OtterSpace SDK, you can use `go get`:
```shell
go get git.dragse.it/anthrove/otter-space-sdk/v2
````
## Usage
Here's a simple usage example:
```go
package main
import (
"context"
"fmt"
"git.dragse.it/anthrove/otter-space-sdk/v2/pkg/database"
"git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models"
)
func main() {
var err error
dbDebug := false
ctx := context.Background()
pgClient := database.NewPostgresqlConnection(dbDebug)
err = pgClient.Connect(ctx, "your-endpoint", "your-username", "your-password", "anthrove", 5432, "disable", "Europe/Berlin")
if err != nil {
fmt.Println(err)
return
}
// further usage of the client...
}
```
This example creates a new client, connects to the OtterSpace API, and then the client can be used to interact with the API.

73
go.mod Normal file
View File

@ -0,0 +1,73 @@
module git.dragse.it/anthrove/otter-space-sdk/v2
go 1.22.0
require (
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/testcontainers/testcontainers-go v0.31.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.10
)
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
)

229
go.sum Normal file
View File

@ -0,0 +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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
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/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/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.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.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 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
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=

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

@ -0,0 +1,131 @@
package postgres
import (
"context"
"errors"
otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/v2/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 CreatePostInBatch(ctx context.Context, db *gorm.DB, anthrovePost []models.Post, batchSize int) error {
if anthrovePost == nil {
return &otterError.EntityValidationFailed{Reason: "anthrovePost cannot be nil"}
}
if len(anthrovePost) == 0 {
return &otterError.EntityValidationFailed{Reason: "anthrovePost cannot be empty"}
}
if batchSize == 0 {
return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"}
}
result := db.WithContext(ctx).CreateInBatches(anthrovePost, batchSize)
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_size": len(anthrovePost),
"batch_size": batchSize,
}).Trace("database: created tag node")
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,468 @@
package postgres
import (
"context"
"fmt"
_ "github.com/lib/pq"
"testing"
"git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models"
"git.dragse.it/anthrove/otter-space-sdk/v2/test"
"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 TestCreatePostInBatch(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
validPosts := []models.Post{
{
Rating: models.SFW,
},
{
Rating: models.NSFW,
},
{
Rating: models.Questionable,
},
}
emptyPost := []models.Post{}
// Test
type args struct {
ctx context.Context
db *gorm.DB
anthrovePost []models.Post
batchSize int
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Test 1: Valid Data",
args: args{
ctx: ctx,
db: gormDB,
anthrovePost: validPosts,
batchSize: len(validPosts),
},
wantErr: false,
},
{
name: "Test 2: Emtpy Data",
args: args{
ctx: ctx,
db: gormDB,
anthrovePost: emptyPost,
batchSize: 0,
},
wantErr: true,
},
{
name: "Test 3: Nil Data",
args: args{
ctx: ctx,
db: gormDB,
anthrovePost: nil,
batchSize: 0,
},
wantErr: true,
},
{
name: "Test 4: batchSize 0",
args: args{
ctx: ctx,
db: gormDB,
anthrovePost: validPosts,
batchSize: 0,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CreatePostInBatch(tt.args.ctx, tt.args.db, tt.args.anthrovePost, tt.args.batchSize); (err != nil) != tt.wantErr {
t.Errorf("CreatePostInBatch() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
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/v2/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/v2/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/v2/pkg/models"
"git.dragse.it/anthrove/otter-space-sdk/v2/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/v2/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/v2/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/v2/pkg/models"
"git.dragse.it/anthrove/otter-space-sdk/v2/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
}

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

@ -0,0 +1,440 @@
package postgres
import (
"context"
"errors"
"gorm.io/gorm/clause"
otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/v2/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 CreateTagInBatchAndUpdate(ctx context.Context, db *gorm.DB, tags []models.Tag, batchSize int) error {
if len(tags) == 0 {
return &otterError.EntityValidationFailed{Reason: "tags cannot be empty"}
}
if tags == nil {
return &otterError.EntityValidationFailed{Reason: "tags cannot be nil"}
}
if batchSize == 0 {
return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"}
}
result := db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoUpdates: clause.AssignmentColumns([]string{"tag_type"}),
}).CreateInBatches(tags, batchSize)
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_size": len(tags),
"batch_size": batchSize,
}).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).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).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 CreateTagAliasInBatch(ctx context.Context, db *gorm.DB, tagAliases []models.TagAlias, batchSize int) error {
if len(tagAliases) == 0 {
return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be empty"}
}
if tagAliases == nil {
return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be nil"}
}
if batchSize == 0 {
return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"}
}
result := db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).CreateInBatches(tagAliases, batchSize)
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_size": len(tagAliases),
"batch_size": batchSize,
}).Trace("database: created tag node")
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 CreateTagGroupInBatch(ctx context.Context, db *gorm.DB, tagGroups []models.TagGroup, batchSize int) error {
if len(tagGroups) == 0 {
return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be empty"}
}
if tagGroups == nil {
return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be nil"}
}
if batchSize == 0 {
return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"}
}
result := db.WithContext(ctx).CreateInBatches(tagGroups, batchSize)
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_size": len(tagGroups),
"batch_size": batchSize,
}).Trace("database: created tag node")
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

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

@ -0,0 +1,398 @@
package postgres
import (
"context"
"errors"
"time"
otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error"
"git.dragse.it/anthrove/otter-space-sdk/v2/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
}
// TODO: FIX THE TEST
func GetUserFavoriteWithPagination(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) {
var favoritePosts []models.Post
if anthroveUserID == "" {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty}
}
if len(anthroveUserID) != 25 {
return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort}
}
db.WithContext(ctx).Joins("RIGHT JOIN \"UserFavorites\" AS of ON \"Post\".id = of.post_id AND of.user_id = ?", anthroveUserID).Preload("References").Offset(skip).Limit(limit).Find(&favoritePosts)
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

11
internal/utils/slices.go Normal file
View File

@ -0,0 +1,11 @@
package utils
func GetOrDefault(data map[string]any, key string, defaultVal any) any {
val, ok := data[key]
if !ok {
return defaultVal
}
return val
}

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/v2/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)
);

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

@ -0,0 +1,31 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models"
)
type Post interface {
// CreatePost adds a new post to the database.
CreatePost(ctx context.Context, anthrovePost *models.Post) error
// TODO: Everything
CreatePostInBatch(ctx context.Context, anthrovePost []models.Post, batchSize int) 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
}

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

@ -0,0 +1,261 @@
package database
import (
"context"
"embed"
"fmt"
log2 "log"
"os"
"time"
"git.dragse.it/anthrove/otter-space-sdk/v2/internal/postgres"
"git.dragse.it/anthrove/otter-space-sdk/v2/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) CreatePostInBatch(ctx context.Context, anthrovePost []models.Post, batchSize int) error {
return postgres.CreatePostInBatch(ctx, p.db, anthrovePost, batchSize)
}
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)
}
func (p *postgresqlConnection) CreateTagInBatchAndUpdate(ctx context.Context, tags []models.Tag, batchSize int) error {
return postgres.CreateTagInBatchAndUpdate(ctx, p.db, tags, batchSize)
}
func (p *postgresqlConnection) CreateTagAliasInBatch(ctx context.Context, tagAliases []models.TagAlias, batchSize int) error {
return postgres.CreateTagAliasInBatch(ctx, p.db, tagAliases, batchSize)
}
func (p *postgresqlConnection) CreateTagGroupInBatch(ctx context.Context, tagGroups []models.TagGroup, batchSize int) error {
return postgres.CreateTagGroupInBatch(ctx, p.db, tagGroups, batchSize)
}
// 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/v2/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)
}

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

@ -0,0 +1,20 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models"
)
type Tag interface {
CreateTag(ctx context.Context, tagName models.AnthroveTagName, tagType models.TagType) error
CreateTagInBatchAndUpdate(ctx context.Context, tags []models.Tag, batchSize int) 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
}

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

@ -0,0 +1,19 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models"
)
type TagAlias interface {
CreateTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error
CreateTagAliasInBatch(ctx context.Context, tagsAliases []models.TagAlias, batchSize int) 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
}

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

@ -0,0 +1,19 @@
package database
import (
"context"
"git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models"
)
type TagGroup interface {
CreateTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error
CreateTagGroupInBatch(ctx context.Context, tagsGroups []models.TagGroup, batchSize int) 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/v2/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)
}
})
}
}

5
pkg/models/api.go Normal file
View File

@ -0,0 +1,5 @@
package models
type FavoriteList struct {
Posts []Post `json:"posts,omitempty"`
}

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"`
}

49
pkg/models/const.go Normal file
View File

@ -0,0 +1,49 @@
package models
import "time"
type AnthroveUserID string
type AnthrovePostID 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 (
SFW Rating = "safe"
NSFW Rating = "explicit"
Questionable Rating = "questionable"
Unknown Rating = "unknown"
)
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 {
case "e":
*r = NSFW
case "q":
*r = Questionable
case "s":
*r = SFW
default:
*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)
}
})
}
}

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

@ -0,0 +1,34 @@
package models
import (
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
"gorm.io/gorm"
)
type ID interface {
AnthroveUserID | AnthroveSourceID | AnthrovePostID
}
type BaseModel[T ID] struct {
ID T `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `json:"-" 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)
}
})
}
}

14
pkg/models/post.go Normal file
View File

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

View File

@ -0,0 +1,19 @@
package models
type PostReference struct {
PostID string `json:"post_id" gorm:"primaryKey"`
SourceID string `json:"source_id" gorm:"primaryKey"`
URL string `json:"url" gorm:"primaryKey"`
PostReferenceConfig
}
type PostReferenceConfig struct {
SourcePostID string `json:"source_post_id"`
FullFileURL string `json:"full_file_url"`
PreviewFileURL string `json:"preview_file_url"`
SampleFileURL string `json:"sample_file_url"`
}
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)
}
})
}
}

15
pkg/models/source.go Normal file
View File

@ -0,0 +1,15 @@
package models
// Source model
type Source struct {
BaseModel[AnthroveSourceID]
DisplayName string `json:"display_name" `
Domain string `json:"domain" gorm:"not null;unique"`
Icon string `json:"icon" gorm:"not null"`
UserSources []UserSource `json:"-" gorm:"foreignKey:SourceID"`
References []PostReference `json:"references" 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)
}
})
}
}

39
pkg/models/tag.go Normal file
View File

@ -0,0 +1,39 @@
package models
// Tag models
type Tag struct {
Name string `json:"name" gorm:"primaryKey"`
Type TagType `json:"type" gorm:"column:tag_type"`
Aliases []TagAlias `json:"aliases" gorm:"foreignKey:TagID"`
Groups []TagGroup `json:"groups" gorm:"foreignKey:TagID"`
Posts []Post `json:"posts" gorm:"many2many:post_tags;"`
}
func (Tag) TableName() string {
return "Tag"
}
// TagAlias model
type TagAlias struct {
Name string `json:"name" gorm:"primaryKey"`
TagID string `json:"tag_id"`
}
func (TagAlias) TableName() string {
return "TagAlias"
}
// TagGroup model
type TagGroup struct {
Name string `json:"name" gorm:"primaryKey"`
TagID string `json:"tag_id"`
}
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)
}
})
}
}

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

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

View File

@ -0,0 +1,13 @@
package models
import "time"
type UserFavorites struct {
UserID string `json:"user_id" gorm:"primaryKey"`
PostID string `json:"post_id" gorm:"primaryKey"`
CreatedAt time.Time `json:"-"`
}
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 `json:"user" gorm:"foreignKey:ID;references:UserID"`
UserID string `json:"user_id" gorm:"primaryKey"`
Source Source `json:"source" gorm:"foreignKey:ID;references:SourceID"`
SourceID string `json:"source_id" gorm:"primaryKey"`
ScrapeTimeInterval string `json:"scrape_time_interval"`
AccountUsername string `json:"account_username"`
AccountID string `json:"account_id"`
LastScrapeTime time.Time `json:"last_scrape_time"`
AccountValidate bool `json:"account_validate"`
AccountValidationKey string `json:"-"`
}
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/v2/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
}