Compare commits

...
34 Commits
Author SHA1 Message Date
gisle b5c77cf8ca try fix again again again again again.
continuous-integration/drone/push Build is passing
2021-07-26 22:18:31 +02:00
gisle 3045931492 try fix again again again again.
continuous-integration/drone/push Build is passing
2021-07-26 21:43:10 +02:00
gisle 928e643ab7 try fix again again again. 2021-07-26 21:26:53 +02:00
gisle 6b1f34db79 try fix again again.
continuous-integration/drone/push Build is passing
2021-07-26 20:56:46 +02:00
gisle bd27b14a88 try fix again.
continuous-integration/drone/push Build is passing
2021-07-26 20:50:03 +02:00
gisle 4bccdc2ded hopefully fix the bug preventing post deletions sometimes.
continuous-integration/drone/push Build is passing
2021-07-26 20:32:55 +02:00
gisle 67d6161c3b fix characters not updating on edit post and (hopefully) the bug preventing post deletions sometimes.
continuous-integration/drone/push Build is passing
2021-07-26 20:15:51 +02:00
gisle a606b79736 fix prev logs showing current log.
continuous-integration/drone/push Build is passing
2021-04-11 02:14:01 +02:00
gisle 87559997a9 fix comments create/update not applying character name.
continuous-integration/drone/push Build is passing
2021-04-11 01:49:08 +02:00
gisle b641f7d516 fix ts query getting mad at you for quoting a single word. 2021-04-02 20:25:32 +02:00
gisle b117aea7f0 fix ChannelFilter.Logged being ignored in postgres driver.
continuous-integration/drone/push Build is passing
2021-04-02 17:01:15 +02:00
gisle 33ac47a12c fix spaced quotes causing weird behavior in search.
continuous-integration/drone/push Build is passing
2021-03-30 21:16:02 +02:00
gisle 18a83073e8 fix repository bugs and make text searching work more like it did in the past.
continuous-integration/drone/push Build is passing
2021-03-30 20:06:16 +02:00
gisle f14386eceb add key and user repositories.
continuous-integration/drone/push Build is passing
2021-03-28 21:08:45 +02:00
gisle 6748ff8bc2 add log and post postgres repos.
continuous-integration/drone/push Build is passing
2021-03-28 15:10:56 +02:00
gisle e4761ad4a0 add comment repository for postgres.
continuous-integration/drone/push Build is passing
2021-03-27 13:56:51 +01:00
gisle 21b7b44159 remove generated psql code from git. 2021-03-27 10:44:55 +01:00
gisle 663a95c70d add chapter repository to postgres.
continuous-integration/drone/push Build is passing
2021-03-26 23:50:33 +01:00
gisle 525191a0f9 tweak tags and add story repository.
continuous-integration/drone/push Build is passing
2021-03-26 15:47:18 +01:00
gisle 410e07a6a2 add stuff that may not compile.
continuous-integration/drone/push Build is failing
2021-03-25 14:13:43 +01:00
gisle 9aa7304982 add change postgres indexes. 2021-03-22 22:51:21 +01:00
gisle 0f06a3750f add change table. 2021-03-22 21:45:12 +01:00
gisle 0bc86ced98 add channel table. 2021-03-22 19:34:31 +01:00
gisle c5ea598ea1 add sqlc and character table. 2021-03-21 20:53:21 +01:00
gisle b6b7b8f1b6 add dump/restore commands to docker file.
continuous-integration/drone/push Build is passing
2021-03-21 12:21:50 +01:00
gisle e9b00b61fd add rpdata-restore command. 2021-03-21 12:19:27 +01:00
gisle c9084287a4 add rpdata-dump command. 2021-03-20 14:24:02 +01:00
gisle 570d0e2bd2 allow tag/untag of open stories.
continuous-integration/drone/push Build is passing
2020-11-01 02:19:17 +01:00
gisle 6ad0b9d467 fix character service not refreshing logs. 2020-10-07 20:33:56 +02:00
gisle 63bc262091 add sortByFictionalDate field to Story, and sorting by fictional date if set.
continuous-integration/drone/push Build is passing
2020-08-28 22:45:13 +02:00
gisle c4eabd69ac Merge branch 'master' of ssh+git://git.aiterp.net/rpdata/api
continuous-integration/drone/push Build is passing
2020-08-20 21:01:38 +02:00
gisle df982fac17 add IrcCloud log importer. 2020-08-20 20:58:16 +02:00
gisle 6fa2dc9800 fix chapter edit changes being reported as comment add. 2020-08-19 21:26:56 +02:00
gisle 6ce38d61b1 fix log importer not refreshing character list.
continuous-integration/drone/push Build is passing
2020-08-16 11:49:39 +02:00
112 changed files with 4901 additions and 231 deletions
+15 -1
View File
@@ -10,6 +10,20 @@ steps:
- go generate ./...
- go test -v ./...
# - name: postgres-migrate
# image: golang:1.15
# depends_on: []
# environment:
# DB_CONNECT:
# from_secret: db_connect
# commands:
# - go get -u github.com/pressly/goose/...
# - cd migrations/postgres
# - goose postgres "$DB_CONNECT" up
# when:
# event:
# - tag
- name: docker-release-tag
image: plugins/docker
settings:
@@ -36,4 +50,4 @@ steps:
registry: r.vmaple.dev
when:
event:
- tag
- tag
+7
View File
@@ -10,6 +10,13 @@ rpdata-*
.vscode
.idea
/main
/main.exe
/graph2/graphcore/*_gen.go
/database/postgres/psqlcore/*.go
!/database/postgres/psqlcore/package.go
generated.gql
generated.go
/dump.zip
/rpdata-dump-v1/
+4 -2
View File
@@ -1,6 +1,6 @@
## 1. Build Server
# Use Go 1.14
FROM golang:1.14 AS build-server
FROM golang:1.16 AS build-server
# Load repository into docker
WORKDIR /project/
@@ -12,10 +12,12 @@ RUN go generate ./...
# Build server and tools (CGO disabled to make them 100% static)
ENV CGO_ENABLED 0
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cmd/rpdata-server
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-dump ./cmd/rpdata-dump
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-restore ./cmd/rpdata-restore
## 2. Distribute
# Use alpine linux
FROM alpine:3.12
FROM alpine:3.13
# Install certificates to enable HTTPS GET requests, and tzdata for log imports.
RUN apk add --no-cache ca-certificates tzdata
+270
View File
@@ -0,0 +1,270 @@
package main
import (
"archive/zip"
"context"
"encoding/json"
"flag"
"fmt"
"git.aiterp.net/rpdata/api/database"
"git.aiterp.net/rpdata/api/database/mongodb"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/models"
"log"
"os"
"time"
)
var flagDriver = flag.String("driver", "mongodb", "The database driver to use.")
var flagHost = flag.String("host", "127.0.0.1", "The host to connect to.")
var flagPort = flag.Int("port", 27017, "The port to connect on.")
var flagDb = flag.String("db", "rpdata", "The database name")
var flagUsername = flag.String("username", "", "")
var flagPassword = flag.String("password", "", "")
var flagMechanism = flag.String("mechanism", "", "")
var flagOutputFile = flag.String("outfile", "dump.zip", "The file to write to.")
var flagIncludeKeys = flag.Bool("include-keys", false, "Whether to include the keys.")
var flagIncludeUsers = flag.Bool("include-users", false, "Whether to include the keys.")
var flagIncludeUnlisted = flag.Bool("include-unlisted", false, "Whether to include unlisted stuff.")
func main() {
flag.Parse()
trueValue := true
truePtr := &trueValue
cfg := config.Database{
Driver: *flagDriver,
Host: *flagHost,
Port: *flagPort,
Db: *flagDb,
Username: *flagUsername,
Password: *flagPassword,
Mechanism: *flagMechanism,
}
blacklist := make(map[string]bool)
mongodb.DisableFixes = true
db, err := database.Init(cfg)
if err != nil {
log.Fatalln("Failed to open database:", err)
}
file, err := os.OpenFile(*flagOutputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
log.Fatalln("Failed to open output file:", err)
}
zipWriter := zip.NewWriter(file)
defer func() {
err = zipWriter.Close()
if err != nil {
log.Fatalln("Failed to close output file:", err)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*30)
defer cancel()
log.Println("Dumping characters...")
characters, err := db.Characters().List(ctx, models.CharacterFilter{})
if err != nil {
log.Println("Failed to get characters:", err)
}
if len(characters) == 0 {
log.Println("No characters to dump.")
}
for i, character := range characters {
err := writeJsonFile(zipWriter, "character", i, character.ID, time.Now(), character)
if err != nil {
log.Println("Failed to write character", character.ID, ":", err)
continue
}
}
log.Println("Dumping channels...")
channels, err := db.Channels().List(ctx, models.ChannelFilter{})
if err != nil {
log.Println("Failed to get channels:", err)
}
if len(channels) == 0 {
log.Println("No channels to dump.")
}
for i, channel := range channels {
err := writeJsonFile(zipWriter, "channel", i, channel.Name, time.Now(), channel)
if err != nil {
log.Println("Failed to write channel", channel.Name, ":", err)
continue
}
}
log.Println("Dumping changes...")
changes, err := db.Changes().List(ctx, models.ChangeFilter{})
if err != nil {
log.Println("Failed to get changes:", err)
}
if len(changes) == 0 {
log.Println("No changes to dump.")
}
for i, change := range changes {
if !*flagIncludeUnlisted && !change.Listed {
continue
}
err := writeJsonFile(zipWriter, "change", i, change.ID, change.Date, change)
if err != nil {
log.Println("Failed to write change", change.ID, ":", err)
continue
}
}
log.Println("Dumping stories...")
stories, err := db.Stories().List(ctx, models.StoryFilter{})
if err != nil {
log.Println("Failed to get stories:", err)
}
unlistedStories, err := db.Stories().List(ctx, models.StoryFilter{Unlisted: truePtr})
if err != nil {
log.Println("Failed to get unlisted stories:", err)
} else {
stories = append(stories, unlistedStories...)
}
if len(stories) == 0 {
log.Println("No stories to dump.")
}
for i, story := range stories {
if !*flagIncludeUnlisted && !story.Listed {
blacklist[story.ID] = true
continue
}
err := writeJsonFile(zipWriter, "story", i, story.ID, story.CreatedDate, story)
if err != nil {
log.Println("Failed to write story", story.ID, ":", err)
continue
}
}
log.Println("Dumping chapters...")
chapters, err := db.Chapters().List(ctx, models.ChapterFilter{})
if err != nil {
log.Println("Failed to get chapters:", err)
}
if len(chapters) == 0 {
log.Println("No chapters to dump.")
}
for i, chapter := range chapters {
if !*flagIncludeUnlisted && blacklist[chapter.StoryID] {
blacklist[chapter.ID] = true
continue
}
err := writeJsonFile(zipWriter, "chapter", i, chapter.ID, chapter.CreatedDate, chapter)
if err != nil {
log.Println("Failed to write chapter", chapter.ID, ":", err)
continue
}
}
log.Println("Dumping comments...")
comments, err := db.Comments().List(ctx, models.CommentFilter{})
if err != nil {
log.Println("Failed to get comments:", err)
}
if len(comments) == 0 {
log.Println("No comments to dump.")
}
for i, comment := range comments {
if !*flagIncludeUnlisted && blacklist[comment.ChapterID] {
continue
}
err := writeJsonFile(zipWriter, "comment", i, comment.ID, comment.CreatedDate, comment)
if err != nil {
log.Println("Failed to write comment", comment.ID, ":", err)
continue
}
}
log.Println("Dumping posts...")
posts, err := db.Posts().List(ctx, models.PostFilter{})
if err != nil {
log.Println("Failed to get posts:", err)
}
if len(posts) == 0 {
log.Println("No posts to dump.")
}
for i, post := range posts {
err := writeJsonFile(zipWriter, "post", i, post.ID, post.Time, post)
if err != nil {
log.Println("Failed to write post", post.ID, ":", err)
continue
}
}
log.Println("Dumping logs...")
logs, err := db.Logs().List(ctx, models.LogFilter{})
if err != nil {
log.Println("Failed to get logs:", err)
}
if len(logs) == 0 {
log.Println("No logs to dump.")
}
for i, logEntry := range logs {
err := writeJsonFile(zipWriter, "log", i, logEntry.ID, logEntry.Date, logEntry)
if err != nil {
log.Println("Failed to write post", logEntry.ID, ":", err)
continue
}
}
if *flagIncludeUsers {
log.Println("Dumping users...")
users, err := db.Users().List(ctx)
if err != nil {
log.Println("Failed to get users:", err)
}
if len(users) == 0 {
log.Println("No users to dump.")
}
for i, userEntry := range users {
err := writeJsonFile(zipWriter, "user", i, userEntry.ID, time.Now(), userEntry)
if err != nil {
log.Println("Failed to write user", userEntry.ID, ":", err)
continue
}
}
}
if *flagIncludeKeys {
log.Println("Dumping keys...")
keys, err := db.Keys().List(ctx, models.KeyFilter{})
if err != nil {
log.Println("Failed to get users:", err)
}
if len(keys) == 0 {
log.Println("No users to dump.")
}
for i, keyEntry := range keys {
err := writeJsonFile(zipWriter, "key", i, keyEntry.ID, time.Now(), keyEntry)
if err != nil {
log.Println("Failed to write key", keyEntry.ID, ":", err)
continue
}
}
}
}
func writeJsonFile(zw *zip.Writer, model string, idx int, id string, t time.Time, data interface{}) error {
w, err := zw.CreateHeader(&zip.FileHeader{
Name: fmt.Sprintf("rpdata_dump_v1/%s/%06d_%s.json", model, idx, id),
Modified: t,
})
if err != nil {
return err
}
return json.NewEncoder(w).Encode(data)
}
+318
View File
@@ -0,0 +1,318 @@
package main
import (
"archive/zip"
"context"
"encoding/json"
"flag"
"fmt"
"git.aiterp.net/rpdata/api/database"
"git.aiterp.net/rpdata/api/database/mongodb"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/models"
"log"
"strings"
"time"
)
var flagDriver = flag.String("driver", "postgres", "The database driver to use.")
var flagHost = flag.String("host", "127.0.0.1", "The host to connect to.")
var flagPort = flag.Int("port", 5432, "The port to connect on.")
var flagDb = flag.String("db", "rpdata", "The database name")
var flagUsername = flag.String("username", "", "")
var flagPassword = flag.String("password", "", "")
var flagMechanism = flag.String("mechanism", "", "")
var flagInputFile = flag.String("infile", "dump.zip", "The file to read from.")
var flagIncludeKeys = flag.Bool("include-keys", false, "Whether to include the keys.")
var flagReplace = flag.Bool("replace", false, "Replace existing content")
var flagOnly = flag.String("only", "", "Only restore these models (e.g. \"log,post\")")
func main() {
flag.Parse()
cfg := config.Database{
Driver: *flagDriver,
Host: *flagHost,
Port: *flagPort,
Db: *flagDb,
Username: *flagUsername,
Password: *flagPassword,
Mechanism: *flagMechanism,
RestoreIDs: true,
}
var only map[string]bool
if *flagOnly != "" {
only = make(map[string]bool)
for _, part := range strings.Split(*flagOnly, ",") {
only[strings.Trim(part, "  \t\n\r")] = true
}
}
mongodb.DisableFixes = true
db, err := database.Init(cfg)
if err != nil {
log.Fatalln("Failed to open database:", err)
}
cfg2 := cfg
cfg2.RestoreIDs = false
zipReader, err := zip.OpenReader(*flagInputFile)
if err != nil {
log.Fatalln("Failed to open input file:", err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Fatalln("Failed to close input file:", err)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*30)
defer cancel()
postMap := make(map[string][]*models.Post)
for _, file := range zipReader.File {
if strings.HasSuffix(file.Name, "/") {
continue
}
parts := strings.Split(file.Name, "/")
if len(parts) < 3 || parts[0] != "rpdata_dump_v1" {
log.Fatalln("Unrecognized file path:", file.Name)
}
if only != nil && !only[parts[1]] {
continue
}
reader, err := file.Open()
if err != nil {
log.Fatalln("Unrecognized file:", file.Name, err)
}
hideList := make(map[string]bool)
log.Println("Loaded", parts[1], parts[2], "from archive.")
switch parts[1] {
case "character":
{
character := models.Character{}
err := json.NewDecoder(reader).Decode(&character)
if err != nil {
log.Fatalln("Could not parse character:", parts[2], err)
}
if *flagReplace {
_ = db.Characters().Delete(ctx, character)
}
_, err = db.Characters().Insert(ctx, character)
if err != nil {
log.Fatalln("Could not insert character:", parts[2], err)
}
log.Println("Character", character.Name, "inserted.")
}
case "channel":
{
channel := models.Channel{}
err := json.NewDecoder(reader).Decode(&channel)
if err != nil {
log.Fatalln("Could not parse channel:", parts[2], err)
}
if *flagReplace {
_ = db.Channels().Remove(ctx, channel)
}
_, err = db.Channels().Insert(ctx, channel)
if err != nil {
log.Fatalln("Could not insert channel:", parts[2], err)
}
log.Println("Channel", channel.Name, "inserted.")
}
case "change":
{
change := models.Change{}
err := json.NewDecoder(reader).Decode(&change)
if err != nil {
log.Fatalln("Could not parse character:", parts[2], err)
}
if *flagReplace {
_ = db.Changes().Remove(ctx, change)
}
_, err = db.Changes().Insert(ctx, change)
if err != nil {
log.Fatalln("Could not insert character:", parts[2], err)
}
if change.Listed {
log.Println("Change", change.ID, "inserted.")
} else {
log.Println("Unlisted change inserted.")
}
}
case "story":
{
story := models.Story{}
err := json.NewDecoder(reader).Decode(&story)
if err != nil {
log.Fatalln("Could not parse story:", parts[2], err)
}
if *flagReplace {
_ = db.Stories().Delete(ctx, story)
}
_, err = db.Stories().Insert(ctx, story)
if err != nil {
log.Fatalln("Could not insert story:", parts[2], err)
}
if story.Listed {
log.Println("Story", story.Name, "inserted.")
} else {
log.Println("Unlisted story inserted.")
hideList[story.ID] = true
}
}
case "chapter":
{
chapter := models.Chapter{}
err := json.NewDecoder(reader).Decode(&chapter)
if err != nil {
log.Fatalln("Could not parse story:", parts[2], err)
}
if *flagReplace {
_ = db.Chapters().Delete(ctx, chapter)
}
_, err = db.Chapters().Insert(ctx, chapter)
if err != nil {
log.Fatalln("Could not insert story:", parts[2], err)
}
if !hideList[chapter.StoryID] {
log.Println("Chapter", fmt.Sprintf("%s (id: %s)", chapter.Title, chapter.ID), "inserted.")
} else {
log.Println("Unlisted chapter inserted.")
hideList[chapter.ID] = true
}
}
case "comment":
{
comment := models.Comment{}
err := json.NewDecoder(reader).Decode(&comment)
if err != nil {
log.Fatalln("Could not parse story:", parts[2], err)
}
if *flagReplace {
_ = db.Comments().Delete(ctx, comment)
}
_, err = db.Comments().Insert(ctx, comment)
if err != nil {
log.Fatalln("Could not insert story:", parts[2], err)
}
if !hideList[comment.ChapterID] {
log.Println("Comment", comment.Subject, "inserted.")
} else {
log.Println("Unlisted comment inserted.")
}
}
case "log":
{
logg := models.Log{}
err := json.NewDecoder(reader).Decode(&logg)
if err != nil {
log.Fatalln("Could not parse log:", parts[2], err)
}
if *flagReplace {
_ = db.Logs().Delete(ctx, logg)
}
_, err = db.Logs().Insert(ctx, logg)
if err != nil {
log.Fatalln("Could not insert log:", parts[2], err)
}
log.Println("Log", logg.Date.Format(time.RFC3339)[:16], logg.ChannelName, "inserted.")
}
case "post":
{
post := models.Post{}
err := json.NewDecoder(reader).Decode(&post)
if err != nil {
log.Fatalln("Could not parse post:", parts[2], err)
}
postMap[post.LogID] = append(postMap[post.LogID], &post)
}
case "user":
{
user := models.User{}
err := json.NewDecoder(reader).Decode(&user)
if err != nil {
log.Fatalln("Could not parse post:", parts[2], err)
}
_, err = db.Users().Insert(ctx, user)
if err != nil {
log.Fatalln("Could not insert user:", parts[2], err)
}
log.Println("User", user.ID, "inserted.")
}
case "key":
{
if !*flagIncludeKeys {
break
}
key := models.Key{}
err := json.NewDecoder(reader).Decode(&key)
if err != nil {
log.Fatalln("Could not parse key:", parts[2], err)
}
_, err = db.Keys().Insert(ctx, key)
if err != nil {
log.Fatalln("Could not insert key:", parts[2], err)
}
log.Println("Key", key.ID, "inserted.")
}
}
_ = reader.Close()
}
if only == nil || only["post"] {
for _, posts := range postMap {
log.Printf("Inserting %d posts for log %s...", len(posts), posts[0].LogID)
_, err = db.Posts().InsertMany(ctx, posts...)
if err != nil {
log.Fatalln("Could not insert post for logId:", posts[0].LogID, err)
}
log.Println("\tDone!")
}
}
}
+4 -18
View File
@@ -3,20 +3,18 @@ package main
import (
"context"
"fmt"
"git.aiterp.net/rpdata/api/space"
"log"
"net/http"
"runtime/debug"
"strings"
"git.aiterp.net/rpdata/api/database"
"git.aiterp.net/rpdata/api/graph2"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/internal/instrumentation"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/services"
"git.aiterp.net/rpdata/api/space"
"github.com/99designs/gqlgen/handler"
"github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
"runtime/debug"
)
func main() {
@@ -54,11 +52,6 @@ func main() {
if err != nil {
log.Println("Refresh Characters:", err)
}
err = serviceBundle.Logs.FixImportDateBug(context.Background())
if err != nil {
log.Println("Fix date bug:", err)
}
}()
go logListedChanges(serviceBundle.Changes)
@@ -112,13 +105,6 @@ func queryHandler(services *services.Bundle) http.HandlerFunc {
handler.ResolverMiddleware(instrumentation.ResolverMiddleware()),
)
return func(w http.ResponseWriter, r *http.Request) {
// >_>
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer of the curse") {
w.Header().Set("X-Emerald-Herald", "Seek souls. Larger, more powerful souls.")
w.Header().Add("X-Emerald-Herald", "Seek the king, that is the only way.")
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
}
r = services.Auth.RequestWithToken(r)
handler.ServeHTTP(w, r)
+3
View File
@@ -3,6 +3,7 @@ package database
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/database/postgres"
"git.aiterp.net/rpdata/api/database/mongodb"
"git.aiterp.net/rpdata/api/internal/config"
@@ -34,6 +35,8 @@ func Init(config config.Database) (Database, error) {
switch config.Driver {
case "mongo", "mgo", "mongodb":
return mongodb.Init(config)
case "postgres", "psql", "pq", "postgressql":
return postgres.Connect(config)
default:
return nil, ErrDriverUnrecognized
}
+38 -12
View File
@@ -2,17 +2,20 @@ package mongodb
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"strconv"
"strings"
"time"
)
type changeRepository struct {
changes *mgo.Collection
idCounter *counter
restoreIDs bool
changes *mgo.Collection
idCounter *counter
}
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) {
@@ -31,9 +34,17 @@ func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter)
if filter.EarliestDate != nil && !filter.EarliestDate.IsZero() {
query["date"] = bson.M{"$gte": *filter.EarliestDate}
}
if filter.LatestDate != nil && !filter.LatestDate.IsZero() {
dateBson, ok := query["date"].(bson.M)
if !ok {
dateBson = bson.M{}
}
dateBson["$gte"] = *filter.EarliestDate
}
if len(filter.Keys) > 0 {
query["keys"] = bson.M{"$in": filter.Keys}
} else {
} else if !r.restoreIDs {
query["listed"] = true
}
if filter.Author != nil && *filter.Author != "" {
@@ -58,14 +69,28 @@ func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter)
}
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
next, err := r.idCounter.Increment(1)
if err != nil {
return nil, err
if !r.restoreIDs || change.ID == "" {
next, err := r.idCounter.Increment(1)
if err != nil {
return nil, err
}
change.ID = "Change_" + strconv.Itoa(next)
} else {
tokens := strings.Split(change.ID, "_")
if len(tokens) != 2 || tokens[0] != "Change" {
return nil, errors.New("Invalid change ID")
}
n, err := strconv.Atoi(tokens[1])
if err != nil {
return nil, err
}
_ = r.idCounter.Bump(n)
}
change.ID = "Change_" + strconv.Itoa(next)
err = r.changes.Insert(&change)
err := r.changes.Insert(&change)
if err != nil {
return nil, err
}
@@ -77,7 +102,7 @@ func (r *changeRepository) Remove(ctx context.Context, change models.Change) err
return r.changes.RemoveId(change.ID)
}
func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error) {
func newChangeRepository(db *mgo.Database, restoreIDs bool) (repositories.ChangeRepository, error) {
collection := db.C("common.changes")
// Delete the old index if it exists.
@@ -103,7 +128,8 @@ func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error
}
return &changeRepository{
changes: collection,
idCounter: newCounter(db, "auto_increment", "Change"),
restoreIDs: restoreIDs,
changes: collection,
idCounter: newCounter(db, "auto_increment", "Change"),
}, nil
}
+16 -6
View File
@@ -2,20 +2,23 @@ package mongodb
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"log"
"strings"
)
type chapterRepository struct {
chapters *mgo.Collection
comments *mgo.Collection
restoreIDs bool
chapters *mgo.Collection
comments *mgo.Collection
}
func newChapterRepository(db *mgo.Database) (repositories.ChapterRepository, error) {
func newChapterRepository(db *mgo.Database, restoreIDs bool) (repositories.ChapterRepository, error) {
collection := db.C("story.chapters")
err := collection.EnsureIndexKey("storyId")
@@ -32,8 +35,9 @@ func newChapterRepository(db *mgo.Database) (repositories.ChapterRepository, err
}
return &chapterRepository{
chapters: collection,
comments: db.C("story.comments"),
restoreIDs: restoreIDs,
chapters: collection,
comments: db.C("story.comments"),
}, nil
}
@@ -67,7 +71,13 @@ func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilte
}
func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) {
chapter.ID = generate.StoryID()
if !r.restoreIDs {
chapter.ID = generate.ChapterID()
} else {
if len(chapter.ID) != len(generate.ChapterID()) && strings.HasPrefix(chapter.ID, "S") {
return nil, errors.New("invalid story id")
}
}
err := r.chapters.Insert(chapter)
if err != nil {
+21 -7
View File
@@ -15,9 +15,10 @@ import (
type characterRepository struct {
characters *mgo.Collection
cidCounter *counter
restoreIDs bool
}
func newCharacterRepository(db *mgo.Database) (repositories.CharacterRepository, error) {
func newCharacterRepository(db *mgo.Database, restoreIDs bool) (repositories.CharacterRepository, error) {
collection := db.C("common.characters")
err := collection.EnsureIndexKey("name")
@@ -52,6 +53,7 @@ func newCharacterRepository(db *mgo.Database) (repositories.CharacterRepository,
}
return &characterRepository{
restoreIDs: restoreIDs,
characters: collection,
cidCounter: newCounter(db, "auto_increment", "Character"),
}, nil
@@ -150,13 +152,25 @@ func (r *characterRepository) List(ctx context.Context, filter models.CharacterF
}
func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) {
nextId, err := r.cidCounter.Increment(1)
if err != nil {
return nil, err
}
character.ID = "C" + strconv.Itoa(nextId)
if !r.restoreIDs {
nextId, err := r.cidCounter.Increment(1)
if err != nil {
return nil, err
}
character.ID = "C" + strconv.Itoa(nextId)
} else {
n, err := strconv.Atoi(character.ID[1:])
if err != nil {
return nil, err
}
err = r.characters.Insert(&character)
err = r.cidCounter.Bump(n)
if err != nil {
return nil, err
}
}
err := r.characters.Insert(&character)
if err != nil {
return nil, err
}
+14 -4
View File
@@ -2,19 +2,22 @@ package mongodb
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"log"
"strings"
)
type commentRepository struct {
comments *mgo.Collection
restoreIDs bool
comments *mgo.Collection
}
func newCommentRepository(db *mgo.Database) (repositories.CommentRepository, error) {
func newCommentRepository(db *mgo.Database, restoreIDs bool) (repositories.CommentRepository, error) {
collection := db.C("story.comments")
err := collection.EnsureIndexKey("chapterId")
@@ -31,7 +34,8 @@ func newCommentRepository(db *mgo.Database) (repositories.CommentRepository, err
}
r := &commentRepository{
comments: collection,
restoreIDs: restoreIDs,
comments: collection,
}
go r.fixFieldTypo()
@@ -69,7 +73,13 @@ func (r *commentRepository) List(ctx context.Context, filter models.CommentFilte
}
func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) {
comment.ID = generate.CommentID()
if !r.restoreIDs {
comment.ID = generate.CommentID()
} else {
if len(comment.ID) != len(generate.CommentID()) && strings.HasPrefix(comment.ID, "SSC") {
return nil, errors.New("invalid story id")
}
}
err := r.comments.Insert(comment)
if err != nil {
+36 -9
View File
@@ -11,6 +11,8 @@ import (
"github.com/globalsign/mgo"
)
var DisableFixes bool
type MongoDB struct {
session *mgo.Session
@@ -103,7 +105,7 @@ func Init(cfg config.Database) (*MongoDB, error) {
db := session.DB(cfg.Db)
characters, err := newCharacterRepository(db)
characters, err := newCharacterRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
@@ -115,19 +117,19 @@ func Init(cfg config.Database) (*MongoDB, error) {
return nil, err
}
changes, err := newChangeRepository(db)
changes, err := newChangeRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
logs, err := newLogRepository(db)
logs, err := newLogRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
posts, err := newPostRepository(db)
posts, err := newPostRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
@@ -139,25 +141,25 @@ func Init(cfg config.Database) (*MongoDB, error) {
return nil, err
}
stories, err := newStoryRepository(db)
stories, err := newStoryRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
chapters, err := newChapterRepository(db)
chapters, err := newChapterRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
comments, err := newCommentRepository(db)
comments, err := newCommentRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
keys, err := newKeyRepository(db)
keys, err := newKeyRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
@@ -169,7 +171,9 @@ func Init(cfg config.Database) (*MongoDB, error) {
return nil, err
}
go posts.fixPositions(logs)
if !DisableFixes {
go posts.fixPositions(logs)
}
return &MongoDB{
session: session,
@@ -227,6 +231,29 @@ func (c *counter) With(category, name string) *counter {
}
}
func (c *counter) Bump(amount int) error {
id := c.category + "." + c.name
err := c.coll.Update(bson.M{
"_id": id,
"value": bson.M{"$lt": amount},
}, bson.M{
"value": amount,
})
if err != nil {
if err == mgo.ErrNotFound {
return c.coll.Insert(bson.M{
"_id": id,
"value": amount,
})
}
return err
}
return nil
}
func (c *counter) Increment(amount int) (int, error) {
type counterDoc struct {
ID string `bson:"_id"`
+10 -4
View File
@@ -10,10 +10,11 @@ import (
)
type keyRepository struct {
keys *mgo.Collection
restoreIDs bool
keys *mgo.Collection
}
func newKeyRepository(db *mgo.Database) (repositories.KeyRepository, error) {
func newKeyRepository(db *mgo.Database, restoreIDs bool) (repositories.KeyRepository, error) {
collection := db.C("auth.keys")
err := collection.EnsureIndexKey("user")
@@ -21,7 +22,10 @@ func newKeyRepository(db *mgo.Database) (repositories.KeyRepository, error) {
return nil, err
}
return &keyRepository{keys: collection}, nil
return &keyRepository{
keys: collection,
restoreIDs: restoreIDs,
}, nil
}
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
@@ -54,7 +58,9 @@ func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*m
}
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
key.ID = generate.KeyID()
if !r.restoreIDs {
key.ID = generate.KeyID()
}
err := r.keys.Insert(&key)
if err != nil {
+21 -10
View File
@@ -12,14 +12,15 @@ import (
)
type logRepository struct {
openMutex sync.Mutex
openMutex sync.Mutex
restoreIds bool
logs *mgo.Collection
posts *mgo.Collection
shortIdCounter *counter
}
func newLogRepository(db *mgo.Database) (*logRepository, error) {
func newLogRepository(db *mgo.Database, restoreIds bool) (*logRepository, error) {
logs := db.C("logbot3.logs")
posts := db.C("logbot3.posts")
@@ -54,6 +55,7 @@ func newLogRepository(db *mgo.Database) (*logRepository, error) {
}
return &logRepository{
restoreIds: restoreIds,
logs: logs,
posts: posts,
shortIdCounter: newCounter(db, "auto_increment", "Log"),
@@ -123,26 +125,35 @@ func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*m
}
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) {
nextShortId, err := r.shortIdCounter.Increment(1)
if err != nil {
return nil, err
}
if !r.restoreIds || log.ID == "" || log.ShortID == "" {
nextShortId, err := r.shortIdCounter.Increment(1)
if err != nil {
return nil, err
}
log.ID = generate.LogID(log)
log.ShortID = "L" + strconv.Itoa(nextShortId)
log.ID = generate.LogID(log)
log.ShortID = "L" + strconv.Itoa(nextShortId)
} else {
n, err := strconv.Atoi(log.ShortID[1:])
if err != nil {
return nil, err
}
_ = r.shortIdCounter.Bump(n)
}
if log.Open {
// There can be only one open log in the same channel.
r.openMutex.Lock()
defer r.openMutex.Unlock()
_, err = r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
_, err := r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
if err != nil {
return nil, errors.New("Cannot close other logs: " + err.Error())
}
}
err = r.logs.Insert(&log)
err := r.logs.Insert(&log)
if err != nil {
return nil, err
}
+25 -8
View File
@@ -2,6 +2,7 @@ package mongodb
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
@@ -14,13 +15,14 @@ import (
)
type postRepository struct {
logs *mgo.Collection
posts *mgo.Collection
restoreIDs bool
logs *mgo.Collection
posts *mgo.Collection
orderMutex sync.Mutex
}
func newPostRepository(db *mgo.Database) (*postRepository, error) {
func newPostRepository(db *mgo.Database, restoreIDs bool) (*postRepository, error) {
posts := db.C("logbot3.posts")
err := posts.EnsureIndexKey("logId")
@@ -48,8 +50,9 @@ func newPostRepository(db *mgo.Database) (*postRepository, error) {
}
return &postRepository{
posts: posts,
logs: db.C("logbot3.logs"),
restoreIDs: restoreIDs,
posts: posts,
logs: db.C("logbot3.logs"),
}, nil
}
@@ -91,7 +94,12 @@ func (r *postRepository) List(ctx context.Context, filter models.PostFilter) ([]
}
posts := make([]*models.Post, 0, 32)
err := r.posts.Find(query).Sort("-logId", "position").Limit(filter.Limit).All(&posts)
var err error
if filter.LogID != nil {
err = r.posts.Find(query).Sort("position").Limit(filter.Limit).All(&posts)
} else {
err = r.posts.Find(query).Limit(filter.Limit).All(&posts)
}
if err != nil {
if err == mgo.ErrNotFound {
return []*models.Post{}, nil
@@ -113,7 +121,14 @@ func (r *postRepository) Insert(ctx context.Context, post models.Post) (*models.
return nil, err
}
post.ID = generate.PostID()
if !r.restoreIDs {
post.ID = generate.PostID()
} else {
if len(post.ID) != len(generate.PostID()) && strings.HasPrefix(post.ID, "P") {
return nil, errors.New("invalid story id")
}
}
post.Position = lastPost.Position + 1 // Position 1 is first position, so this is safe.
err = r.posts.Insert(post)
@@ -135,7 +150,9 @@ func (r *postRepository) InsertMany(ctx context.Context, posts ...*models.Post)
return nil, repositories.ErrParentMismatch
}
post.ID = generate.PostID()
if !r.restoreIDs || post.ID == "" {
post.ID = generate.PostID()
}
}
r.orderMutex.Lock()
+22 -8
View File
@@ -2,21 +2,24 @@ package mongodb
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"log"
"strings"
)
type storyRepository struct {
stories *mgo.Collection
chapters *mgo.Collection
comments *mgo.Collection
stories *mgo.Collection
chapters *mgo.Collection
comments *mgo.Collection
restoreIDs bool
}
func newStoryRepository(db *mgo.Database) (repositories.StoryRepository, error) {
func newStoryRepository(db *mgo.Database, restoreIDs bool) (repositories.StoryRepository, error) {
collection := db.C("story.stories")
err := collection.EnsureIndexKey("tags")
@@ -41,9 +44,10 @@ func newStoryRepository(db *mgo.Database) (repositories.StoryRepository, error)
}
return &storyRepository{
stories: collection,
chapters: db.C("story.chapters"),
comments: db.C("story.comments"),
stories: collection,
restoreIDs: restoreIDs,
chapters: db.C("story.chapters"),
comments: db.C("story.comments"),
}, nil
}
@@ -105,7 +109,13 @@ func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) (
}
func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) {
story.ID = generate.StoryID()
if !r.restoreIDs {
story.ID = generate.StoryID()
} else {
if len(story.ID) != len(generate.StoryID()) && strings.HasPrefix(story.ID, "S") {
return nil, errors.New("invalid story id")
}
}
err := r.stories.Insert(story)
if err != nil {
@@ -145,6 +155,10 @@ func (r *storyRepository) Update(ctx context.Context, story models.Story, update
updateBson["listed"] = *update.Listed
story.Listed = *update.Listed
}
if update.SortByFictionalDate != nil {
updateBson["sortByFictionalDate"] = *update.SortByFictionalDate
story.SortByFictionalDate = *update.SortByFictionalDate
}
err := r.stories.UpdateId(story.ID, bson.M{"$set": updateBson})
if err != nil {
+11
View File
@@ -5,6 +5,7 @@ import (
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
type userRepository struct {
@@ -31,6 +32,16 @@ func (r *userRepository) Find(ctx context.Context, id string) (*models.User, err
return user, nil
}
func (r *userRepository) List(ctx context.Context) ([]*models.User, error) {
users := make([]*models.User, 0, 8)
err := r.users.Find(bson.M{}).All(&users)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
return users, nil
}
func (r *userRepository) Insert(ctx context.Context, user models.User) (*models.User, error) {
err := r.users.Insert(user)
if err != nil {
+180
View File
@@ -0,0 +1,180 @@
package postgres
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/models"
"strconv"
"time"
)
type changeRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) {
row, err := psqlcore.New(r.db).SelectChangeByID(ctx, id)
if err != nil {
return nil, err
}
return r.change(row)
}
func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) {
params := psqlcore.SelectChangesParams{LimitSize: 0}
if filter.Keys != nil {
params.FilterKeys = true
keys := make([]string, len(filter.Keys))
for i, ck := range filter.Keys {
keys[i] = ck.String()
}
params.Keys = keys
}
if filter.Author != nil {
params.FilterAuthor = true
params.Author = *filter.Author
}
if filter.EarliestDate != nil {
params.FilterEarliestDate = true
params.EarliestDate = filter.EarliestDate.In(time.UTC)
}
if filter.LatestDate != nil {
params.FilterLatestDate = true
params.LatestDate = filter.LatestDate.In(time.UTC)
}
if filter.Limit != nil && *filter.Limit <= 0 {
params.LimitSize = int32(*filter.Limit)
}
rows, err := psqlcore.New(r.db).SelectChanges(ctx, params)
if err != nil {
return nil, err
}
return r.changes(rows)
}
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
if !r.insertWithIDs || change.ID == "" {
next, err := q.IncrementCounter(ctx, "data_change_id")
if err != nil {
return nil, err
}
change.ID = fmt.Sprintf("Change_%d", next)
} else {
if len(change.ID) < 8 {
return nil, errors.New("invalid change id")
}
n, err := strconv.Atoi(change.ID[7:])
if err != nil {
return nil, err
}
err = q.BumpCounter(ctx, psqlcore.BumpCounterParams{ID: "data_change_id", Value: int32(n)})
if err != nil {
return nil, err
}
}
data, err := r.dataChange(change)
if err != nil {
return nil, err
}
err = q.InsertChange(ctx, psqlcore.InsertChangeParams(*data))
if err != nil {
return nil, err
}
return &change, tx.Commit()
}
func (r *changeRepository) Remove(ctx context.Context, change models.Change) error {
return psqlcore.New(r.db).DeleteChange(ctx, change.ID)
}
func (r *changeRepository) dataChange(change models.Change) (*psqlcore.DataChange, error) {
objectsJSON, err := json.Marshal(change.ChangeObjectSet)
if err != nil {
return nil, err
}
keys := make([]string, len(change.Keys))
for i, ck := range change.Keys {
keys[i] = ck.String()
}
return &psqlcore.DataChange{
ID: change.ID,
Model: string(change.Model),
Op: change.Op,
Author: change.Author,
Listed: change.Listed,
Date: change.Date.In(time.UTC),
Keys: keys,
Objects: objectsJSON,
}, nil
}
func (r *changeRepository) change(row psqlcore.DataChange) (*models.Change, error) {
objects := models.ChangeObjectSet{}
err := json.Unmarshal(row.Objects, &objects)
if err != nil {
return nil, err
}
keys := make([]models.ChangeKey, len(row.Keys))
for i := range keys {
err := keys[i].Decode(row.Keys[i])
if err != nil {
return nil, err
}
}
model := models.ChangeModel(row.Model)
if !model.IsValid() {
return nil, errors.New("invalid model")
}
return &models.Change{
ID: row.ID,
Model: model,
Op: row.Op,
Author: row.Author,
Listed: row.Listed,
Date: row.Date,
Keys: keys,
ChangeObjectSet: objects,
}, nil
}
func (r *changeRepository) changes(rows []psqlcore.DataChange) ([]*models.Change, error) {
results := make([]*models.Change, len(rows))
for i, row := range rows {
change, err := r.change(row)
if err != nil {
return nil, err
}
results[i] = change
}
return results, nil
}
+104
View File
@@ -0,0 +1,104 @@
package postgres
import (
"context"
"database/sql"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/models"
)
type channelRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *channelRepository) Find(ctx context.Context, name string) (*models.Channel, error) {
row, err := psqlcore.New(r.db).SelectChannelByName(ctx, name)
if err != nil {
return nil, err
}
return r.channel(row), nil
}
func (r *channelRepository) List(ctx context.Context, filter models.ChannelFilter) ([]*models.Channel, error) {
params := psqlcore.SelectChannelsParams{
LimitSize: 0,
}
if filter.Names != nil {
params.FilterName = true
params.Names = filter.Names
}
if filter.LocationName != nil {
params.FilterLocationName = true
params.LocationName = *filter.LocationName
}
if filter.EventName != nil {
params.FilterEventName = true
params.EventName = *filter.EventName
}
if filter.Logged != nil {
params.FilterLogged = true
params.Logged = *filter.Logged
}
if filter.Limit > 0 {
params.LimitSize = int32(filter.Limit)
}
rows, err := psqlcore.New(r.db).SelectChannels(ctx, params)
if err != nil {
return nil, err
}
return r.channels(rows), nil
}
func (r *channelRepository) Insert(ctx context.Context, channel models.Channel) (*models.Channel, error) {
err := psqlcore.New(r.db).InsertChannel(ctx, psqlcore.InsertChannelParams(channel))
if err != nil {
return nil, err
}
return &channel, nil
}
func (r *channelRepository) Update(ctx context.Context, channel models.Channel, update models.ChannelUpdate) (*models.Channel, error) {
channel.ApplyUpdate(update)
err := psqlcore.New(r.db).UpdateChannel(ctx, psqlcore.UpdateChannelParams{
Name: channel.Name,
Logged: channel.Logged,
Hub: channel.Hub,
EventName: channel.EventName,
LocationName: channel.LocationName,
})
if err != nil {
return nil, err
}
return &channel, nil
}
func (r *channelRepository) Remove(ctx context.Context, channel models.Channel) error {
return psqlcore.New(r.db).DeleteChannel(ctx, channel.Name)
}
func (r *channelRepository) channel(row psqlcore.DataChannel) *models.Channel {
return &models.Channel{
Name: row.Name,
Logged: row.Logged,
Hub: row.Hub,
EventName: row.EventName,
LocationName: row.LocationName,
}
}
func (r *channelRepository) channels(rows []psqlcore.DataChannel) []*models.Channel {
results := make([]*models.Channel, 0, len(rows))
for _, row := range rows {
results = append(results, r.channel(row))
}
return results
}
+144
View File
@@ -0,0 +1,144 @@
package postgres
import (
"context"
"database/sql"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
)
type chapterRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) {
chapter, err := psqlcore.New(r.db).SelectChapter(ctx, id)
if err != nil {
return nil, err
}
return r.chapter(chapter), nil
}
func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) {
params := psqlcore.SelectChaptersParams{
StoryID: "",
LimitSize: 0,
}
if filter.StoryID != nil {
params.StoryID = *filter.StoryID
}
if filter.Limit > 0 {
params.LimitSize = int32(filter.Limit)
}
chapters, err := psqlcore.New(r.db).SelectChapters(ctx, params)
if err != nil {
return nil, err
}
return r.chapters(chapters), nil
}
func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) {
if !r.insertWithIDs || len(chapter.ID) < 8 {
chapter.ID = generate.ChapterID()
}
err := psqlcore.New(r.db).InsertChapter(ctx, psqlcore.InsertChapterParams{
ID: chapter.ID,
StoryID: chapter.StoryID,
Title: chapter.Title,
Author: chapter.Author,
Source: chapter.Source,
CreatedDate: chapter.CreatedDate.UTC(),
FictionalDate: chapter.FictionalDate.UTC(),
EditedDate: chapter.EditedDate.UTC(),
CommentMode: string(chapter.CommentMode),
CommentsLocked: chapter.CommentsLocked,
})
if err != nil {
return nil, err
}
return &chapter, nil
}
func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) {
chapter.ApplyUpdate(update)
err := psqlcore.New(r.db).UpdateChapter(ctx, psqlcore.UpdateChapterParams{
Title: chapter.Title,
Source: chapter.Source,
FictionalDate: chapter.FictionalDate.UTC(),
CommentMode: string(chapter.CommentMode),
CommentsLocked: chapter.CommentsLocked,
ID: chapter.ID,
})
if err != nil {
return nil, err
}
return &chapter, nil
}
func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, _, to models.Story) (*models.Chapter, error) {
err := psqlcore.New(r.db).UpdateChapterStoryID(ctx, psqlcore.UpdateChapterStoryIDParams{
StoryID: to.ID,
ID: chapter.ID,
})
if err != nil {
return nil, err
}
chapter.StoryID = to.ID
return &chapter, nil
}
func (r *chapterRepository) Delete(ctx context.Context, chapter models.Chapter) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
err = q.DeleteCommentsByChapterID(ctx, chapter.ID)
if err != nil {
return err
}
err = q.DeleteChapter(ctx, chapter.ID)
if err != nil {
return err
}
return tx.Commit()
}
func (r *chapterRepository) chapter(chapter psqlcore.StoryChapter) *models.Chapter {
return &models.Chapter{
ID: chapter.ID,
StoryID: chapter.StoryID,
Title: chapter.Title,
Author: chapter.Author,
Source: chapter.Source,
CreatedDate: chapter.CreatedDate,
FictionalDate: chapter.FictionalDate,
EditedDate: chapter.EditedDate,
CommentMode: models.ChapterCommentMode(chapter.CommentMode),
CommentsLocked: chapter.CommentsLocked,
}
}
func (r *chapterRepository) chapters(chapters []psqlcore.StoryChapter) []*models.Chapter {
results := make([]*models.Chapter, 0, len(chapters))
for _, chapter := range chapters {
results = append(results, r.chapter(chapter))
}
return results
}
+234
View File
@@ -0,0 +1,234 @@
package postgres
import (
"context"
"database/sql"
"errors"
"fmt"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/models"
"strconv"
"strings"
)
var ErrNickConflict = errors.New("nick already in use by another character")
type characterRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *characterRepository) Find(ctx context.Context, id string) (*models.Character, error) {
row, err := psqlcore.New(r.db).SelectCharacterByID(ctx, id)
if err != nil {
return nil, err
}
return r.character(row), nil
}
func (r *characterRepository) FindNick(ctx context.Context, nick string) (*models.Character, error) {
row, err := psqlcore.New(r.db).SelectCharacterByNick(ctx, nick)
if err != nil {
return nil, err
}
return r.character(psqlcore.SelectCharacterByIDRow(row)), nil
}
func (r *characterRepository) FindName(ctx context.Context, name string) (*models.Character, error) {
row, err := psqlcore.New(r.db).SelectCharacterByName(ctx, name)
if err != nil {
return nil, err
}
return r.character(psqlcore.SelectCharacterByIDRow(row)), nil
}
func (r *characterRepository) List(ctx context.Context, filter models.CharacterFilter) ([]*models.Character, error) {
params := psqlcore.SelectCharactersParams{
LimitSize: 0,
}
if filter.IDs != nil {
params.FilterID = true
params.Ids = filter.IDs
}
if filter.Nicks != nil {
params.FilterNick = true
params.Nicks = filter.Nicks
}
if filter.Names != nil {
params.FilterName = true
params.Names = filter.Names
}
if filter.Author != nil {
params.FilterAuthor = true
params.Author = *filter.Author
}
if filter.Search != nil {
params.FilterSearch = true
params.Search = TSQueryFromSearch(*filter.Search)
}
if filter.Limit > 0 {
params.LimitSize = int32(filter.Limit)
}
rows, err := psqlcore.New(r.db).SelectCharacters(ctx, params)
if err != nil {
return nil, err
}
return r.characters(rows), nil
}
func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
if !r.insertWithIDs || character.ID == "" {
next, err := q.IncrementCounter(ctx, "data_character_id")
if err != nil {
return nil, err
}
character.ID = fmt.Sprintf("C%d", next)
} else {
n, err := strconv.Atoi(character.ID[1:])
if err != nil {
return nil, err
}
err = q.BumpCounter(ctx, psqlcore.BumpCounterParams{ID: "data_character_id", Value: int32(n)})
if err != nil {
return nil, err
}
}
rows, err := q.SelectCharacters(ctx, psqlcore.SelectCharactersParams{
FilterNick: true,
Nicks: character.Nicks,
LimitSize: 1,
})
if err != nil {
return nil, fmt.Errorf("failed to select: %s", err)
}
if len(rows) != 0 {
return nil, ErrNickConflict
}
err = q.InsertCharacter(ctx, psqlcore.InsertCharacterParams{
ID: character.ID,
Nicks: character.Nicks,
Name: character.Name,
ShortName: character.ShortName,
Author: character.Author,
Description: character.Description,
})
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
return &character, nil
}
func (r *characterRepository) Update(ctx context.Context, character models.Character, update models.CharacterUpdate) (*models.Character, error) {
character.ApplyUpdate(update)
err := psqlcore.New(r.db).UpdateCharacter(ctx, psqlcore.UpdateCharacterParams{
ID: character.ID,
Name: character.Name,
ShortName: character.ShortName,
Description: character.Description,
})
if err != nil {
return nil, err
}
return &character, nil
}
func (r *characterRepository) AddNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
rows, err := q.SelectCharacters(ctx, psqlcore.SelectCharactersParams{
FilterNick: true,
Nicks: []string{nick},
LimitSize: 1,
})
if err != nil {
return nil, err
}
if len(rows) != 0 {
return nil, ErrNickConflict
}
err = q.AddCharacterNick(ctx, psqlcore.AddCharacterNickParams{ID: character.ID, Nick: nick})
if err != nil {
return nil, err
}
character.Nicks = append(character.Nicks, nick)
err = tx.Commit()
if err != nil {
return nil, err
}
return &character, nil
}
func (r *characterRepository) RemoveNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
err := psqlcore.New(r.db).RemoveCharacterNick(ctx, psqlcore.RemoveCharacterNickParams{ID: character.ID, Nick: nick})
if err != nil {
return nil, err
}
for i, nick2 := range character.Nicks {
if nick2 == nick {
character.Nicks = append(character.Nicks[:i], character.Nicks[i+1:]...)
break
}
}
return &character, nil
}
func (r *characterRepository) Delete(ctx context.Context, character models.Character) error {
return psqlcore.New(r.db).DeleteCharacter(ctx, character.ID)
}
func (r *characterRepository) character(row psqlcore.SelectCharacterByIDRow) *models.Character {
return &models.Character{
ID: strings.Trim(row.ID, " "),
Nicks: row.Nicks,
Name: row.Name,
ShortName: row.ShortName,
Author: row.Author,
Description: row.Description,
}
}
func (r *characterRepository) characters(rows []psqlcore.SelectCharactersRow) []*models.Character {
results := make([]*models.Character, 0, len(rows))
for _, row := range rows {
results = append(results, r.character(psqlcore.SelectCharacterByIDRow(row)))
}
return results
}
+114
View File
@@ -0,0 +1,114 @@
package postgres
import (
"context"
"database/sql"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
)
type commentRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *commentRepository) Find(ctx context.Context, id string) (*models.Comment, error) {
comment, err := psqlcore.New(r.db).SelectComment(ctx, id)
if err != nil {
return nil, err
}
return r.comment(comment), nil
}
func (r *commentRepository) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) {
params := psqlcore.SelectCommentsParams{
ChapterID: "",
LimitSize: 0,
}
if filter.ChapterID != nil {
params.ChapterID = *filter.ChapterID
}
if filter.Limit > 0 {
params.LimitSize = int32(filter.Limit)
}
comments, err := psqlcore.New(r.db).SelectComments(ctx, params)
if err != nil {
return nil, err
}
return r.comments(comments), nil
}
func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) {
if !r.insertWithIDs || len(comment.ID) < 8 {
comment.ID = generate.CommentID()
}
err := psqlcore.New(r.db).InsertComment(ctx, psqlcore.InsertCommentParams{
ID: comment.ID,
ChapterID: comment.ChapterID,
CharacterID: comment.CharacterID,
Subject: comment.Subject,
Author: comment.Author,
CharacterName: comment.CharacterName,
Source: comment.Source,
CreatedDate: comment.CreatedDate.UTC(),
FictionalDate: comment.FictionalDate.UTC(),
EditedDate: comment.EditedDate.UTC(),
})
if err != nil {
return nil, err
}
return &comment, nil
}
func (r *commentRepository) Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) {
comment.ApplyUpdate(update)
err := psqlcore.New(r.db).UpdateComment(ctx, psqlcore.UpdateCommentParams{
Subject: comment.Subject,
Source: comment.Source,
FictionalDate: comment.FictionalDate.UTC(),
EditedDate: comment.EditedDate.UTC(),
CharacterName: comment.CharacterName,
CharacterID: comment.CharacterID,
ID: comment.ID,
})
if err != nil {
return nil, err
}
return &comment, nil
}
func (r *commentRepository) Delete(ctx context.Context, comment models.Comment) error {
return psqlcore.New(r.db).DeleteComment(ctx, comment.ID)
}
func (r *commentRepository) comment(comment psqlcore.StoryComment) *models.Comment {
return &models.Comment{
ID: comment.ID,
ChapterID: comment.ChapterID,
Subject: comment.Subject,
Author: comment.Author,
CharacterName: comment.CharacterName,
CharacterID: comment.CharacterID,
FictionalDate: comment.FictionalDate,
CreatedDate: comment.CreatedDate,
EditedDate: comment.EditedDate,
Source: comment.Source,
}
}
func (r *commentRepository) comments(comments []psqlcore.StoryComment) []*models.Comment {
results := make([]*models.Comment, 0, len(comments))
for _, comment := range comments {
results = append(results, r.comment(comment))
}
return results
}
+107
View File
@@ -0,0 +1,107 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/repositories"
"time"
)
func Connect(cfg config.Database) (*DB, error) {
sslMode := "disable"
if cfg.SSL {
sslMode = "enable"
}
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Db, sslMode,
)
timeout, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
db, err := sql.Open("postgres", psqlInfo)
if err != nil {
return nil, err
}
if err := db.PingContext(timeout); err != nil {
return nil, err
}
db.SetMaxIdleConns(32)
q := psqlcore.New(db)
if err := q.EnsureCounter(timeout, "data_character_id"); err != nil {
return nil, err
}
if err := q.EnsureCounter(timeout, "data_change_id"); err != nil {
return nil, err
}
if err := q.EnsureCounter(timeout, "log_short_id"); err != nil {
return nil, err
}
return &DB{
db: db,
insertWithIDs: cfg.RestoreIDs,
}, nil
}
type DB struct {
insertWithIDs bool
db *sql.DB
}
func (d *DB) Changes() repositories.ChangeRepository {
return &changeRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Channels() repositories.ChannelRepository {
return &channelRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Characters() repositories.CharacterRepository {
return &characterRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Tags() repositories.TagRepository {
return &tagRepository{db: d.db}
}
func (d *DB) Logs() repositories.LogRepository {
return &logRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Posts() repositories.PostRepository {
return &postRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Stories() repositories.StoryRepository {
return &storyRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Chapters() repositories.ChapterRepository {
return &chapterRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Comments() repositories.CommentRepository {
return &commentRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Keys() repositories.KeyRepository {
return &keyRepository{insertWithIDs: d.insertWithIDs, db: d.db}
}
func (d *DB) Users() repositories.UserRepository {
return &userRepository{db: d.db}
}
func (d *DB) Files() repositories.FileRepository {
panic("implement me")
}
func (d *DB) Close(context.Context) error {
return d.db.Close()
}
+82
View File
@@ -0,0 +1,82 @@
package postgres
import (
"context"
"database/sql"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
)
type keyRepository struct {
db *sql.DB
insertWithIDs bool
}
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
key, err := psqlcore.New(r.db).SelectKey(ctx, id)
if err != nil {
return nil, err
}
return &models.Key{
ID: key.ID,
Name: key.Name,
User: key.User,
Secret: key.Secret,
}, nil
}
func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error) {
var keys []psqlcore.Key
if filter.UserID != nil {
keys2, err := psqlcore.New(r.db).SelectKeysByUser(ctx, *filter.UserID)
if err != nil {
return nil, err
}
keys = keys2
} else {
keys2, err := psqlcore.New(r.db).SelectKeys(ctx)
if err != nil {
return nil, err
}
keys = keys2
}
results := make([]*models.Key, 0, len(keys))
for _, key := range keys {
results = append(results, &models.Key{
ID: key.ID,
Name: key.Name,
User: key.User,
Secret: key.Secret,
})
}
return results, nil
}
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
if !r.insertWithIDs || len(key.ID) < 4 {
key.ID = generate.KeyID()
}
err := psqlcore.New(r.db).InsertKey(ctx, psqlcore.InsertKeyParams{
ID: key.ID,
Name: key.Name,
UserID: key.User,
Secret: key.Secret,
})
if err != nil {
return nil, err
}
return &key, nil
}
func (r *keyRepository) Delete(ctx context.Context, key models.Key) error {
return psqlcore.New(r.db).DeleteKey(ctx, key.ID)
}
+249
View File
@@ -0,0 +1,249 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"strconv"
"time"
)
type logRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) {
log, err := psqlcore.New(r.db).SelectLog(ctx, id)
if err != nil {
return nil, err
}
return r.log(log), nil
}
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) {
q := psqlcore.New(r.db)
params := psqlcore.SelectLogsParams{
LimitSize: 0,
}
if filter.Search != nil {
params.FilterSearch = true
params.Search = TSQueryFromSearch(*filter.Search)
}
if filter.Open != nil {
params.FilterOpen = true
params.Open = *filter.Open
}
if len(filter.Characters) > 0 {
params.FilterCharacterID = true
params.CharacterIds = filter.Characters
}
if len(filter.Channels) > 0 {
params.FilterChannelName = true
params.ChannelNames = filter.Channels
}
if len(filter.Events) > 0 {
params.FilterEventName = true
params.EventNames = filter.Events
}
if filter.MinDate != nil {
params.FilterEarlistDate = true
params.EarliestDate = *filter.MinDate
}
if filter.MaxDate != nil {
params.FilterLastestDate = true
params.LatestDate = *filter.MaxDate
}
if filter.Limit > 0 {
params.LimitSize = int32(filter.Limit)
}
logs, err := q.SelectLogs(ctx, params)
if err != nil {
return nil, err
}
return r.logs(logs), nil
}
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
if !r.insertWithIDs || log.ID == "" {
log.ID = generate.LogID(log)
}
if !r.insertWithIDs || log.ShortID == "" {
next, err := q.IncrementCounter(ctx, "log_short_id")
if err != nil {
return nil, err
}
log.ShortID = fmt.Sprintf("L%d", next)
} else {
n, err := strconv.Atoi(log.ShortID[1:])
if err != nil {
return nil, err
}
err = q.BumpCounter(ctx, psqlcore.BumpCounterParams{
ID: "log_short_id",
Value: int32(n),
})
if err != nil {
return nil, err
}
}
if log.CharacterIDs == nil {
log.CharacterIDs = []string{}
}
err = q.InsertLog(ctx, psqlcore.InsertLogParams{
ID: log.ID,
ShortID: log.ShortID,
Date: log.Date.UTC(),
ChannelName: log.ChannelName,
EventName: log.EventName,
Title: log.Title,
Description: log.Description,
Open: log.Open,
CharacterIds: log.CharacterIDs,
})
if err != nil {
return nil, err
}
err = tx.Commit()
if err != nil {
return nil, err
}
_ = r.updateTags(log)
return &log, nil
}
func (r *logRepository) Update(ctx context.Context, log models.Log, update models.LogUpdate) (*models.Log, error) {
log.ApplyUpdate(update)
err := psqlcore.New(r.db).UpdateLog(ctx, psqlcore.UpdateLogParams{
Title: log.Title,
EventName: log.EventName,
Description: log.Description,
Open: log.Open,
CharacterIds: log.CharacterIDs,
ID: log.ID,
})
if err != nil {
return nil, err
}
_ = r.updateTags(log)
return &log, nil
}
func (r *logRepository) Delete(ctx context.Context, log models.Log) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
err = q.DeleteLog(ctx, log.ID)
if err != nil {
return err
}
err = q.DeletePostsByLogShortID(ctx, log.ShortID)
if err != nil {
return err
}
return tx.Commit()
}
func (r *logRepository) log(log psqlcore.SelectLogRow) *models.Log {
return &models.Log{
ID: log.ID,
ShortID: log.ShortID,
Date: log.Date,
ChannelName: log.ChannelName,
EventName: log.EventName,
Title: log.Title,
Description: log.Description,
Open: log.Open,
CharacterIDs: log.CharacterIds,
}
}
func (r *logRepository) logs(logs []psqlcore.SelectLogsRow) []*models.Log {
results := make([]*models.Log, 0, len(logs))
for _, log := range logs {
results = append(results, r.log(psqlcore.SelectLogRow(log)))
}
return results
}
func (r *logRepository) updateTags(log models.Log) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
err = q.ClearTagsByTarget(ctx, psqlcore.ClearTagsByTargetParams{TargetKind: "Log", TargetID: log.ID})
if err != nil {
return err
}
if len(log.CharacterIDs) > 0 {
err := q.SetCharacterTagsFromIDs(ctx, psqlcore.SetCharacterTagsFromIDsParams{
TargetKind: "log",
TargetID: log.ID,
CharacterIds: log.CharacterIDs,
})
if err != nil {
return err
}
}
err = q.SetLocationTagFromChannelName(ctx, psqlcore.SetLocationTagFromChannelNameParams{
TargetKind: "log",
TargetID: log.ID,
ChannelName: log.ChannelName,
})
if err != nil {
return err
}
if log.EventName != "" {
err := q.SetTag(ctx, psqlcore.SetTagParams{
Tag: "Event:" + log.EventName,
TargetKind: "Log",
TargetID: log.ID,
})
if err != nil {
return err
}
}
return tx.Commit()
}
@@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE data_character (
id CHAR(8) NOT NULL PRIMARY KEY,
nicks TEXT[] NOT NULL,
name TEXT NOT NULL,
short_name TEXT NOT NULL,
author TEXT NOT NULL,
description TEXT NOT NULL,
ts_vector TSVECTOR NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE data_character;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_character_index_character_name ON data_character(name);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_character_index_character_name;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_character_index_character_short_name ON data_character(short_name);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_character_index_character_short_name;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_character_index_character_author ON data_character(author);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_character_index_character_author;
-- +goose StatementEnd
@@ -0,0 +1,12 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE core_counter (
id TEXT NOT NULL PRIMARY KEY,
value INTEGER
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE core_counter;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_character_index_character_nicks on data_character USING GIN (nicks);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_character_index_character_nicks;
-- +goose StatementEnd
@@ -0,0 +1,15 @@
-- +goose Up
-- +goose StatementBegin
CREATE FUNCTION immutable_array_to_string(arr ANYARRAY, sep TEXT)
RETURNS text
AS $$
SELECT array_to_string(arr, sep);
$$
LANGUAGE SQL
IMMUTABLE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP FUNCTION IF EXISTS immutable_array_to_string;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_character_index_search ON data_character USING GIN (ts_vector);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_character_index_search;
-- +goose StatementEnd
@@ -0,0 +1,15 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE data_channel (
name TEXT NOT NULL PRIMARY KEY,
logged BOOLEAN NOT NULL,
hub BOOLEAN NOT NULL,
event_name TEXT NOT NULL,
location_name TEXT NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS data_channel;
-- +goose StatementEnd
@@ -0,0 +1,18 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE data_change (
id TEXT NOT NULL PRIMARY KEY,
model TEXT NOT NULL,
op TEXT NOT NULL,
author TEXT NOT NULL,
listed BOOLEAN NOT NULL,
date TIMESTAMP NOT NULL,
keys TEXT[] NOT NULL,
objects JSONB NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS data_change;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_change_index_keys on data_change USING GIN (keys);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_change_index_keys;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_change_index_date ON data_change (date);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_change_index_date;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX data_change_index_author ON data_change (author);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS data_change_index_author;
-- +goose StatementEnd
@@ -0,0 +1,15 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE common_tag (
tag TEXT NOT NULL,
target_kind TEXT NOT NULL,
target_id TEXT NOT NULL,
PRIMARY KEY (tag, target_kind, target_id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE common_tag;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX common_tag_index_target ON common_tag (target_kind, target_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS common_tag_index_target;
-- +goose StatementEnd
@@ -0,0 +1,20 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE story (
id TEXT NOT NULL PRIMARY KEY,
author TEXT NOT NULL,
name TEXT NOT NULL,
category TEXT NOT NULL,
open BOOLEAN NOT NULL,
listed BOOLEAN NOT NULL,
sort_by_fictional_date BOOLEAN NOT NULL,
created_date TIMESTAMP NOT NULL,
fictional_date TIMESTAMP NOT NULL,
updated_date TIMESTAMP NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE story;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX story_index_author ON story (author);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS story_index_author
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX story_index_fictional_date ON story (fictional_date);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS story_index_fictional_date
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX story_index_created_date ON story (created_date);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS story_index_created_date
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX story_index_updated_date ON story (updated_date);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS story_index_updated_date
-- +goose StatementEnd
@@ -0,0 +1,20 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE story_chapter (
id TEXT NOT NULL PRIMARY KEY,
story_id TEXT NOT NULL,
title TEXT NOT NULL,
author TEXT NOT NULL,
source TEXT NOT NULL,
created_date TIMESTAMP NOT NULL,
fictional_date TIMESTAMP NOT NULL,
edited_date TIMESTAMP NOT NULL,
comment_mode TEXT NOT NULL,
comments_locked BOOLEAN NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE story_chapter;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX story_chapter_index_story_id ON story_chapter (story_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS story_chapter_index_story_id;
-- +goose StatementEnd
@@ -0,0 +1,20 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE story_comment (
id TEXT NOT NULL PRIMARY KEY,
chapter_id TEXT NOT NULL,
character_id TEXT NOT NULL,
subject TEXT NOT NULL,
author TEXT NOT NULL,
character_name TEXT NOT NULL,
source TEXT NOT NULL,
created_date TIMESTAMP NOT NULL,
fictional_date TIMESTAMP NOT NULL,
edited_date TIMESTAMP NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE story_comment;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX story_comment_index_chapter_id ON story_comment (chapter_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS story_comment_index_chapter_id;
-- +goose StatementEnd
@@ -0,0 +1,19 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE log (
id TEXT NOT NULL PRIMARY KEY,
short_id TEXT NOT NULL UNIQUE,
character_ids TEXT[] NOT NULL,
date TIMESTAMP NOT NULL,
channel_name TEXT NOT NULL,
event_name TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
open BOOLEAN NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE log;
-- +goose StatementEnd
@@ -0,0 +1,20 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE log_post (
id TEXT NOT NULL PRIMARY KEY,
log_short_id TEXT NOT NULL,
time TIMESTAMP NOT NULL,
kind TEXT NOT NULL,
nick TEXT NOT NULL,
text TEXT NOT NULL,
position INT NOT NULL,
ts_vector TSVECTOR NOT NULL,
UNIQUE (log_short_id, position)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE log_post;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_post_index_search ON log_post USING GIN (ts_vector);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_post_index_search;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_index_open ON log (open);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_index_open
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_index_character_ids on log USING GIN (character_ids);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_index_character_ids;
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_index_channel_name ON log (channel_name);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_index_channel_name
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_index_event_name ON log (event_name);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_index_event_name
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_index_date ON log (date);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_index_date
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_post_index_kind ON log_post (kind);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_post_index_kind
-- +goose StatementEnd
@@ -0,0 +1,14 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE key (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
"user" TEXT NOT NULL,
secret TEXT NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE key;
-- +goose StatementEnd
@@ -0,0 +1,13 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE "user" (
id TEXT NOT NULL PRIMARY KEY,
nick TEXT NOT NULL,
permissions TEXT[] NOT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE "user";
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE log ADD COLUMN "ts_vector" TSVECTOR;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE log DROP COLUMN "ts_vector";
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE INDEX log_index_search ON log USING GIN (ts_vector);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS log_index_search;
-- +goose StatementEnd
@@ -0,0 +1,13 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE log_post
DROP CONSTRAINT log_post_log_short_id_position_key,
ADD CONSTRAINT log_post_log_short_id_position_key UNIQUE(log_short_id, position) DEFERRABLE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE log_post
DROP CONSTRAINT log_post_log_short_id_position_key,
ADD CONSTRAINT log_post_log_short_id_position_key UNIQUE(log_short_id, position);
-- +goose StatementEnd
+314
View File
@@ -0,0 +1,314 @@
package postgres
import (
"context"
"database/sql"
"errors"
"fmt"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"strings"
)
type postRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *postRepository) Find(ctx context.Context, id string) (*models.Post, error) {
post, err := psqlcore.New(r.db).SelectPost(ctx, id)
if err != nil {
return nil, err
}
return r.post(post), nil
}
func (r *postRepository) List(ctx context.Context, filter models.PostFilter) ([]*models.Post, error) {
q := psqlcore.New(r.db)
params := psqlcore.SelectPostsParams{LimitSize: 0}
if filter.LogID != nil {
params.FilterLogShortID = true
if !strings.HasPrefix(*filter.LogID, "L") {
log, err := q.SelectLog(ctx, *filter.LogID)
if err != nil {
return nil, err
}
params.LogShortID = log.ShortID
} else {
params.LogShortID = *filter.LogID
}
}
if filter.Kinds != nil {
params.FilterKinds = true
params.Kinds = filter.Kinds
}
if filter.Search != nil {
params.FilterSearch = true
params.Search = TSQueryFromSearch(*filter.Search)
}
if filter.Limit > 0 {
params.LimitSize = int32(filter.Limit)
}
posts, err := q.SelectPosts(ctx, params)
if err != nil {
return nil, err
}
return r.posts(posts), nil
}
func (r *postRepository) Insert(ctx context.Context, post models.Post) (*models.Post, error) {
if !r.insertWithIDs || len(post.ID) < 8 {
post.ID = generate.PostID()
}
position, err := psqlcore.New(r.db).InsertPost(ctx, psqlcore.InsertPostParams{
ID: post.ID,
LogShortID: post.LogID,
Time: post.Time.UTC(),
Kind: post.Kind,
Nick: post.Nick,
Text: post.Text,
})
if err != nil {
return nil, err
}
_ = psqlcore.New(r.db).GenerateLogTSVector(ctx, post.LogID)
post.Position = int(position)
return &post, nil
}
func (r *postRepository) InsertMany(ctx context.Context, posts ...*models.Post) ([]*models.Post, error) {
if len(posts) == 0 {
return []*models.Post{}, nil
}
allowedLogID := ""
for _, post := range posts {
if allowedLogID == "" {
allowedLogID = post.LogID
} else if allowedLogID != post.LogID {
return nil, errors.New("cannot insert multiple posts with different log IDs")
}
}
params := psqlcore.InsertPostsParams{
LogShortID: posts[0].LogID,
}
for _, post := range posts {
if !r.insertWithIDs || len(post.ID) < 8 {
post.ID = generate.PostID()
}
params.Ids = append(params.Ids, post.ID)
params.Kinds = append(params.Kinds, post.Kind)
params.Nicks = append(params.Nicks, post.Nick)
params.Offsets = append(params.Offsets, int32(len(params.Offsets)+1))
params.Times = append(params.Times, post.Time.UTC())
params.Texts = append(params.Texts, post.Text)
}
offset, err := psqlcore.New(r.db).InsertPosts(ctx, params)
if err != nil {
return nil, err
}
for i, post := range posts {
post.Position = int(offset) + i
}
err = psqlcore.New(r.db).GenerateLogTSVector(ctx, posts[0].LogID)
if err != nil {
return nil, err
}
return posts, nil
}
func (r *postRepository) Update(ctx context.Context, post models.Post, update models.PostUpdate) (*models.Post, error) {
post.ApplyUpdate(update)
err := psqlcore.New(r.db).UpdatePost(ctx, psqlcore.UpdatePostParams{
Time: post.Time,
Kind: post.Kind,
Nick: post.Nick,
Text: post.Text,
ID: post.ID,
})
if err != nil {
return nil, err
}
_ = psqlcore.New(r.db).GenerateLogTSVector(ctx, post.LogID)
return &post, nil
}
func (r *postRepository) Move(ctx context.Context, post models.Post, position int) ([]*models.Post, error) {
if position == post.Position {
return r.List(ctx, models.PostFilter{LogID: &post.LogID})
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
_, err = tx.Exec("LOCK TABLE log_post IN SHARE UPDATE EXCLUSIVE MODE")
if err != nil {
return nil, err
}
var lowest, highest int32
q := psqlcore.New(tx)
err = q.MovePost(ctx, psqlcore.MovePostParams{ID: post.ID, Position: -1})
if err != nil {
return nil, err
}
if position > post.Position {
lowest = int32(post.Position)
highest = int32(position)
err := q.ShiftPostsBetween(ctx, psqlcore.ShiftPostsBetweenParams{
ShiftOffset: -1,
LogShortID: post.LogID,
FromPosition: lowest + 1,
ToPosition: highest,
})
if err != nil {
return nil, err
}
} else {
lowest = int32(position)
highest = int32(post.Position)
err := q.ShiftPostsBetween(ctx, psqlcore.ShiftPostsBetweenParams{
ShiftOffset: 1,
LogShortID: post.LogID,
FromPosition: lowest,
ToPosition: highest - 1,
})
if err != nil {
return nil, err
}
}
err = q.MovePost(ctx, psqlcore.MovePostParams{ID: post.ID, Position: int32(position)})
if err != nil {
return nil, err
}
posts, err := q.SelectPostsByPositionRange(ctx, psqlcore.SelectPostsByPositionRangeParams{
LogShortID: post.LogID,
FromPosition: lowest,
ToPosition: highest,
})
if err != nil {
return nil, err
}
positions, err := q.SelectPositionsByLogShortID(ctx, post.LogID)
if err != nil {
return nil, err
}
prev := int32(0)
for _, pos := range positions {
if pos != prev+1 {
return nil, errors.New("post discontinuity detected")
}
prev = pos
}
err = tx.Commit()
if err != nil {
return nil, err
}
return r.posts2(posts), nil
}
func (r *postRepository) Delete(ctx context.Context, post models.Post) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
_, err = tx.Exec("LOCK TABLE log_post IN SHARE UPDATE EXCLUSIVE MODE")
if err != nil {
return err
}
err = q.MovePost(ctx, psqlcore.MovePostParams{ID: post.ID, Position: -1})
if err != nil {
return fmt.Errorf("pre-delete move failed: %s", err)
}
err = q.ShiftPostsAfter(ctx, psqlcore.ShiftPostsAfterParams{
ShiftOffset: -1,
LogShortID: post.LogID,
FromPosition: int32(post.Position + 1),
})
if err != nil {
return fmt.Errorf("shift failed: %s", err)
}
err = q.DeletePost(ctx, post.ID)
if err != nil {
return fmt.Errorf("delete failed: %s", err)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("tx commit failed: %s", err)
}
_ = psqlcore.New(r.db).GenerateLogTSVector(ctx, post.LogID)
return nil
}
func (r *postRepository) post(post psqlcore.SelectPostRow) *models.Post {
return &models.Post{
ID: post.ID,
LogID: post.LogShortID,
Time: post.Time,
Kind: post.Kind,
Nick: post.Nick,
Text: post.Text,
Position: int(post.Position),
}
}
func (r *postRepository) posts(posts []psqlcore.SelectPostsRow) []*models.Post {
results := make([]*models.Post, 0, len(posts))
for _, post := range posts {
results = append(results, r.post(psqlcore.SelectPostRow(post)))
}
return results
}
func (r *postRepository) posts2(posts []psqlcore.SelectPostsByPositionRangeRow) []*models.Post {
results := make([]*models.Post, 0, len(posts))
for _, post := range posts {
results = append(results, r.post(psqlcore.SelectPostRow(post)))
}
return results
}
+4
View File
@@ -0,0 +1,4 @@
//go:generate go run github.com/kyleconroy/sqlc/cmd/sqlc generate -f ../../../sqlc.yaml
// Package psqlcore contains the generated code for the PostgresSQL drivers. It's generated using sqlc.
package psqlcore
+22
View File
@@ -0,0 +1,22 @@
-- name: SelectChangeByID :one
SELECT * FROM data_change WHERE id = $1 LIMIT 1;
-- name: SelectChanges :many
SELECT * FROM data_change
WHERE (@filter_keys::bool = false OR keys && (@keys::text[]))
AND (@filter_earliest_date::bool = false OR date >= @earliest_date::timestamp)
AND (@filter_latest_date::bool = false OR date <= @latest_date::timestamp)
AND (@filter_author::bool = false OR author = @author::text)
ORDER BY date DESC
LIMIT NULLIF(@limit_size::INT, 0);
-- name: InsertChange :exec
INSERT INTO data_change (id, model, op, author, listed, date, keys, objects)
VALUES (
@id::text, @model::text, @op::text, @author::text,
@listed::boolean, @date::timestamp, @keys::text[],
@objects::jsonb
);
-- name: DeleteChange :exec
DELETE FROM data_change WHERE id = $1;
+30
View File
@@ -0,0 +1,30 @@
-- name: SelectChannelByName :one
SELECT * FROM data_channel WHERE name = $1 LIMIT 1;
-- name: InsertChannel :exec
INSERT INTO data_channel (name, logged, hub, event_name, location_name)
VALUES (
@name::text,
@logged::boolean, @hub::boolean,
@event_name::text, @location_name::text
);
-- name: SelectChannels :many
SELECT * FROM data_channel
WHERE (@filter_name::bool = false OR name = ANY(@names::text[]))
AND (@filter_logged::bool = false OR logged = @logged)
AND (@filter_event_name::bool = false OR event_name = @event_name)
AND (@filter_location_name::bool = false OR location_name = @location_name)
ORDER BY name
LIMIT NULLIF(@limit_size::INT, 0);
-- name: UpdateChannel :exec
UPDATE data_channel
SET logged = @logged::boolean,
hub = @hub::boolean,
event_name = @event_name::text,
location_name = @location_name::text
WHERE name = @name::text;
-- name: DeleteChannel :exec
DELETE FROM data_channel WHERE name=$1;
+36
View File
@@ -0,0 +1,36 @@
-- name: SelectChapter :one
SELECT * FROM story_chapter WHERE id=$1::TEXT LIMIT 1;
-- name: SelectChapters :many
SELECT * FROM story_chapter
WHERE (@story_id::TEXT = '' OR story_id = @story_id::TEXT)
ORDER BY created_date
LIMIT NULLIF(@limit_size::INT, 0);
-- name: InsertChapter :exec
INSERT INTO story_chapter (id, story_id, title, author, source, created_date, fictional_date, edited_date, comment_mode, comments_locked)
VALUES (
@id::TEXT, @story_id::TEXT, @title::TEXT, @author::TEXT, @source::TEXT,
@created_date::TIMESTAMP, @fictional_date::TIMESTAMP, @edited_date::TIMESTAMP,
@comment_mode::TEXT, @comments_locked::BOOLEAN
);
-- name: UpdateChapterStoryID :exec
UPDATE story_chapter
SET story_id = @story_id::TEXT
WHERE id = @id;
-- name: UpdateChapter :exec
UPDATE story_chapter
SET title = @title,
source = @source,
fictional_date = @fictional_date,
comment_mode = @comment_mode,
comments_locked = @comments_locked
WHERE id = @id;
-- name: DeleteChapter :exec
DELETE FROM story_chapter WHERE id=$1;
-- name: DeleteChaptersByStoryID :exec
DELETE FROM story_chapter WHERE story_id=$1;
+47
View File
@@ -0,0 +1,47 @@
-- name: SelectCharacterByID :one
SELECT id, nicks, name, short_name, author, description FROM data_character WHERE id = @id::text;
-- name: SelectCharacterByNick :one
SELECT id, nicks, name, short_name, author, description FROM data_character WHERE nicks <@ ARRAY[@nick::text];
-- name: SelectCharacterByName :one
SELECT id, nicks, name, short_name, author, description FROM data_character WHERE name = @name::text;
-- name: SelectCharacters :many
SELECT id, nicks, name, short_name, author, description FROM data_character
WHERE (@filter_id::bool = false OR id = ANY(@ids::text[]))
AND (@filter_name::bool = false OR name = ANY(@names::text[]))
AND (@filter_nick::bool = false OR nicks && (@nicks::text[]))
AND (@filter_author::bool = false OR author = @author::text)
AND (@filter_search::bool = false OR "ts_vector" @@ to_tsquery(@search::text))
LIMIT NULLIF(@limit_size::INT, 0);
-- name: InsertCharacter :exec
INSERT INTO data_character (id, nicks, name, short_name, author, description, ts_vector) VALUES (
@id::text, @nicks::text[], @name::text,
@short_name::text, @author::text, @description::text,
to_tsvector(
'english', @name::text || ' ' || @description::text || ' ' || @author::text || ' '
|| immutable_array_to_string(@nicks::text[], ' ')
)
);
-- name: UpdateCharacter :exec
UPDATE data_character
SET name = @name::text,
short_name = @short_name::text,
description = @description::text
WHERE id = @id::text;
-- name: AddCharacterNick :exec
UPDATE data_character
SET nicks=array_append(nicks, @nick::text)
WHERE id = @id::text;
-- name: RemoveCharacterNick :exec
UPDATE data_character
SET nicks=array_remove(nicks, @nick::text)
WHERE id = @id::text;
-- name: DeleteCharacter :exec
DELETE FROM data_character WHERE id=$1;
+37
View File
@@ -0,0 +1,37 @@
-- name: SelectComment :one
SELECT * FROM story_comment WHERE id=$1;
-- name: InsertComment :exec
INSERT INTO story_comment (id, chapter_id, character_id, subject, author, character_name, source, created_date, fictional_date, edited_date)
VALUES (
@id, @chapter_id, @character_id,
@subject, @author, @character_name, @source,
@created_date, @fictional_date, @edited_date
);
-- name: UpdateComment :exec
UPDATE story_comment
SET subject = @subject,
source = @source,
fictional_date = @fictional_date,
edited_date = @edited_date,
character_name = @character_name,
character_id = @character_id
WHERE id = @id;
-- name: SelectComments :many
SELECT * FROM story_comment
WHERE (@chapter_id = '' OR chapter_id = @chapter_id)
LIMIT NULLIF(@limit_size::INT, 0);
-- name: DeleteComment :exec
DELETE FROM story_comment WHERE id = $1;
-- name: DeleteCommentsByChapterID :exec
DELETE FROM story_comment WHERE chapter_id = $1;
-- name: DeleteCommentsByStoryID :exec
DELETE FROM story_comment
WHERE chapter_id IN (
SELECT id FROM story_chapter WHERE story_id = $1
);
+8
View File
@@ -0,0 +1,8 @@
-- name: EnsureCounter :exec
INSERT INTO core_counter (id, value) VALUES (@id::text, 0) ON CONFLICT DO NOTHING;
-- name: IncrementCounter :one
UPDATE core_counter SET value = value + 1 WHERE id = @id::text RETURNING value::int;
-- name: BumpCounter :exec
UPDATE core_counter SET value = @value::int WHERE id = @id::text AND value <= @value::int;
+15
View File
@@ -0,0 +1,15 @@
-- name: SelectKey :one
SELECT * FROM "key" WHERE id=$1 LIMIT 1;
-- name: SelectKeys :many
SELECT * FROM "key";
-- name: SelectKeysByUser :many
SELECT * FROM "key" WHERE "user" = $1;
-- name: InsertKey :exec
INSERT INTO key (id, name, "user", secret)
VALUES (@id, @name, @user_id, @secret);
-- name: DeleteKey :exec
DELETE FROM "key" WHERE id = $1;
+44
View File
@@ -0,0 +1,44 @@
-- name: SelectLog :one
SELECT id, short_id, character_ids, date, channel_name, event_name, title, description, open FROM log WHERE id=$1 OR short_id=$1 LIMIT 1;
-- name: SelectLogs :many
SELECT id, short_id, character_ids, date, channel_name, event_name, title, description, open FROM log
WHERE (@filter_short_id::BOOL = false OR short_id = ANY(@short_ids::TEXT[]))
AND (@filter_character_id::BOOL = false OR character_ids @> (@character_ids::TEXT[]))
AND (@filter_channel_name::BOOL = false OR channel_name = ANY(@channel_names::TEXT[]))
AND (@filter_event_name::BOOL = false OR event_name = ANY(@event_names::TEXT[]))
AND (@filter_open::BOOL = false OR open = @open::BOOL)
AND (@filter_earlist_date::BOOL = false OR date >= @earliest_date::TIMESTAMP)
AND (@filter_lastest_date::BOOL = false OR date <= @latest_date::TIMESTAMP)
AND (@filter_search::BOOL = false OR "ts_vector" @@ to_tsquery(@search::TEXT))
ORDER BY date DESC
LIMIT NULLIF(@limit_size::INT, 0);
-- name: InsertLog :exec
INSERT INTO log (id, short_id, character_ids, date, channel_name, event_name, title, description, open)
VALUES (
@id, @short_id, @character_ids, @date, @channel_name, @event_name, @title, @description, @open
);
-- name: UpdateLog :exec
UPDATE log
SET title = @title,
event_name = @event_name,
description = @description,
open = @open,
character_ids = @character_ids
WHERE id = @id;
-- name: GenerateLogTSVector :exec
UPDATE log
SET "ts_vector" = (
SELECT TO_TSVECTOR(
immutable_array_to_string(array_agg(text), '\n\n')
)
FROM log_post
WHERE log_short_id = @short_id
)
WHERE short_id = @short_id;
-- name: DeleteLog :exec
DELETE FROM log WHERE id=$1;
+63
View File
@@ -0,0 +1,63 @@
-- name: SelectPost :one
SELECT id, log_short_id, time, kind, nick, text, position FROM log_post WHERE id=$1 LIMIT 1;
-- name: SelectLogIDsFromPostSearch :many
SELECT DISTINCT(log_short_id) FROM log_post WHERE "ts_vector" @@ to_tsquery(@search::TEXT);
-- name: SelectPostsByPositionRange :many
SELECT id, log_short_id, time, kind, nick, text, position FROM log_post WHERE log_short_id = @log_short_id AND position >= @from_position AND position <= @to_position;
-- name: SelectPositionsByLogShortID :many
SELECT position FROM log_post WHERE log_short_id = $1 ORDER BY position;
-- name: SelectPosts :many
SELECT id, log_short_id, time, kind, nick, text, position FROM log_post
WHERE (@filter_ids::BOOL = false OR id = ANY(@ids::TEXT[]))
AND (@filter_kinds::BOOL = false OR kind = ANY(@kinds::TEXT[]))
AND (@filter_log_short_id::BOOL = false OR log_short_id = @log_short_id)
AND (@filter_search::BOOL = false OR "ts_vector" @@ to_tsquery(@search::TEXT))
ORDER BY log_short_id, position
LIMIT NULLIF(@limit_size::INT, 0);
-- name: InsertPost :one
INSERT INTO log_post (id, log_short_id, time, kind, nick, text, position, ts_vector)
SELECT @id, @log_short_id, @time, @kind, @nick, @text, COALESCE(MAX(position), 0)+1, to_tsvector(@nick || ' ' || @text)
FROM log_post
WHERE log_short_id = @log_short_id
RETURNING position;
-- name: InsertPosts :one
INSERT INTO log_post (id, log_short_id, time, kind, nick, text, position, ts_vector)
SELECT UNNEST(@ids::TEXT[]), @log_short_id, UNNEST(@times::TIMESTAMP[]), UNNEST(@kinds::TEXT[]),
UNNEST(@nicks::TEXT[]), UNNEST(@texts::TEXT[]), COALESCE(MAX(position), 0) + UNNEST(@offsets::INT[]),
to_tsvector(UNNEST(@nicks::TEXT[]) || ' ' || UNNEST(@texts::TEXT[]))
FROM log_post
WHERE log_short_id = @log_short_id
RETURNING position;
-- name: UpdatePost :exec
UPDATE log_post
SET time = @time,
kind = @kind,
nick = @nick,
text = @text,
ts_vector = to_tsvector(@nick || ' ' || @text)
WHERE id = @id;
-- name: MovePost :exec
UPDATE log_post SET position = @position WHERE id = @id;
-- name: ShiftPostsBetween :exec
UPDATE log_post SET position = position + @shift_offset::INT WHERE log_short_id = @log_short_id AND position >= @from_position AND position <= @to_position;
-- name: ShiftPostsAfter :exec
UPDATE log_post SET position = position + @shift_offset::INT WHERE log_short_id = @log_short_id AND position >= @from_position;
-- name: ShiftPostsBefore :exec
UPDATE log_post SET position = position + @shift_offset::INT WHERE log_short_id = @log_short_id AND position <= @to_position;
-- name: DeletePost :exec
DELETE FROM log_post WHERE id = @id;
-- name: DeletePostsByLogShortID :exec
DELETE FROM log_post WHERE log_short_id = @log_short_id;
+37
View File
@@ -0,0 +1,37 @@
-- name: SelectStory :one
SELECT * FROM story WHERE id = $1 LIMIT 1;
-- name: SelectStories :many
SELECT * FROM story
WHERE (@filter_id::bool = false OR id = ANY(@ids::text[]))
AND (@filter_author::bool = false OR author = @author::text)
AND (@filter_earlist_fictional_date::bool = false OR fictional_date >= @earliest_fictional_date::timestamp)
AND (@filter_lastest_fictional_date::bool = false OR fictional_date <= @latest_fictional_date::timestamp)
AND (@filter_category::bool = false OR category = @category::text)
AND (@filter_open::bool = false OR open = @open::bool)
AND (@filter_listed::bool = false OR listed = @listed::bool)
ORDER BY updated_date DESC
LIMIT NULLIF(@limit_size::INT, 0);
-- name: InsertStory :exec
INSERT INTO story (id, author, name, category, open, listed, sort_by_fictional_date, created_date, fictional_date, updated_date)
VALUES (
@id::text, @author::text, @name::text, @category::text,
@open::boolean, @listed::boolean, @sort_by_fictional_date::boolean,
@created_date::timestamp, @fictional_date::timestamp, @updated_date::timestamp
);
-- name: UpdateStory :exec
UPDATE story
SET name = @name,
category = @category,
author = @author,
open = @open,
listed = @listed,
fictional_date = @fictional_date,
updated_date = @updated_date,
sort_by_fictional_date = @sort_by_fictional_date
WHERE id = @id;
-- name: DeleteStory :exec
DELETE FROM story WHERE id=$1;
+62
View File
@@ -0,0 +1,62 @@
-- name: SetTag :exec
INSERT INTO common_tag (tag, target_kind, target_id)
VALUES (
@tag::TEXT, @target_kind::TEXT, @target_id::TEXT
)
ON CONFLICT DO NOTHING;
-- name: SetTags :exec
INSERT INTO common_tag (tag, target_kind, target_id)
SELECT unnest(@tags::TEXT[]), @target_kind::TEXT as target_kind, @target_id::TEXT as target_id
ON CONFLICT DO NOTHING;
-- name: ClearTag :exec
DELETE FROM common_tag
WHERE tag = @tag::TEXT
AND target_kind = @target_kind::TEXT
AND target_id = @target_id::TEXT;
-- name: ClearTagsByTargetLike :exec
DELETE FROM common_tag
WHERE target_kind = @target_kind::TEXT
AND target_id = @target_id::TEXT
AND tag LIKE @tag_like::TEXT;
-- name: ClearTagsByTarget :exec
DELETE FROM common_tag
WHERE target_kind = @target_kind::TEXT
AND target_id = @target_id::TEXT;
-- name: SetCharacterTagsFromIDs :exec
INSERT INTO common_tag
SELECT 'Character:'||name AS tag, @target_kind::TEXT as target_kind, @target_id::TEXT as target_id
FROM data_character WHERE id = ANY(@character_ids::TEXT[])
ON CONFLICT DO NOTHING;
-- name: SetLocationTagFromChannelName :exec
INSERT INTO common_tag
SELECT 'Location:'||location_name AS tag, @target_kind::TEXT as target_kind, @target_id::TEXT as target_id
FROM data_channel
WHERE name = @channel_name AND location_name != ''
ON CONFLICT DO NOTHING;
-- name: SelectDistinctTags :many
SELECT DISTINCT tag FROM common_tag ORDER BY tag;
-- name: SelectDistinctTagsLike :many
SELECT DISTINCT tag FROM common_tag WHERE tag LIKE $1::text ORDER BY tag;
-- name: SelectTagsByTag :many
SELECT * FROM common_tag WHERE tag = @tag_name::text ORDER BY tag;
-- name: SelectTagsByTags :many
SELECT * FROM common_tag WHERE tag = ANY(@tag_names::text[]) ORDER BY tag;
-- name: SelectTargetsByTags :many
SELECT DISTINCT target_id FROM common_tag WHERE tag = ANY(@tag_names::text[]) AND target_kind = @target_kind::text;
-- name: SelectTagsByTarget :many
SELECT * FROM common_tag WHERE target_kind = @target_kind AND target_id = @target_id::text ORDER BY tag;
-- name: SelectTagsByTargets :many
SELECT * FROM common_tag WHERE target_kind = @target_kind AND target_id = ANY(@target_ids::text[]) ORDER BY tag;
+11
View File
@@ -0,0 +1,11 @@
-- name: SelectUser :one
SELECT * FROM "user" WHERE id=$1 LIMIT 1;
-- name: SelectUsers :many
SELECT * FROM "user";
-- name: InsertUser :exec
INSERT INTO "user" (id, nick, permissions)
VALUES (
@id, @nick, @permissions
);
+250
View File
@@ -0,0 +1,250 @@
package postgres
import (
"context"
"database/sql"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
)
type storyRepository struct {
insertWithIDs bool
db *sql.DB
}
func (r *storyRepository) Find(ctx context.Context, id string) (*models.Story, error) {
q := psqlcore.New(r.db)
story, err := q.SelectStory(ctx, id)
if err != nil {
return nil, err
}
tags, err := q.SelectTagsByTarget(ctx, psqlcore.SelectTagsByTargetParams{
TargetKind: "Story",
TargetID: story.ID,
})
if err != nil && err != sql.ErrNoRows {
return nil, err
}
return r.story(story, tags), nil
}
func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) {
q := psqlcore.New(r.db)
params := psqlcore.SelectStoriesParams{LimitSize: 0}
if len(filter.Tags) > 0 {
targets, err := q.SelectTargetsByTags(ctx, psqlcore.SelectTargetsByTagsParams{
TagNames: models.EncodeTagArray(filter.Tags),
TargetKind: "Story",
})
if err != nil && err != sql.ErrNoRows {
return nil, err
}
params.FilterID = true
params.Ids = targets
if len(params.Ids) == 0 {
return []*models.Story{}, nil
}
}
if filter.Author != nil {
params.FilterAuthor = true
params.Author = *filter.Author
}
if filter.Category != nil {
params.FilterCategory = true
params.Category = string(*filter.Category)
}
if !filter.EarliestFictionalDate.IsZero() {
params.FilterEarlistFictionalDate = true
params.EarliestFictionalDate = filter.EarliestFictionalDate.UTC()
}
if !filter.LatestFictionalDate.IsZero() {
params.FilterLastestFictionalDate = true
params.LatestFictionalDate = filter.LatestFictionalDate.UTC()
}
if filter.Open != nil {
params.FilterOpen = true
params.Open = *filter.Open
}
if filter.Unlisted != nil {
params.FilterListed = true
params.Listed = !*filter.Unlisted
}
if filter.Limit > 0 {
params.LimitSize = int32(filter.Limit)
}
stories, err := q.SelectStories(ctx, params)
if err != nil {
return nil, err
}
targetIDs := make([]string, len(stories))
for i, story := range stories {
targetIDs[i] = story.ID
}
tags, err := q.SelectTagsByTargets(ctx, psqlcore.SelectTagsByTargetsParams{
TargetKind: "Story",
TargetIds: targetIDs,
})
return r.stories(stories, tags), nil
}
func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
if !r.insertWithIDs || len(story.ID) < 8 {
story.ID = generate.StoryID()
}
if story.UpdatedDate.Before(story.CreatedDate) {
story.UpdatedDate = story.CreatedDate
}
err = q.InsertStory(ctx, psqlcore.InsertStoryParams{
ID: story.ID,
Author: story.Author,
Name: story.Name,
Category: string(story.Category),
Open: story.Open,
Listed: story.Listed,
SortByFictionalDate: story.SortByFictionalDate,
CreatedDate: story.CreatedDate.UTC(),
FictionalDate: story.FictionalDate.UTC(),
UpdatedDate: story.UpdatedDate.UTC(),
})
if err != nil {
return nil, err
}
// This is inefficient, but tags are very rarely added before the story is submitted.
err = q.SetTags(ctx, psqlcore.SetTagsParams{
Tags: models.EncodeTagArray(story.Tags),
TargetKind: "Story",
TargetID: story.ID,
})
if err != nil {
return nil, err
}
return &story, tx.Commit()
}
func (r *storyRepository) Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) {
story.ApplyUpdate(update)
err := psqlcore.New(r.db).UpdateStory(ctx, psqlcore.UpdateStoryParams{
Name: story.Name,
Category: string(story.Category),
Author: story.Author,
Open: story.Open,
Listed: story.Listed,
FictionalDate: story.FictionalDate.UTC(),
UpdatedDate: story.UpdatedDate.UTC(),
SortByFictionalDate: story.SortByFictionalDate,
ID: story.ID,
})
if err != nil {
return nil, err
}
return &story, nil
}
func (r *storyRepository) AddTag(ctx context.Context, story models.Story, tag models.Tag) error {
return psqlcore.New(r.db).SetTag(ctx, psqlcore.SetTagParams{
TargetKind: "Story",
TargetID: story.ID,
Tag: tag.String(),
})
}
func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error {
return psqlcore.New(r.db).ClearTag(ctx, psqlcore.ClearTagParams{
TargetKind: "Story",
TargetID: story.ID,
Tag: tag.String(),
})
}
func (r *storyRepository) Delete(ctx context.Context, story models.Story) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
q := psqlcore.New(tx)
err = q.ClearTagsByTarget(ctx, psqlcore.ClearTagsByTargetParams{
TargetKind: "Story",
TargetID: story.ID,
})
if err != nil {
return err
}
err = q.DeleteCommentsByStoryID(ctx, story.ID)
if err != nil {
return err
}
err = q.DeleteChaptersByStoryID(ctx, story.ID)
if err != nil {
return err
}
err = q.DeleteStory(ctx, story.ID)
if err != nil {
return err
}
return tx.Commit()
}
func (r *storyRepository) story(story psqlcore.Story, tags []psqlcore.CommonTag) *models.Story {
storyTags := make([]models.Tag, 0, 8)
for _, tag := range tags {
if tag.TargetKind == "Story" && tag.TargetID == story.ID {
newTag := models.Tag{}
err := newTag.Decode(tag.Tag)
if err != nil {
continue
}
storyTags = append(storyTags, newTag)
}
}
return &models.Story{
ID: story.ID,
Author: story.Author,
Name: story.Name,
Category: models.StoryCategory(story.Category),
Open: story.Open,
Listed: story.Listed,
Tags: storyTags,
CreatedDate: story.CreatedDate,
FictionalDate: story.FictionalDate,
UpdatedDate: story.UpdatedDate,
SortByFictionalDate: story.SortByFictionalDate,
}
}
func (r *storyRepository) stories(stories []psqlcore.Story, tags []psqlcore.CommonTag) []*models.Story {
results := make([]*models.Story, 0, len(stories))
for _, story := range stories {
results = append(results, r.story(story, tags))
}
return results
}
+43
View File
@@ -0,0 +1,43 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/models"
)
type tagRepository struct {
db *sql.DB
}
func (r *tagRepository) Find(ctx context.Context, kind models.TagKind, name string) (*models.Tag, error) {
tag, err := psqlcore.New(r.db).SelectTagsByTag(ctx, fmt.Sprintf("%s:%s", kind, name))
if err != nil {
return nil, err
}
tag2 := &models.Tag{}
err = tag2.Decode(tag[0].Tag)
if err != nil {
return nil, err
}
return tag2, nil
}
func (r *tagRepository) List(ctx context.Context, filter models.TagFilter) ([]*models.Tag, error) {
var tags []string
var err error
if filter.Kind != nil {
tags, err = psqlcore.New(r.db).SelectDistinctTagsLike(ctx, string(*filter.Kind)+"%")
} else {
tags, err = psqlcore.New(r.db).SelectDistinctTags(ctx)
}
if err != nil {
return nil, err
}
return models.DecodeTagArray(tags)
}
+56
View File
@@ -0,0 +1,56 @@
package postgres
import (
"context"
"database/sql"
"git.aiterp.net/rpdata/api/database/postgres/psqlcore"
"git.aiterp.net/rpdata/api/models"
)
type userRepository struct {
db *sql.DB
}
func (r *userRepository) Find(ctx context.Context, id string) (*models.User, error) {
user, err := psqlcore.New(r.db).SelectUser(ctx, id)
if err != nil {
return nil, err
}
return &models.User{
ID: user.ID,
Nick: user.Nick,
Permissions: user.Permissions,
}, nil
}
func (r *userRepository) List(ctx context.Context) ([]*models.User, error) {
users, err := psqlcore.New(r.db).SelectUsers(ctx)
if err != nil {
return nil, err
}
results := make([]*models.User, 0, len(users))
for _, user := range users {
results = append(results, &models.User{
ID: user.ID,
Nick: user.Nick,
Permissions: user.Permissions,
})
}
return results, nil
}
func (r *userRepository) Insert(ctx context.Context, user models.User) (*models.User, error) {
err := psqlcore.New(r.db).InsertUser(ctx, psqlcore.InsertUserParams{
ID: user.ID,
Nick: user.Nick,
Permissions: user.Permissions,
})
if err != nil {
return nil, err
}
return &user, nil
}
+61
View File
@@ -0,0 +1,61 @@
package postgres
import "strings"
// TSQueryFromSearch generates a TS query from a typical search string.
func TSQueryFromSearch(search string) string {
if strings.HasPrefix(search, "tsq:") {
return search[4:]
}
tokens := strings.Split(search, " ")
inQuotes := false
result := ""
for i, token := range tokens {
clearQuotes := false
startQuotes := false
if strings.HasPrefix(token, "\"") && len(token) > 1 {
startQuotes = true
}
if strings.HasSuffix(token, "\"") {
if !startQuotes {
clearQuotes = true
}
startQuotes = false
}
token := strings.Trim(token, ",.;:()\"")
if len(token) > 0 {
if startQuotes {
if i > 0 {
result += " & "
}
result += "("
} else if inQuotes {
result += "<->"
} else if i != 0 {
result += " & "
}
result += token
}
if startQuotes {
inQuotes = true
}
if clearQuotes {
inQuotes = false
result += ")"
}
}
if inQuotes {
return result
}
return strings.TrimLeft(result, " ")
}
+25
View File
@@ -0,0 +1,25 @@
package postgres
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestTSQueryFromSearch(t *testing.T) {
rows := [][2]string{
{`asari matron`, `asari & matron`},
{`"asari matron"`, `(asari<->matron)`},
{`"asari matron" blue`, `(asari<->matron) & blue`},
{`"christmas present" "wrapping paper"`, `(christmas<->present) & (wrapping<->paper)`},
{`stuff`, `stuff`},
{`"stuff"`, `stuff`},
{`"her slowly, rubbing the back of his head awkwardly. "`, `(her<->slowly<->rubbing<->the<->back<->of<->his<->head<->awkwardly)`},
}
for i, row := range rows {
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
assert.Equal(t, row[1], TSQueryFromSearch(row[0]))
})
}
}
+5 -15
View File
@@ -4,32 +4,22 @@ go 1.12
require (
github.com/99designs/gqlgen v0.9.0
github.com/beorn7/perks v1.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e
github.com/go-ini/ini v1.35.0 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/google/go-cmp v0.3.0 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/h2non/filetype v1.0.8
github.com/kyleconroy/sqlc v1.7.0
github.com/lib/pq v1.10.0
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 // indirect
github.com/prometheus/client_golang v0.9.1
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
github.com/prometheus/common v0.4.0 // indirect
github.com/prometheus/client_golang v0.9.3
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e // indirect
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
github.com/stretchr/testify v1.3.0
github.com/stretchr/testify v1.5.1
github.com/tidwall/pretty v1.0.0 // indirect
github.com/vektah/dataloaden v0.3.0
github.com/vektah/gqlparser v1.1.2
go.mongodb.org/mongo-driver v1.0.3
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
golang.org/x/net v0.0.0-20190514140710-3ec191127204 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/text v0.3.0 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.4.0
)
replace golang.org/x/net => golang.org/x/net v0.0.0-20180416171110-a35a21de978d
+384 -29
View File
@@ -1,102 +1,322 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/gqlgen v0.9.0 h1:g1arBPML74Vqv0L3Q+TqIhGXLspV+2MYtRLkBxuZrlE=
github.com/99designs/gqlgen v0.9.0/go.mod h1:HrrG7ic9EgLPsULxsZh/Ti+p0HNWgR3XRuvnD0pb5KY=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antlr/antlr4 v0.0.0-20200209180723-1177c0b58d07 h1:ylxsz+1ifp/XBbiaFMqhB5YKshU47EzuZWpUIiH8urY=
github.com/antlr/antlr4 v0.0.0-20200209180723-1177c0b58d07/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/parser v0.0.0-20160622100904-31edd927e5b1/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM=
github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/cznic/y v0.0.0-20170802143616-045f81c6662a/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs=
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e h1:hmrbHva/wKD6X4M7pgHVbg/KfVV1wwgr1NEkQaVD3rU=
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-ini/ini v1.35.0 h1:D/my3+xOfqZMkJpciRcyqU7XMBUgiZa9qXjZIa8uv2k=
github.com/go-ini/ini v1.35.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/h2non/filetype v1.0.8 h1:le8gpf+FQA0/DlDABbtisA1KiTS0Xi+YSC/E8yY3Y14=
github.com/h2non/filetype v1.0.8/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyleconroy/sqlc v1.7.0 h1:a6/SblJ/NB48NhymCXsHHJlQzEqUOCKjvVJXTeqLn24=
github.com/kyleconroy/sqlc v1.7.0/go.mod h1:BZmkzwSB91So4gfoDpwXoMqni9zfenoF/3Rv4t8NEj0=
github.com/lfittl/pg_query_go v1.0.2 h1:adHtAAb0LpoXI3cYqdJTbo5W682+V9vg3/8ONAhnhqE=
github.com/lfittl/pg_query_go v1.0.2/go.mod h1:Ch/Oiv5pRI5jf9M7Hvf0XwCvFwa4PWRcNoFcLtVU8pc=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05 h1:YEyFCqmHcmbcTLqZ7yEAg7VIUat6PuDzGbIctZy888k=
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk=
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg=
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=
github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pingcap/errors v0.11.5-0.20201021055732-210aacd3fd99 h1:PVuEvTi/LlviMG7X3av44NRwcdPf0tiqL/YdVOIKCpA=
github.com/pingcap/errors v0.11.5-0.20201021055732-210aacd3fd99/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI=
github.com/pingcap/log v0.0.0-20200511115504-543df19646ad h1:SveG82rmu/GFxYanffxsSF503SiQV+2JLnWEiGiF+Tc=
github.com/pingcap/log v0.0.0-20200511115504-543df19646ad/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
github.com/pingcap/parser v0.0.0-20201024025010-3b2fb4b41d73 h1:MN/e9n6GUBmmi6sKtriW0Dv1dfIohZRoKr/ZU1B4jrM=
github.com/pingcap/parser v0.0.0-20201024025010-3b2fb4b41d73/go.mod h1:74+OEdwM4B/jMpBRl92ch6CSmSYkQtv2TNxIjFdT/GE=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e h1:zK8d1aZ+gw/Ne4uMfZTFRxj08PUOp+gGwm4HWUeGI1k=
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
@@ -104,39 +324,174 @@ github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180416171110-a35a21de978d h1:O2P57H5Cc+d+DJos+iweraI9rmzMYwV+45vkZdDY0Oo=
golang.org/x/net v0.0.0-20180416171110-a35a21de978d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5 h1:mzjBh+S5frKOsOBobWIMAbXavqjmgO17k/2puhcFR94=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6 h1:iZgcI2DDp6zW5v9Z/5+f0NuqoxNdmzg4hivjk2WLXpY=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191219041853-979b82bfef62 h1:vDaiisQl0rGVXqk3wT2yc43gSnwlj4haEG5J78IGZP4=
golang.org/x/tools v0.0.0-20191219041853-979b82bfef62/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo=
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
+2 -1
View File
@@ -6,6 +6,7 @@ import (
)
func complexity() (cr graphcore.ComplexityRoot) {
tinySubListComplexity := 2
subListComplexity := 25
bigSublistComplexity := 75
hugeSublistComplexity := 100
@@ -175,7 +176,7 @@ func complexity() (cr graphcore.ComplexityRoot) {
return childComplexity + bigSublistComplexity
}
cr.Story.Tags = func(childComplexity int) int {
return childComplexity + subListComplexity
return childComplexity + tinySubListComplexity
}
cr.Chapter.Comments = func(childComplexity int, limit *int) int {
+6 -1
View File
@@ -55,7 +55,12 @@ func (r *mutationResolver) ImportLog(ctx context.Context, input graphcore.LogImp
tz = parsedTZ
}
return r.s.Logs.Import(ctx, input.Importer, date, tz, input.ChannelName, input.Data)
sessionThreshold := time.Hour * 12
if input.SessionThresholdMs != nil {
sessionThreshold = time.Duration(*input.SessionThresholdMs) * time.Millisecond
}
return r.s.Logs.Import(ctx, input.Importer, date, tz, input.ChannelName, sessionThreshold, input.Data)
}
func (r *mutationResolver) SplitLog(ctx context.Context, input graphcore.LogSplitInput) (*models.Log, error) {
+4 -2
View File
@@ -36,7 +36,9 @@ func (r *mutationResolver) AddStory(ctx context.Context, input graphcore.StoryAd
fictionalDate = *input.FictionalDate
}
return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate)
sortByFictionalDate := input.SortByFictionalDate != nil && *input.SortByFictionalDate
return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate, sortByFictionalDate)
}
func (r *mutationResolver) AddStoryTag(ctx context.Context, input graphcore.StoryTagAddInput) (*models.Story, error) {
@@ -63,7 +65,7 @@ func (r *mutationResolver) EditStory(ctx context.Context, input graphcore.StoryE
return nil, err
}
return r.s.Stories.EditStory(ctx, story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate)
return r.s.Stories.EditStory(ctx, story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate, input.SortByFictionalDate)
}
func (r *mutationResolver) RemoveStory(ctx context.Context, input graphcore.StoryRemoveInput) (*models.Story, error) {
+8
View File
@@ -132,6 +132,9 @@ input LogImportInput {
"The date of the log, if not provided in the log body."
date: Time
"Session threwhold"
sessionThresholdMs: Int
"The log body itself."
data: String!
}
@@ -171,4 +174,9 @@ enum LogImporter {
Forum log: This importer parses the format on the forum. The displayed log, not the post source.
"""
ForumLog
"""
IRCCloud: This importer parses irccloud exports.
"""
IrcCloud
}
+9
View File
@@ -33,6 +33,9 @@ type Story {
# The date of the last major update to the story. This being bumped means a new chapter has been added.
updatedDate: Time!
# Whether to sort by fictional date.
sortByFictionalDate: Boolean!
}
# Filter for stories query.
@@ -86,6 +89,9 @@ input StoryAddInput {
# Set the fictional date of the story.
fictionalDate: Time
# Whether to sort by fictional date.
sortByFictionalDate: Boolean
}
# Input for the editStory mutation
@@ -115,6 +121,9 @@ input StoryEditInput {
# Clear the fictional date of the story.
clearFictionalDate: Boolean
# Whether to sort by fictional date.
sortByFictionalDate: Boolean
}
# Input for the addStoryTag mutation
+9 -7
View File
@@ -35,13 +35,15 @@ type Space struct {
// Database is configuration for spaces.
type Database struct {
Driver string `json:"driver" yaml:"driver"`
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
Db string `json:"db" yaml:"db"`
Username string `json:"username" yaml:"username"`
Password string `json:"password" yaml:"password"`
Mechanism string `json:"mechanism" yaml:"mechanism"`
Driver string `json:"driver" yaml:"driver"`
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
Db string `json:"db" yaml:"db"`
Username string `json:"username" yaml:"username"`
Password string `json:"password" yaml:"password"`
Mechanism string `json:"mechanism" yaml:"mechanism"`
RestoreIDs bool `json:"restoreIDs" yaml:"restoreIDs"`
SSL bool `json:"ssl" yaml:"ssl"`
}
// Wiki is the wiki stuff.
+38 -10
View File
@@ -1,7 +1,9 @@
package models
import (
"fmt"
"reflect"
"strings"
"time"
)
@@ -15,19 +17,23 @@ type Change struct {
Keys []ChangeKey `bson:"keys"`
Date time.Time `bson:"date"`
Logs []*Log `bson:"logs"`
Characters []*Character `bson:"characters"`
Channels []*Channel `bson:"channels"`
Posts []*Post `bson:"posts"`
Stories []*Story `bson:"stories"`
Tags []*Tag `bson:"tags"`
Chapters []*Chapter `bson:"chapters"`
Comments []*Comment `bson:"comments"`
Files []*File `bson:"files"`
ChangeObjectSet
}
type ChangeObjectSet struct {
Logs []*Log `bson:"logs" json:"logs,omitempty"`
Characters []*Character `bson:"characters" json:"characters,omitempty"`
Channels []*Channel `bson:"channels" json:"channels,omitempty"`
Posts []*Post `bson:"posts" json:"posts,omitempty"`
Stories []*Story `bson:"stories" json:"stories,omitempty"`
Tags []*Tag `bson:"tags" json:"tags,omitempty"`
Chapters []*Chapter `bson:"chapters" json:"chapters,omitempty"`
Comments []*Comment `bson:"comments" json:"comments,omitempty"`
Files []*File `bson:"files" json:"files,omitempty"`
}
// AddObject adds the model into the appropriate array.
func (change *Change) AddObject(object interface{}) bool {
func (change *ChangeObjectSet) AddObject(object interface{}) bool {
if v := reflect.ValueOf(object); v.Kind() == reflect.Struct {
ptr := reflect.PtrTo(v.Type())
ptrValue := reflect.New(ptr.Elem())
@@ -174,10 +180,32 @@ type ChangeKey struct {
ID string `bson:"id"`
}
func (ck *ChangeKey) String() string {
return ck.Model.String() + ":" + ck.ID
}
func (ck *ChangeKey) Decode(str string) error {
split := strings.Split(str, ":")
if len(split) != 2 {
return fmt.Errorf("invalid change key: %s", str)
}
model := ChangeModel(split[0])
if !model.IsValid() {
return fmt.Errorf("invalid change key model: %s", model)
}
ck.Model = model
ck.ID = split[1]
return nil
}
// ChangeFilter is a filter for listing changes.
type ChangeFilter struct {
Keys []ChangeKey
EarliestDate *time.Time
LatestDate *time.Time
Author *string
Limit *int
}
+15
View File
@@ -9,6 +9,21 @@ type Channel struct {
LocationName string `bson:"locationName,omitempty"`
}
func (channel *Channel) ApplyUpdate(update ChannelUpdate) {
if update.Logged != nil {
channel.Logged = *update.Logged
}
if update.Hub != nil {
channel.Hub = *update.Hub
}
if update.EventName != nil {
channel.EventName = *update.EventName
}
if update.LocationName != nil {
channel.LocationName = *update.LocationName
}
}
// IsChangeObject is an interface implementation to identify it as a valid
// ChangeObject in GQL.
func (*Channel) IsChangeObject() {
+18
View File
@@ -16,6 +16,24 @@ type Chapter struct {
CommentsLocked bool `bson:"commentsLocked"`
}
func (chapter *Chapter) ApplyUpdate(update ChapterUpdate) {
if update.Title != nil {
chapter.Title = *update.Title
}
if update.Source != nil {
chapter.Source = *update.Source
}
if update.FictionalDate != nil {
chapter.FictionalDate = update.FictionalDate.UTC()
}
if update.CommentMode != nil {
chapter.CommentMode = *update.CommentMode
}
if update.CommentsLocked != nil {
chapter.CommentsLocked = *update.CommentsLocked
}
}
// CanComment returns true if the chapter can be commented to.
func (chapter *Chapter) CanComment() bool {
return !chapter.CommentsLocked && chapter.CommentMode.IsEnabled()
+18 -6
View File
@@ -2,12 +2,24 @@ package models
// Character is a common data model representing an RP character or NPC.
type Character struct {
ID string `json:"id" bson:"_id"`
Nicks []string `json:"nicks" bson:"nicks"`
Name string `json:"name" bson:"name"`
ShortName string `json:"shortName" bson:"shortName"`
Author string `json:"author" bson:"author"`
Description string `json:"description" bson:"description"`
ID string `json:"id" bson:"_id" db:"id"`
Nicks []string `json:"nicks" bson:"nicks" db:"nicks"`
Name string `json:"name" bson:"name" db:"name"`
ShortName string `json:"shortName" bson:"shortName" db:"short_name"`
Author string `json:"author" bson:"author" db:"author"`
Description string `json:"description" bson:"description" db:"description"`
}
func (character *Character) ApplyUpdate(update CharacterUpdate) {
if update.Name != nil {
character.Name = *update.Name
}
if update.ShortName != nil {
character.ShortName = *update.ShortName
}
if update.Description != nil {
character.Description = *update.Description
}
}
// Nick gets the character's nick.
+19
View File
@@ -16,6 +16,25 @@ type Comment struct {
Source string `bson:"source"`
}
func (comment *Comment) ApplyUpdate(update CommentUpdate) {
if update.Source != nil {
comment.Source = *update.Source
}
if update.CharacterName != nil {
comment.CharacterName = *update.CharacterName
}
if update.CharacterID != nil {
comment.CharacterID = *update.CharacterID
}
if update.Subject != nil {
comment.Subject = *update.Subject
}
if update.FictionalDate != nil {
comment.FictionalDate = *update.FictionalDate
}
comment.EditedDate = time.Now()
}
// IsChangeObject is an interface implementation to identify it as a valid
// ChangeObject in GQL.
func (*Comment) IsChangeObject() {
+3 -1
View File
@@ -14,12 +14,14 @@ const (
LogImporterMircLike LogImporter = "MircLike"
// LogImporterForumLog is a value of LogImporter
LogImporterForumLog LogImporter = "ForumLog"
// LogImporterForumLog is a value of LogImporter
LogImporterIrcCloud LogImporter = "IrcCloud"
)
// IsValid returns true if the underlying string is one of the correct values.
func (e LogImporter) IsValid() bool {
switch e {
case LogImporterForumLog, LogImporterMircLike:
case LogImporterForumLog, LogImporterMircLike, LogImporterIrcCloud:
return true
}
return false
+19 -1
View File
@@ -15,6 +15,24 @@ type Log struct {
CharacterIDs []string `bson:"characterIds"`
}
func (log *Log) ApplyUpdate(update LogUpdate) {
if update.Open != nil {
log.Open = *update.Open
}
if update.EventName != nil {
log.EventName = *update.EventName
}
if update.Title != nil {
log.Title = *update.Title
}
if update.Description != nil {
log.Description = *update.Description
}
if update.CharacterIDs != nil {
log.CharacterIDs = update.CharacterIDs
}
}
// A LogSuggestion is a suggestion for a log.
type LogSuggestion struct {
Log *Log
@@ -31,13 +49,13 @@ func (*Log) IsChangeObject() {
// A LogFilter is a filter that can be used to list logs.
type LogFilter struct {
Search *string
Open *bool
Characters []string
Channels []string
Events []string
MinDate *time.Time
MaxDate *time.Time
Search *string
Limit int
}
+15
View File
@@ -13,6 +13,21 @@ type Post struct {
Position int `bson:"position"`
}
func (post *Post) ApplyUpdate(update PostUpdate) {
if update.Time != nil {
post.Time = *update.Time
}
if update.Kind != nil {
post.Kind = *update.Kind
}
if update.Nick != nil {
post.Nick = *update.Nick
}
if update.Text != nil {
post.Text = *update.Text
}
}
// IsChangeObject is an interface implementation to identify it as a valid
// ChangeObject in GQL.
func (*Post) IsChangeObject() {
+47 -18
View File
@@ -5,21 +5,49 @@ import "time"
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
// The story model is a container for multiple chapters this time, in contrast to the previous version.
type Story struct {
ID string `bson:"_id"`
Author string `bson:"author"`
Name string `bson:"name"`
Category StoryCategory `bson:"category"`
Open bool `bson:"open"`
Listed bool `bson:"listed"`
Tags []Tag `bson:"tags"`
CreatedDate time.Time `bson:"createdDate"`
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
UpdatedDate time.Time `bson:"updatedDate"`
ID string `bson:"_id"`
Author string `bson:"author"`
Name string `bson:"name"`
Category StoryCategory `bson:"category"`
Open bool `bson:"open"`
Listed bool `bson:"listed"`
Tags []Tag `bson:"tags"`
CreatedDate time.Time `bson:"createdDate"`
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
UpdatedDate time.Time `bson:"updatedDate"`
SortByFictionalDate bool `bson:"sortByFictionalDate"`
}
func (story *Story) ApplyUpdate(update StoryUpdate) {
if update.Name != nil {
story.Name = *update.Name
}
if update.Category != nil {
story.Category = *update.Category
}
if update.Author != nil {
story.Author = *update.Author
}
if update.Open != nil {
story.Open = *update.Open
}
if update.Listed != nil {
story.Listed = *update.Listed
}
if update.SortByFictionalDate != nil {
story.SortByFictionalDate = *update.SortByFictionalDate
}
if update.FictionalDate != nil {
story.FictionalDate = *update.FictionalDate
}
if update.UpdatedDate != nil {
story.UpdatedDate = *update.UpdatedDate
}
}
// IsChangeObject is an interface implementation to identify it as a valid
// ChangeObject in GQL.
func (*Story) IsChangeObject() {
func (_ *Story) IsChangeObject() {
panic("this method is a dummy, and so is its caller")
}
@@ -35,11 +63,12 @@ type StoryFilter struct {
}
type StoryUpdate struct {
Name *string
Category *StoryCategory
Author *string
Open *bool
Listed *bool
FictionalDate *time.Time
UpdatedDate *time.Time
Name *string
Category *StoryCategory
Author *string
Open *bool
Listed *bool
FictionalDate *time.Time
UpdatedDate *time.Time
SortByFictionalDate *bool
}
+47 -1
View File
@@ -1,8 +1,10 @@
package models
import (
"errors"
"fmt"
"io"
"strings"
)
// A Tag associates a story with other content, like other stories, logs and more.
@@ -11,6 +13,28 @@ type Tag struct {
Name string `bson:"name"`
}
func (tag *Tag) Decode(v string) error {
split := strings.SplitN(v, ":", 2)
if len(split) == 1 {
return errors.New("bad tag name: " + v)
}
kind := TagKind("")
err := kind.UnmarshalGQL(split[0])
if err != nil {
return err
}
tag.Kind = kind
tag.Name = split[1]
return nil
}
func (tag *Tag) String() string {
return string(tag.Kind) + ":" + tag.Name
}
// Equal returns true if the tags match one another.
func (tag *Tag) Equal(other Tag) bool {
return tag.Kind == other.Kind && tag.Name == other.Name
@@ -22,6 +46,28 @@ func (*Tag) IsChangeObject() {
panic("this method is a dummy, and so is its caller")
}
func EncodeTagArray(arr []Tag) []string {
result := make([]string, len(arr))
for i, v := range arr {
result[i] = v.String()
}
return result
}
func DecodeTagArray(arr []string) ([]*Tag, error) {
result := make([]*Tag, 0, len(arr))
for i, v := range arr {
result = append(result, &Tag{})
err := result[i].Decode(v)
if err != nil {
return nil, err
}
}
return result, nil
}
// TagKind represents the kind of tags.
type TagKind string
@@ -60,7 +106,7 @@ func (e *TagKind) UnmarshalGQL(v interface{}) error {
// MarshalGQL turns it into a JSON string
func (e TagKind) MarshalGQL(w io.Writer) {
fmt.Fprint(w, "\""+string(e), "\"")
_, _ = fmt.Fprintf(w, "\"%s\"", string(e))
}
// TagFilter is a filter for tag listing.

Some files were not shown because too many files have changed in this diff Show More