Compare commits

...
104 Commits
Author SHA1 Message Date
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
gisle dc3f77f8da update drone ci.
continuous-integration/drone/push Build is passing
2020-08-09 16:09:11 +02:00
gisle 6321ea1893 update drone ci.
continuous-integration/drone/push Build is failing
2020-08-09 16:08:11 +02:00
gisle 9b346fb97b update drone ci.
continuous-integration/drone/push Build is passing
2020-08-09 16:07:44 +02:00
gisle ed21b7c8cd improve parser error. 2020-08-09 16:01:45 +02:00
gisle 47b7877ab4 add nextLogs and prevLogs suggestions to log. 2020-08-09 15:35:49 +02:00
gisle 757b0bc616 add a hackish migration for the changes expiration index. 2020-08-09 15:35:33 +02:00
gisle 5dbed6d632 UI for file stuff.
the build was successful
2019-09-15 16:34:16 +02:00
gisle c8ced52e1a File uploads, mostly. Also added extra behavior to addCharacter.
the build was successful
2019-08-31 22:46:55 +02:00
gisle 8a313b3bb2 More destruction. Utils too, since they cause weird go mod shenanigans.
the build was successful
2019-08-26 22:22:05 +02:00
gisle e690e7ab97 A lot of delete and failed scream tests.
the build was successful
2019-08-26 22:14:59 +02:00
gisle 0e70e37038 Moved unknown nicks to log service
the build was successful
2019-08-26 20:23:36 +02:00
gisle 3b10c4d9b1 go mod tidy
the build was successful
2019-08-25 16:14:29 +02:00
gisle 690eb4f121 Deleted a bunch of old db code.
the build was successful
2019-08-25 16:13:04 +02:00
gisle 2eed815993 Finished refactor of story/chapter/comment related stuff.
the build was successful
2019-08-25 16:10:10 +02:00
gisle 8c4670580c Story/chapter/comment repos + started on story service.
the build was successful
2019-08-18 08:59:38 +02:00
gisle 8fbaea2078 Added mergeLog mutation.
the build was successful
2019-08-16 12:32:50 +02:00
gisle 2a0e6534ff Added splitLog mutation.
the build was successful
2019-08-15 11:28:09 +02:00
gisle 4bd75b8240 Fixed character filter not being exclusive. 2019-08-15 11:28:02 +02:00
gisle ed38d576b2 Added fix for date bug.
the build was successful
2019-08-14 22:01:48 +02:00
gisle 51b2c9bfe0 Fixed log service bugs with importing and character list not updating.
the build was successful
2019-08-14 11:28:02 +02:00
gisle c0687d76ad Added log character refresh function to log service.
the build was successful
2019-08-07 12:26:59 +02:00
gisle 60dcd8a80f no idea
the build was successful
2019-08-06 13:51:51 +02:00
gisle 5bcc7479ff Early work on file stuff.
the build was successful
2019-07-25 23:21:25 +02:00
gisle 2c00358f4f Fixed advance by one date bug by treating any out of order posts as a date bump.
the build was successful
2019-07-20 15:20:39 +02:00
gisle acb6b637a5 Moved channels to new system, fixed change insert not working.
the build was successful
2019-06-24 20:30:14 +02:00
gisle f46b6f0774 Added channel repository and mongodb impl.
the build was successful
2019-06-20 22:32:35 +02:00
gisle 73a1c79903 Logs and posts are now on the new architecture.
the build was successful
2019-06-18 20:34:58 +02:00
gisle e194b6536e Removed PassAll from ChangeFilter and fixed sort not being descending order.
the build was successful
2019-06-16 11:46:23 +02:00
gisle f6ad0602c2 Replaced repositories.Bundle with a database interface.
the build was successful
2019-06-16 11:39:33 +02:00
gisle 021f67ea4b Changes to change system.
the build was successful
2019-06-16 10:32:34 +02:00
gisle 13f82399ef Removed ./idea/workspace.xml from git.
the build was successful
2019-06-10 19:29:38 +02:00
gisle 2a1423d28f Added character service and such.
the build was successful
2019-06-10 17:03:52 +02:00
gisle 7b253748fe Replaced custom Date scalar code with gqlgen's built-in Time scalar.
the build was successful
2019-06-09 12:49:18 +02:00
gisle 53c3d4597b Renamed queries package to resolvers.
the build was successful
2019-06-09 12:40:40 +02:00
gisle cdf4e6f667 Started work on new layered setup, making a huge mess.
the build was successful
2019-06-09 12:37:17 +02:00
gisle 6fa9f64f2d Instrumented the living s*** out of the graphql.
the build was successful
2019-05-21 19:35:29 +02:00
gisle 2fb391321e Fixed bug in resolver array conversion that caused results to all be copied of the last element. 2019-05-21 19:35:11 +02:00
gisle 015bdfd2e8 Updated resolvers to be compatible with gqlgen 0.9
the build was successful
2019-05-19 17:06:46 +02:00
gisle 9925907e93 Removed combine.sh, using new glob schema features instead.
the build was successful
2019-05-17 17:31:42 +02:00
gisle c7c418aeab Fixed Change.object resolver not returning anything. 2019-05-17 17:12:56 +02:00
gisle d5573e7f39 Added Prometheus metrics, don't forget to restrict access on /metrics endpoint. 2019-05-17 17:12:27 +02:00
gisle e794311eb8 Migrated rpdata-api to using go modules and a more recent version of
the build was successful
gqlgen

Squashed commit of the following:

commit 162ccf6bfb
Author: Gisle Aune <dev@gisle.me>
Date:   Fri May 17 11:51:23 2019 +0200

    The great module madness continus, now with pointers.

commit 7e3dc49635
Author: Gisle Aune <dev@gisle.me>
Date:   Thu May 16 15:57:08 2019 +0200

    Basic stuff, doesn't compile
2019-05-17 12:20:08 +02:00
gisle aff0d99842 graph2: Added exception for open logs query in complexity.
the build was successful
2019-04-27 11:37:01 +02:00
gisle 00150118d2 graph2: Added complexity to prevent queries that can take down the server.
the build was successful
2019-04-25 20:47:20 +02:00
gisle 332a1eb32b Fixed rpdata-as2storyimport not compiling. 2019-03-17 21:32:21 +01:00
gisle db4d46c368 Added story to comment changes.
the build was successful
2019-03-17 20:44:30 +01:00
gisle a8c8610ca2 models: Fixed wrong database mapping for comment source.
the build was successful
2019-01-17 21:19:13 +01:00
gisle d09c17d30d graph2: Changed addComment error message on can-comment check to be consistent with others.
the build was successful
2019-01-16 22:07:00 +01:00
gisle e3e3ea07db graph2: Changed Chapter to not return any comments if mode is disabled, even if there are comments.
the build was successful
2019-01-16 22:05:40 +01:00
gisle 22592439da graph2, models: Added removeComment mutation.
the build was successful
2019-01-16 22:03:28 +01:00
gisle dfa9fb4025 graph2: Changed Comment to return null instead of a zero-date if it's cleared.
the build was successful
2019-01-16 21:50:53 +01:00
gisle 0e2a68a2d4 graph2, models: Added editComment mutation. 2019-01-16 21:45:00 +01:00
gisle 1eefa65527 models: Fixed wrong database mapping for comment id. 2019-01-16 21:41:36 +01:00
gisle f9b6af58c9 graph2: Changed Comment character resolver to use loader.
the build was successful
2019-01-15 21:42:44 +01:00
gisle 56ecf48b57 graph2, models: Added addComment mutation.
the build was successful
2019-01-15 21:34:57 +01:00
gisle 06fbfc90c5 graph2, model: Added Comment type to Change. 2019-01-14 19:58:05 +01:00
gisle 39a4fef506 models, graph2: Added comment removal on chapter removal.
the build was successful
2019-01-14 18:01:43 +01:00
gisle f9fe0bb5f8 models, graph2: Added comment options to chapter mutations.
the build was successful
2019-01-14 17:55:38 +01:00
gisle d22ab387dd models, graph2: Added Comment type and queries, and hooked it up to chapter.
the build was successful
2019-01-14 17:03:16 +01:00
gisle 53a86c0b9c store, config: Added option to disable space.
the build was successful
2019-01-12 12:13:24 +01:00
gisle e0b93c6b4b graph2: Changed story tagging permissions to allow anyone to tag open stories.
the build was successful
2019-01-12 11:38:19 +01:00
gisle 1ea04f1577 importers: Fixed panic in mirclike.ParsePost if the post contains no text.
the build was successful
2019-01-05 10:33:50 +01:00
gisle 17fd3c60f8 rpdata-server: Fixed unhelpful error message being returned on panic.
the build was successful
2019-01-05 10:09:31 +01:00
gisle c5b62a42db graph2: Fixed data race in change logging for importLog.
the build was successful
2018-12-11 20:05:03 +01:00
gisle fa0926c311 rpdata-lb2logimport: Updated it to fix build errors related to change in logs.UpdateChracters.
the build was successful
2018-12-08 16:08:28 +01:00
gisle d21efc1c7f rpdata-logpaste: Removed the command since it's replaced by GraphQL API endpoint. 2018-12-08 16:07:44 +01:00
gisle 852030b141 docker: Trimmed down DockerFile. 2018-12-08 16:07:11 +01:00
gisle f7f0b76379 changes: Reversed ordering in result.
the build was successful
2018-12-05 21:00:52 +01:00
gisle 2711654816 posts: Fixed AddMany not returning the IDs in the result.
the build was successful
2018-12-05 20:58:37 +01:00
gisle b60897a597 unknownnicks, logs: Changed unknownnicks counter to use posts instead of logs as a metric to make characters stick out more.
the build was successful
2018-12-04 20:49:31 +01:00
gisle 80e6c048c9 graph2: Added unknownNick query
the build was successful
2018-12-04 20:33:21 +01:00
gisle 6310cbbc1e unknownnicks: Added unknown nicks model and hooked it up. 2018-12-04 20:17:48 +01:00
gisle 89a1c2366c changes: Fixed crash on disconnect when there are more than one client connected. 2018-12-03 20:11:15 +01:00
gisle ac2b79e88a posts, counter: Fixed post positions issues when a post is deleted.
the build was successful
2018-12-02 21:27:50 +01:00
gisle 31239330df graph2, changes, rpdata-server: Added changes subscription. 2018-12-02 21:17:49 +01:00
gisle a58535fcb9 graph2: Added importLog query and associated models and repository functions.
the build was successful
2018-12-01 16:52:06 +01:00
gisle 2a91b6f9bc logs: Added forumlog parsers.
the build was successful
2018-12-01 12:48:05 +01:00
gisle caa7727c02 logs: Added mirclike parsers.
the build was successful
2018-11-28 19:21:53 +01:00
gisle 047c9784ae rpdata-logpaste: Fixed characters not being updated.
the build was successful
2018-11-07 19:56:56 +01:00
gisle d038c1cfea Dockerfile: Added tzdata package to final container.
the build was successful
2018-11-07 19:48:03 +01:00
271 changed files with 13462 additions and 5383 deletions
+48 -16
View File
@@ -1,21 +1,53 @@
workspace:
base: /go
path: src/git.aiterp.net/rpdata/api
name: rpdata-api
pipeline:
docker-release:
kind: pipeline
type: docker
steps:
- name: go-test
image: golang:1.14
commands:
- 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
repo: vmaple/rpdata
auto_tag: true
secrets: [ docker_username, docker_password ]
settings:
auto_tag: true
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/rpdata-api
registry: r.vmaple.dev
when:
event: tag
ref: refs/tags/*
docker-release-latest:
event:
- tag
- name: docker-release-latest
image: plugins/docker
repo: vmaple/rpdata
tags: latest
secrets: [ docker_username, docker_password ]
settings:
auto_tag: false
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/rpdata-api
registry: r.vmaple.dev
when:
event: tag
ref: refs/tags/*
event:
- tag
+10 -1
View File
@@ -8,6 +8,15 @@ rpdata-graphiql
rpdata-*
!rpdata-*/
.vscode
.idea
/main
/main.exe
/graph2/graphcore/*_gen.go
/database/postgres/psqlcore/*.go
!/database/postgres/psqlcore/package.go
generated.gql
generated.go
generated.go
/dump.zip
/rpdata-dump-v1/
+10 -19
View File
@@ -1,35 +1,26 @@
## 1. Build Server
# Use Go 1.10
FROM golang:1.11 AS build-server
# Use Go 1.14
FROM golang:1.16 AS build-server
# Load repository into docker
WORKDIR /go/src/git.aiterp.net/rpdata/api/
WORKDIR /project/
COPY . .
RUN rm -rf ./vendor
# Load build tools and dependencies
RUN go get -u github.com/golang/dep/cmd/dep
RUN go get -u github.com/vektah/gorunpkg
RUN go get -u github.com/jteeuwen/go-bindata/...
RUN dep ensure
# Install build tools and dependencies
RUN go generate ./...
# Build server and tools (CGO disabled to make them 100% static)
ENV CGO_ENABLED 0
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cmd/rpdata-server
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2charimport ./cmd/rpdata-lb2charimport
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2logimport ./cmd/rpdata-lb2logimport
# TODO: RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifileimport ./cmd/rpdata-wikifileimport
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-ensurechannels ./cmd/rpdata-ensurechannels
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-as2storyimport ./cmd/rpdata-as2storyimport
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-logpaste ./cmd/rpdata-logpaste
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.7
FROM alpine:3.13
# Install certificates to enable HTTPS GET requests
RUN apk add --no-cache ca-certificates
# Install certificates to enable HTTPS GET requests, and tzdata for log imports.
RUN apk add --no-cache ca-certificates tzdata
# Copy build files into final container
COPY --from=build-server /binaries/* /usr/local/bin/
Generated
-254
View File
@@ -1,254 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/99designs/gqlgen"
packages = [
".",
"cmd",
"codegen",
"codegen/templates",
"complexity",
"graphql",
"graphql/introspection",
"handler",
"internal/gopath"
]
revision = "636435b68700211441303f1a5ed92f3768ba5774"
version = "v0.5.1"
[[projects]]
name = "github.com/agnivade/levenshtein"
packages = ["."]
revision = "3d21ba515fe27b856f230847e856431ae1724adc"
version = "v1.0.0"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
[[projects]]
branch = "master"
name = "github.com/globalsign/mgo"
packages = [
".",
"bson",
"internal/json",
"internal/sasl",
"internal/scram"
]
revision = "f76e4f9da92ecd56e3be26f5ba92580af1ef97b4"
[[projects]]
name = "github.com/go-ini/ini"
packages = ["."]
revision = "ace140f73450505f33e8b8418216792275ae82a7"
version = "v1.35.0"
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
revision = "d523deb1b23d913de5bdada721a6071e71283618"
version = "v1.4.0"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
version = "v1.4.0"
[[projects]]
name = "github.com/graph-gophers/dataloader"
packages = ["."]
revision = "78139374585c29dcb97b8f33089ed11959e4be59"
version = "v5"
[[projects]]
branch = "master"
name = "github.com/graph-gophers/graphql-go"
packages = [
".",
"errors",
"internal/common",
"internal/exec",
"internal/exec/packer",
"internal/exec/resolvable",
"internal/exec/selected",
"internal/query",
"internal/schema",
"internal/validation",
"introspection",
"log",
"relay",
"trace"
]
revision = "9ebf33af539ab8cb832c7107bc0a978ca8dbc0de"
[[projects]]
name = "github.com/hashicorp/golang-lru"
packages = [
".",
"simplelru"
]
revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768"
version = "v0.5.0"
[[projects]]
branch = "master"
name = "github.com/jmoiron/sqlx"
packages = [
".",
"reflectx"
]
revision = "0dae4fefe7c0e190f7b5a78dac28a1c82cc8d849"
[[projects]]
name = "github.com/minio/minio-go"
packages = [
".",
"pkg/credentials",
"pkg/encrypt",
"pkg/s3signer",
"pkg/s3utils",
"pkg/set"
]
revision = "3d2d02921f0510e9d1f66ef77a265b8dddd36992"
version = "6.0.0"
[[projects]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
[[projects]]
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"ext",
"log"
]
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
name = "github.com/urfave/cli"
packages = ["."]
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
version = "v1.20.0"
[[projects]]
branch = "master"
name = "github.com/vektah/gqlparser"
packages = [
".",
"ast",
"gqlerror",
"lexer",
"parser",
"validator",
"validator/rules"
]
revision = "14e83ae06ec152e6d0afb9766a00e0c0918aa8fc"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"argon2",
"blake2b",
"ssh/terminal"
]
revision = "d6449816ce06963d9d136eee5a56fca5b0616e7e"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"idna",
"lex/httplex"
]
revision = "a35a21de978d84ffc92f010a153705b170b2f9d1"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "2f57af4873d00d535c5c9028850aa2152e6a5566"
[[projects]]
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
name = "golang.org/x/tools"
packages = [
"go/ast/astutil",
"go/buildutil",
"go/internal/cgo",
"go/loader",
"imports",
"internal/fastwalk"
]
revision = "677d2ff680c188ddb7dcd2bfa6bc7d3f2f2f75b2"
[[projects]]
name = "google.golang.org/appengine"
packages = ["cloudsql"]
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
version = "v1.1.0"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "3c9430afa8b9260026925c8f496af934e2e573a2bd4587bb634fd12a20ce6c2c"
solver-name = "gps-cdcl"
solver-version = 1
-43
View File
@@ -1,43 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
required = [ "github.com/99designs/gqlgen" ]
[[constraint]]
branch = "master"
name = "github.com/globalsign/mgo"
[[constraint]]
name = "github.com/minio/minio-go"
version = "6.0.0"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/graph-gophers/graphql-go"
-139
View File
@@ -1,139 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"strings"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/chapters"
"git.aiterp.net/rpdata/api/models/stories"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var flagHost = flag.String("host", "127.0.0.1:3306", "SQL host")
var flagDB = flag.String("database", "aitestory", "SQL database")
var flagUser = flag.String("user", "aitestory", "SQL user")
var flagPassword = flag.String("password", "", "SQL password")
func main() {
flag.Parse()
db, err := sqlx.Connect("mysql", fmt.Sprintf("%s:%s@(%s)/%s", *flagUser, *flagPassword, *flagHost, *flagDB))
if err != nil {
log.Fatalln(err)
}
err = db.Ping()
if err != nil {
log.Fatalln(err)
}
store.Init()
results := make([]storyResult, 0, 64)
rows, err := db.Queryx("SELECT * FROM page WHERE unlisted=0;")
for rows.Next() {
result := storyResult{}
err := rows.StructScan(&result)
if err != nil {
log.Fatalln(err)
}
results = append(results, result)
}
tagResults := make([]tagResult, 0, 256)
rows, err = db.Queryx("SELECT page_id,type,name FROM page_tag LEFT JOIN tag ON tag_id=tag.id;")
for rows.Next() {
result := tagResult{}
err := rows.StructScan(&result)
if err != nil {
log.Fatalln(err)
}
tagResults = append(tagResults, result)
}
for _, result := range results {
fictionalDate, err := time.Parse("2006-01-02 15:04:05", result.FictionalDate)
if err != nil {
if result.FictionalDate != "0000-00-00 00:00:00" {
log.Fatalln(err)
}
}
if fictionalDate.Year() < 1800 {
fictionalDate = time.Time{}
}
publishDate, err := time.Parse("2006-01-02 15:04:05", result.PublishDate)
if err != nil {
log.Fatalln(err)
}
tags := make([]models.Tag, 0, 8)
for _, tagResult := range tagResults {
if tagResult.PageID == result.ID {
tags = append(tags, models.Tag{Kind: models.TagKind(tagResult.Type), Name: tagResult.Name})
}
}
category := models.StoryCategory(result.Category)
if !category.IsValid() {
log.Println(result.Name, "does not have a valid category:", result.Category)
continue
}
story, err := stories.Add(result.Name, result.Author[5:], category, true, false, tags, publishDate, fictionalDate)
if err != nil {
log.Fatalln(err)
}
// Change the story title
title := result.Name
if strings.HasPrefix(result.Source, "#") {
firstNewline := strings.Index(result.Source, "\n")
result.Name = strings.Replace(result.Source[1:firstNewline], "\r", "", -1)
result.Source = result.Source[firstNewline+1:]
result.Source = strings.Replace(result.Source, "\r\n", "\n", -1)
if strings.HasPrefix(result.Source, "\r") || strings.HasPrefix(result.Source, "\n") {
result.Source = result.Source[1:]
}
}
chapter, err := chapters.Add(story, title, result.Author[5:], result.Source, publishDate, &fictionalDate)
if err != nil {
log.Fatalln(err)
}
fmt.Println(result.ID, "->", story.ID, chapter.ID)
}
}
type tagResult struct {
PageID string `db:"page_id"`
Type string `db:"type"`
Name string `db:"name"`
}
type storyResult struct {
ID string `db:"id"`
Name string `db:"name"`
Author string `db:"author"`
Category string `db:"category"`
FictionalDate string `db:"fictional_date"`
PublishDate string `db:"publish_date"`
EditDate string `db:"edit_date"`
Unlisted bool `db:"unlisted"`
Dated bool `db:"dated"`
Spesific bool `db:"specific"`
Indexed bool `db:"indexed"`
Published bool `db:"published"`
Type string `db:"type"`
Source string `db:"source"`
Cache string `db:"cache"`
BackgroundURL *string `db:"background_url"`
}
+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)
}
-41
View File
@@ -1,41 +0,0 @@
package main
import (
"fmt"
"os"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models/channels"
"git.aiterp.net/rpdata/api/models/logs"
)
func main() {
err := store.Init()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
logs, err := logs.List(nil)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
added := make(map[string]bool, 1024)
for _, log := range logs {
if added[log.ChannelName] {
continue
}
_, err := channels.Ensure(log.ChannelName, false)
if err != nil {
fmt.Fprintln(os.Stderr, log.ID, err)
continue
}
added[log.ChannelName] = true
fmt.Println(log.ChannelName, "ensured")
}
}
-3
View File
@@ -1,3 +0,0 @@
characters.json
characters.cson
debug
-55
View File
@@ -1,55 +0,0 @@
package main
import (
"encoding/json"
"io"
"strings"
)
type charInfo struct {
Nicks []string `json:"nicks"`
Name string `json:"name"`
Author string `json:"player"`
ShortName string `json:"first"`
}
func load(reader io.Reader) ([]charInfo, error) {
data := make(map[string]interface{})
err := json.NewDecoder(reader).Decode(&data)
if err != nil {
return nil, err
}
links := make(map[string]string, len(data))
infos := make([]charInfo, 0, 64)
for key, value := range data {
if info, ok := value.(map[string]interface{}); ok {
name := info["name"].(string)
author := info["player"].(string)
shortName, ok := info["first"].(string)
if !ok {
shortName = strings.SplitN(name, " ", 2)[0]
}
infos = append(infos, charInfo{
Nicks: []string{key},
Name: name,
Author: author,
ShortName: shortName,
})
} else if nick, ok := value.(string); ok {
links[key] = nick
}
}
for key, value := range links {
for i := range infos {
if infos[i].Nicks[0] == value {
infos[i].Nicks = append(infos[i].Nicks, key)
}
}
}
return infos, nil
}
-62
View File
@@ -1,62 +0,0 @@
package main
import (
"flag"
"log"
"os"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models/characters"
)
var fileName = flag.String("file", "./characters.json", "json file to load")
func main() {
file, err := os.Open(*fileName)
if err != nil {
log.Fatalln("Open:", err)
}
infos, err := load(file)
if err != nil {
log.Fatalln("Parse:", err)
}
err = store.Init()
if err != nil {
log.Fatalln("Store:", err)
}
charsAdded, charsFailed := 0, 0
nicksAdded, nicksFailed := 0, 0
for _, info := range infos {
char, err := characters.Add(info.Nicks[0], info.Name, info.ShortName, info.Author, "")
if err != nil {
log.Println(info.Nicks[0], "failed to insert:", err)
charsFailed++
char, err = characters.FindNick(info.Nicks[0])
if err != nil {
continue
}
} else {
log.Println(info.Nicks[0], "added")
charsAdded++
}
for _, alt := range info.Nicks[1:] {
char, err = characters.AddNick(char, alt)
if err != nil {
log.Println(info.Nicks[0], "failed to add nick", alt, "error:", err)
nicksFailed++
} else {
log.Println(info.Nicks[0], "addded nick", alt)
nicksAdded++
}
}
}
log.Printf("Characters – %d/%d", charsAdded, charsFailed+charsAdded)
log.Printf("Alt. Nicks – %d/%d", nicksAdded, nicksFailed+nicksAdded)
}
-2
View File
@@ -1,2 +0,0 @@
logs
debug
-85
View File
@@ -1,85 +0,0 @@
package main
import (
"bufio"
"errors"
"io"
"strconv"
"strings"
"time"
)
type logLine struct {
Verb string
Args []string
Text string
}
func parseFile(reader io.Reader) []logLine {
bufReader := bufio.NewReader(reader)
results := make([]logLine, 0, 512)
for {
line, err := bufReader.ReadString('\n')
if err == io.EOF {
if len(line) < 2 {
break
}
} else if err != nil {
break
}
if len(line) <= 2 {
continue
}
line = strings.Replace(line, "\n", "", 1)
line = strings.Replace(line, "\r", "", 1)
results = append(results, parseLine(line))
}
return results
}
func parseLine(line string) logLine {
textSplit := strings.SplitN(line, " :", 2)
tokens := strings.Split(textSplit[0], " ")
ll := logLine{
Verb: tokens[0],
}
if len(tokens) > 1 {
ll.Args = tokens[1:]
}
if len(textSplit) > 1 {
ll.Text = textSplit[1]
}
return ll
}
func parseFilename(fname string) (date time.Time, channel string, err error) {
date, err = time.ParseInLocation("2006-01-02_150405", fname[:17], time.Local)
if err != nil {
return
}
ms, err := strconv.Atoi(fname[17:20])
if err != nil {
return
}
date = date.Add(time.Duration(ms) * time.Millisecond)
if len(fname) < 23 {
err = errors.New("filename too short")
return
}
channel = fname[21:]
return
}
-110
View File
@@ -1,110 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path"
"strings"
"time"
"git.aiterp.net/rpdata/api/models/posts"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models/logs"
)
var prefixReplacer = strings.NewReplacer("+", "", "@", "", "!", "", "%", "")
func main() {
err := store.Init()
if err != nil {
log.Fatalln(err)
}
for _, filepath := range os.Args[1:] {
name := strings.Replace(path.Base(filepath), ".txt", "", 1)
file, err := os.Open(filepath)
if err != nil {
log.Println(filepath, err)
continue
}
logLines := parseFile(file)
file.Close()
// Get title and event
title := ""
event := ""
for _, line := range logLines {
if line.Verb == "TITLE" {
title = line.Text
}
if line.Verb == "TAG" {
event = line.Text
}
}
date, channel, err := parseFilename(name)
if err != nil {
log.Fatalln(err)
}
l, err := logs.Add(date, channel, title, event, "", false)
if err != nil {
log.Println(err)
continue
}
_, err = posts.Add(l, time.Now(), "annotation.info", "rpdata-lb2logimport", "This logfile is imported from aitelogs2 and may contain errors or wrong timestamps.")
if err != nil {
log.Println(err)
}
for _, line := range logLines {
if line.Verb != "CHARS" {
continue
}
_, err = posts.Add(l, l.Date, "chars", prefixReplacer.Replace(line.Args[0]), line.Text)
if err != nil {
log.Println(err)
}
}
for _, line := range logLines {
if line.Verb != "SCENE" && line.Verb != "ACTION" && line.Verb != "TEXT" {
continue
}
postTime, err := time.ParseInLocation("2006-01-02 15:04:05", date.Format("2006-01-02")+" "+line.Args[1], time.Local)
diff := postTime.Sub(date)
if err != nil {
log.Println(err)
continue
}
if diff < 0 {
if diff > -time.Second {
postTime = postTime.Add(diff)
} else {
postTime = postTime.Add(time.Hour * 24)
}
}
if line.Args[0][0] == '=' {
line.Verb = "SCENE"
}
_, err = posts.Add(l, postTime, strings.ToLower(line.Verb), prefixReplacer.Replace(line.Args[0]), line.Text)
if err != nil {
log.Println(err)
}
}
l, err = logs.UpdateCharacters(l)
if err != nil {
log.Println(err)
}
fmt.Println(l.ID, "completed")
}
}
-78
View File
@@ -1,78 +0,0 @@
package main
import (
"flag"
"log"
"os"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models/posts"
"git.aiterp.net/rpdata/api/models/logs"
)
var dateStr string
var tzStr string
var channel string
var fileName string
func main() {
var inputPosts []Post
flag.StringVar(&dateStr, "date", "2013-04-04", "Date in YYYY-MM-DD form.")
flag.StringVar(&tzStr, "tz", "Europe/Oslo", "Time zone for the date.")
flag.StringVar(&channel, "channel", "#RedrockAgency", "Channel it's set in.")
flag.StringVar(&fileName, "file", "-", "File to read from (- = stdin).")
flag.Parse()
tz, err := time.LoadLocation(tzStr)
if err != nil {
log.Fatalln("Parse timezone:", err)
}
date, err := time.ParseInLocation("2006-01-02", dateStr, tz)
if err != nil {
log.Fatalln("Parse date:", err)
}
err = store.Init()
if err != nil {
log.Fatalln("Init store:", err)
}
if fileName != "-" {
file, fileErr := os.Open(fileName)
if fileErr != nil {
log.Fatalln("Open file:", fileErr)
}
inputPosts, err = parsePosts(file, date)
file.Close()
} else {
inputPosts, err = parsePosts(os.Stdin, date)
}
if err != nil {
log.Fatalln("Read inputPosts:", err)
}
if len(inputPosts) == 0 {
log.Fatalln("Input file contained no posts.")
}
l, err := logs.Add(inputPosts[0].Time, channel, "", "", "", false)
if err != nil {
log.Fatalln("Add log:", err)
}
log.Println("Added log", l.ID)
for _, inputPost := range inputPosts {
p, err := posts.Add(l, inputPost.Time, inputPost.Kind, inputPost.Nick, inputPost.Text)
if err != nil {
log.Fatalln("Add post:", err)
}
log.Println("Added post", p.ID)
}
}
-126
View File
@@ -1,126 +0,0 @@
package main
import (
"bufio"
"errors"
"io"
"strconv"
"strings"
"time"
)
// ErrNotPost is returned by parsePost if the line is empty or not a post.
var ErrNotPost = errors.New("not a post")
// ErrTooShort is returned by parsePost if the line too short to be a post.
var ErrTooShort = errors.New("post too short")
// ErrInvalidTimestamp is returned by parsePost if the line is empty or not a post.
var ErrInvalidTimestamp = errors.New("invalid timestamp")
// A Post is a part of a log.
type Post struct {
Time time.Time
Kind string
Nick string
Text string
}
func parsePosts(reader io.Reader, date time.Time) ([]Post, error) {
prev := Post{}
posts := make([]Post, 0, 8)
bufReader := bufio.NewReader(reader)
for {
line, err := bufReader.ReadString('\n')
if err != nil && err != io.EOF {
return nil, err
}
if len(line) > 8 {
post, err := parsePost(strings.Trim(line, "  \n\r"), date, prev)
if err != nil {
return nil, err
}
posts = append(posts, post)
prev = post
}
if err == io.EOF {
break
}
}
return posts, nil
}
func parsePost(line string, date time.Time, prev Post) (Post, error) {
// Do basic validation
line = strings.Trim(line, "  \t\n\r")
if len(line) == 0 || !strings.HasPrefix(line, "[") {
return Post{}, ErrNotPost
}
// Parse timestamp
tsEndIndex := strings.IndexByte(line, ']')
if tsEndIndex == -1 || len(line) < tsEndIndex+5 {
return Post{}, ErrNotPost
}
tsStr := line[1:tsEndIndex]
tsSplit := strings.Split(tsStr, ":")
tsUnits := make([]int, len(tsSplit))
if len(tsSplit) < 2 {
return Post{}, ErrNotPost
}
for i := range tsSplit {
n, err := strconv.Atoi(tsSplit[i])
if err != nil {
return Post{}, ErrNotPost
}
tsUnits[i] = n
}
if len(tsUnits) == 2 {
tsUnits = append(tsUnits, 0)
}
ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location())
if !prev.Time.IsZero() && prev.Time.After(ts) {
ts = ts.AddDate(0, 0, 1)
}
if line[tsEndIndex+2] == '*' {
split := strings.SplitN(line[tsEndIndex+4:], " ", 2)
post := Post{
Time: ts,
Kind: "action",
Nick: strings.TrimLeft(split[0], "+@!~"),
Text: split[1],
}
if post.Nick[0] == '=' {
post.Kind = "scene"
}
return post, nil
} else if line[tsEndIndex+2] == '<' {
split := strings.SplitN(line[tsEndIndex+2:], " ", 2)
post := Post{
Time: ts,
Kind: "text",
Nick: strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"),
Text: split[1],
}
if post.Nick[0] == '=' {
post.Kind = "scene"
}
return post, nil
} else {
return Post{}, ErrNotPost
}
}
+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!")
}
}
}
+78 -34
View File
@@ -3,65 +3,109 @@ package main
import (
"context"
"fmt"
"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"
"strings"
"git.aiterp.net/rpdata/api/models/logs"
"git.aiterp.net/rpdata/api/graph2"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/internal/loader"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/99designs/gqlgen/handler"
)
func main() {
err := store.Init()
cfg := config.Global()
db, err := database.Init(cfg.Database)
if err != nil {
log.Fatalln("Failed to init store:", err)
log.Fatalln("Failed to init db:", err)
}
defer db.Close(context.Background())
http.Handle("/", handler.Playground("RPData API", "/graphql"))
http.Handle("/graphql", queryHandler())
go func() {
err := logs.RunFullUpdate()
var spaceClient *space.Client
if cfg.Space.Enabled {
spaceClient, err = space.Connect(cfg.Space)
if err != nil {
log.Println(err)
log.Fatalln("Failed to init space:", err)
}
log.Println("Characters updated")
log.Println("Space loaded")
} else {
log.Println("Space is disabled, file upload will not work!")
}
serviceBundle := services.NewBundle(db, spaceClient)
instrumentation.Register()
http.Handle("/", handler.Playground("RPData API", "/graphql"))
http.Handle("/graphql", queryHandler(serviceBundle))
http.Handle("/metrics", promhttp.Handler())
go func() {
err := serviceBundle.Logs.RefreshAllLogCharacters(context.Background())
if err != nil {
log.Println("Refresh Characters:", err)
}
}()
go logListedChanges(serviceBundle.Changes)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func queryHandler() http.HandlerFunc {
func logListedChanges(changes *services.ChangeService) {
sub := changes.Subscribe(context.Background(), models.ChangeFilter{})
for change := range sub {
log.Printf("Change: Author=%#+v Model=%#+v Op=%#+v", change.Author, change.Model, change.Op)
for _, object := range change.Objects() {
switch object := object.(type) {
case models.Log:
log.Printf("\tLog: %s (%s)", object.ID, object.ShortID)
case models.Character:
log.Printf("\tCharacter: %s", object.ID)
case models.Channel:
log.Printf("\tChannel: %s", object.Name)
case models.Post:
log.Printf("\tPost: %s", object.ID)
case models.Story:
log.Printf("\tStory: %s", object.ID)
case models.Tag:
log.Printf("\tTag: (%s,%s)", object.Kind, object.Name)
case models.Chapter:
log.Printf("\tChapter: %s", object.ID)
case models.Comment:
log.Printf("\tComment: %s", object.ID)
}
}
}
log.Println("Change subscription closed.")
}
func queryHandler(services *services.Bundle) http.HandlerFunc {
handler := handler.GraphQL(
graph2.New(),
graph2.New(services),
handler.UploadMaxSize(10485760),
handler.UploadMaxMemory(1048576),
handler.RecoverFunc(func(ctx context.Context, err interface{}) error {
// send this panic somewhere
log.Println(err)
log.Println(string(debug.Stack()))
return fmt.Errorf("shit")
return fmt.Errorf("The server failed to serve your request due to an internal error, it has been logged")
}),
handler.ComplexityLimit(300),
handler.RequestMiddleware(instrumentation.RequestMiddleware()),
handler.ResolverMiddleware(instrumentation.ResolverMiddleware()),
)
return func(w http.ResponseWriter, r *http.Request) {
l := loader.New()
r = r.WithContext(l.ToContext(r.Context()))
// >_>
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 = auth.RequestWithToken(r)
r = services.Auth.RequestWithToken(r)
handler.ServeHTTP(w, r)
}
-77
View File
@@ -1,77 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models/files"
)
func main() {
client := http.Client{Timeout: time.Second * 30}
res, err := client.Get(config.Global().Wiki.URL + "?action=query&list=allimages&ailimit=5&aiprop=dimensions|mime|url|user|timestamp&format=json&ailimit=500")
if err != nil {
log.Fatalln(err)
}
if res.StatusCode != 200 {
log.Fatalln(res.Status)
}
data := ResponseData{}
err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
log.Fatalln(err)
}
store.Init()
for _, info := range data.Query.Allimages {
existing, err := files.FindName("wiki", info.Name, info.User)
if err == nil && existing.ID != "" {
log.Println("Skipping", info.Name, "because it already exists")
continue
}
ts, err := time.Parse(time.RFC3339, info.Timestamp)
if err != nil {
log.Println("Skipping", info.Name, "error:", err)
continue
}
file, err := files.Insert(info.Name, "wiki", info.Mime, info.User, ts, info.Size, info.URL)
if err != nil {
log.Println("Skipping", info.Name, "error:", err)
continue
}
fmt.Println(file.Name, "inserted ( id:", file.ID, ")")
}
}
// The ResponseData from the wiki looks like this
type ResponseData struct {
Batchcomplete string `json:"batchcomplete"`
Query struct {
Allimages []struct {
Descriptionshorturl string `json:"descriptionshorturl"`
Descriptionurl string `json:"descriptionurl"`
Height int `json:"height"`
Mime string `json:"mime"`
Name string `json:"name"`
Ns int `json:"ns"`
Size int64 `json:"size"`
Timestamp string `json:"timestamp"`
Title string `json:"title"`
URL string `json:"url"`
User string `json:"user"`
Width int `json:"width"`
} `json:"allimages"`
} `json:"query"`
}
+43
View File
@@ -0,0 +1,43 @@
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"
"git.aiterp.net/rpdata/api/repositories"
)
// ErrDriverUnrecognized is returned if the driver is not recognized
var ErrDriverUnrecognized = errors.New("driver not recognized, check installed version or your configuration")
type Database interface {
Changes() repositories.ChangeRepository
Channels() repositories.ChannelRepository
Characters() repositories.CharacterRepository
Tags() repositories.TagRepository
Logs() repositories.LogRepository
Posts() repositories.PostRepository
Stories() repositories.StoryRepository
Chapters() repositories.ChapterRepository
Comments() repositories.CommentRepository
Keys() repositories.KeyRepository
Users() repositories.UserRepository
Files() repositories.FileRepository
Close(ctx context.Context) error
}
// Init sets up the database.
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
}
}
+135
View File
@@ -0,0 +1,135 @@
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 {
restoreIDs bool
changes *mgo.Collection
idCounter *counter
}
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) {
change := new(models.Change)
err := r.changes.FindId(id).One(change)
if err != nil {
return nil, err
}
return change, nil
}
func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) {
query := bson.M{}
limit := 0
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 if !r.restoreIDs {
query["listed"] = true
}
if filter.Author != nil && *filter.Author != "" {
query["author"] = *filter.Author
}
if filter.Limit != nil {
limit = *filter.Limit
}
initialSize := 64
if limit > 0 && limit < 256 {
initialSize = limit
}
changes := make([]*models.Change, 0, initialSize)
err := r.changes.Find(query).Sort("-date").Limit(limit).All(&changes)
if err != nil {
return nil, err
}
return changes, nil
}
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
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)
}
err := r.changes.Insert(&change)
if err != nil {
return nil, err
}
return &change, nil
}
func (r *changeRepository) Remove(ctx context.Context, change models.Change) error {
return r.changes.RemoveId(change.ID)
}
func newChangeRepository(db *mgo.Database, restoreIDs bool) (repositories.ChangeRepository, error) {
collection := db.C("common.changes")
// Delete the old index if it exists.
_ = collection.DropIndexName("date_1")
err := collection.EnsureIndex(mgo.Index{
Name: "expiry",
Key: []string{"date"},
ExpireAfter: time.Hour * 2400, // 100 days
})
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("keys")
if err != nil {
return nil, err
}
return &changeRepository{
restoreIDs: restoreIDs,
changes: collection,
idCounter: newCounter(db, "auto_increment", "Change"),
}, nil
}
+107
View File
@@ -0,0 +1,107 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
func newChannelRepository(db *mgo.Database) (*channelRepository, error) {
collection := db.C("common.channels")
err := collection.EnsureIndexKey("logged")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("event")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("location")
if err != nil {
return nil, err
}
return &channelRepository{
channels: collection,
}, nil
}
type channelRepository struct {
channels *mgo.Collection
}
func (r *channelRepository) Find(ctx context.Context, name string) (*models.Channel, error) {
channel := new(models.Channel)
err := r.channels.FindId(name).One(channel)
if err != nil {
return nil, err
}
return channel, nil
}
func (r *channelRepository) List(ctx context.Context, filter models.ChannelFilter) ([]*models.Channel, error) {
query := bson.M{}
if filter.Logged != nil {
query["logged"] = *filter.Logged
}
if filter.EventName != nil {
query["eventName"] = *filter.EventName
}
if filter.LocationName != nil {
query["locationName"] = *filter.LocationName
}
if len(filter.Names) > 0 {
query["_id"] = bson.M{"$in": filter.Names}
}
channels := make([]*models.Channel, 0, 32)
err := r.channels.Find(query).Limit(filter.Limit).Sort("_id").All(&channels)
if err != nil {
if err == mgo.ErrNotFound {
return channels, nil
}
return nil, err
}
return channels, nil
}
func (r *channelRepository) Insert(ctx context.Context, channel models.Channel) (*models.Channel, error) {
err := r.channels.Insert(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) {
updateBson := bson.M{}
if update.Logged != nil {
updateBson["logged"] = *update.Logged
channel.Logged = *update.Logged
}
if update.LocationName != nil {
updateBson["locationName"] = *update.LocationName
channel.LocationName = *update.LocationName
}
if update.EventName != nil {
updateBson["eventName"] = *update.EventName
channel.EventName = *update.EventName
}
err := r.channels.UpdateId(channel.Name, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &channel, nil
}
func (r *channelRepository) Remove(ctx context.Context, channel models.Channel) error {
return r.channels.RemoveId(channel.Name)
}
+147
View File
@@ -0,0 +1,147 @@
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 {
restoreIDs bool
chapters *mgo.Collection
comments *mgo.Collection
}
func newChapterRepository(db *mgo.Database, restoreIDs bool) (repositories.ChapterRepository, error) {
collection := db.C("story.chapters")
err := collection.EnsureIndexKey("storyId")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("createdDate")
if err != nil {
return nil, err
}
return &chapterRepository{
restoreIDs: restoreIDs,
chapters: collection,
comments: db.C("story.comments"),
}, nil
}
func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) {
chapter := new(models.Chapter)
err := r.chapters.FindId(id).One(chapter)
if err != nil {
return nil, err
}
return chapter, nil
}
func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) {
query := bson.M{}
if filter.StoryID != nil {
query["storyId"] = *filter.StoryID
}
chapters := make([]*models.Chapter, 0, 32)
err := r.chapters.Find(query).Sort("createdDate").Limit(filter.Limit).All(&chapters)
if err != nil {
if err == mgo.ErrNotFound {
return chapters, nil
}
return nil, err
}
return chapters, nil
}
func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) {
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 {
return nil, err
}
return &chapter, nil
}
func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) {
updateBson := bson.M{}
if update.Title != nil {
updateBson["title"] = *update.Title
chapter.Title = *update.Title
}
if update.Source != nil {
updateBson["source"] = *update.Source
chapter.Source = *update.Source
}
if update.FictionalDate != nil {
updateBson["fictionalDate"] = *update.FictionalDate
chapter.FictionalDate = *update.FictionalDate
}
if update.CommentMode != nil {
updateBson["commentMode"] = *update.CommentMode
chapter.CommentMode = *update.CommentMode
}
if update.CommentsLocked != nil {
updateBson["commentsLocked"] = *update.CommentsLocked
chapter.CommentsLocked = *update.CommentsLocked
}
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &chapter, nil
}
func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) {
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": bson.M{"storyId": to.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 {
err := r.chapters.RemoveId(chapter.ID)
if err != nil {
return err
}
c, err := r.comments.RemoveAll(bson.M{"chapterId": chapter.ID})
if err != nil {
log.Println("Failed to remove comments:", err)
return nil
}
log.Printf("Removed chapter %s (%d comments)", chapter.ID, c.Removed)
return nil
}
+262
View File
@@ -0,0 +1,262 @@
package mongodb
import (
"context"
"errors"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"sort"
"strconv"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
)
type characterRepository struct {
characters *mgo.Collection
cidCounter *counter
restoreIDs bool
}
func newCharacterRepository(db *mgo.Database, restoreIDs bool) (repositories.CharacterRepository, error) {
collection := db.C("common.characters")
err := collection.EnsureIndexKey("name")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("shortName")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndex(mgo.Index{
Key: []string{"nicks"},
Unique: true,
DropDups: true,
})
if err != nil {
return nil, err
}
err = collection.EnsureIndex(mgo.Index{
Key: []string{"$text:description"},
})
if err != nil {
return nil, err
}
return &characterRepository{
restoreIDs: restoreIDs,
characters: collection,
cidCounter: newCounter(db, "auto_increment", "Character"),
}, nil
}
func (r *characterRepository) Find(ctx context.Context, id string) (*models.Character, error) {
character := new(models.Character)
err := r.characters.FindId(id).One(character)
if err != nil {
return nil, err
}
return character, nil
}
func (r *characterRepository) FindNick(ctx context.Context, nick string) (*models.Character, error) {
character := new(models.Character)
err := r.characters.Find(bson.M{"nick": nick}).One(character)
if err != nil {
return nil, err
}
return character, nil
}
func (r *characterRepository) FindName(ctx context.Context, name string) (*models.Character, error) {
query := bson.M{
"$or": []bson.M{
{"shortName": name},
{"name": name},
},
}
// Look for all characters matching query
characters := make([]*models.Character, 0, 8)
err := r.characters.Find(query).All(&characters)
if err != nil {
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
}
return nil, err
} else if len(characters) == 0 {
return nil, repositories.ErrNotFound
}
// Prioritize exact match
for _, character := range characters {
if character.Name == name {
return character, nil
}
}
return characters[0], nil
}
func (r *characterRepository) List(ctx context.Context, filter models.CharacterFilter) ([]*models.Character, error) {
query := bson.M{}
if filter.Author != nil {
query["author"] = *filter.Author
}
if len(filter.IDs) > 0 {
query["_id"] = bson.M{"$in": filter.IDs}
}
if len(filter.Nicks) > 0 {
query["nicks"] = bson.M{"$in": filter.Nicks}
}
if len(filter.Names) > 0 {
query["$or"] = []bson.M{
{"name": bson.M{"$in": filter.Names}},
{"shortName": bson.M{"$in": filter.Names}},
}
}
if filter.Search != nil {
query["$text"] = bson.M{"$search": *filter.Search}
}
characters := make([]*models.Character, 0, 32)
err := r.characters.Find(query).Limit(filter.Limit).All(&characters)
if err != nil {
if err == mgo.ErrNotFound {
return characters, nil
}
return nil, err
}
sort.Slice(characters, func(i, j int) bool {
ni, _ := strconv.Atoi(characters[i].ID[1:])
nj, _ := strconv.Atoi(characters[j].ID[1:])
return ni < nj
})
return characters, nil
}
func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) {
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.cidCounter.Bump(n)
if err != nil {
return nil, err
}
}
err := r.characters.Insert(&character)
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) {
updateBson := bson.M{}
if update.Name != nil {
updateBson["name"] = *update.Name
character.Name = *update.Name
}
if update.ShortName != nil {
updateBson["shortName"] = *update.ShortName
character.ShortName = *update.ShortName
}
if update.Description != nil {
updateBson["description"] = *update.Description
character.Description = *update.Description
}
err := r.characters.UpdateId(character.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &character, nil
}
func (r *characterRepository) AddNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
if character.HasNick(nick) {
return nil, errors.New("nick already exist")
}
match := bson.M{
"_id": character.ID,
"nicks": bson.M{"$ne": nick},
}
err := r.characters.Update(match, bson.M{"$push": bson.M{"nicks": nick}})
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
} else if err != nil {
return nil, err
}
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
copy(newNicks, character.Nicks)
character.Nicks = append(newNicks, nick)
return &character, nil
}
func (r *characterRepository) RemoveNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
if !character.HasNick(nick) {
return nil, errors.New("nick does not exist")
}
match := bson.M{
"_id": character.ID,
"nicks": nick,
}
err := r.characters.Update(match, bson.M{"$pull": bson.M{"nicks": nick}})
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
} else if mErr, ok := err.(*mgo.LastError); ok && mErr.Code == 11000 {
return nil, errors.New("The nick belongs to another character already")
} else if err != nil {
return nil, err
}
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
copy(newNicks, character.Nicks)
for i := range newNicks {
if newNicks[i] == nick {
newNicks = append(newNicks[:i], newNicks[i+1:]...)
break
}
}
character.Nicks = newNicks
return &character, nil
}
func (r *characterRepository) Delete(ctx context.Context, character models.Character) error {
return r.characters.RemoveId(character.ID)
}
+144
View File
@@ -0,0 +1,144 @@
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 {
restoreIDs bool
comments *mgo.Collection
}
func newCommentRepository(db *mgo.Database, restoreIDs bool) (repositories.CommentRepository, error) {
collection := db.C("story.comments")
err := collection.EnsureIndexKey("chapterId")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("createdDate")
if err != nil {
return nil, err
}
r := &commentRepository{
restoreIDs: restoreIDs,
comments: collection,
}
go r.fixFieldTypo()
return r, nil
}
func (r *commentRepository) Find(ctx context.Context, id string) (*models.Comment, error) {
comment := new(models.Comment)
err := r.comments.FindId(id).One(comment)
if err != nil {
return nil, err
}
return comment, nil
}
func (r *commentRepository) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) {
query := bson.M{}
if filter.ChapterID != nil {
query["chapterId"] = *filter.ChapterID
}
comments := make([]*models.Comment, 0, 32)
err := r.comments.Find(query).Sort("createdDate").Limit(filter.Limit).All(&comments)
if err != nil {
if err == mgo.ErrNotFound {
return comments, nil
}
return nil, err
}
return comments, nil
}
func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) {
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 {
return nil, err
}
return &comment, nil
}
func (r *commentRepository) Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) {
updateBson := bson.M{}
if update.Subject != nil {
updateBson["subject"] = *update.Subject
comment.Subject = *update.Subject
}
if update.Source != nil {
updateBson["source"] = *update.Source
comment.Source = *update.Source
}
if update.FictionalDate != nil {
updateBson["fictionalDate"] = *update.FictionalDate
comment.FictionalDate = *update.FictionalDate
}
if update.CharacterID != nil {
updateBson["characterId"] = *update.CharacterID
comment.CharacterID = *update.CharacterID
}
if update.CharacterName != nil {
updateBson["characterName"] = *update.CharacterName
comment.CharacterName = *update.CharacterName
}
err := r.comments.UpdateId(comment.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &comment, nil
}
func (r *commentRepository) Delete(ctx context.Context, comment models.Comment) error {
return r.comments.RemoveId(comment.ID)
}
func (r *commentRepository) fixFieldTypo() {
c, err := r.comments.UpdateAll(bson.M{
"editeddDate": bson.M{"$ne": nil},
}, bson.M{
"$rename": bson.M{"editeddDate": "editedDate"},
})
if err != nil {
if err == mgo.ErrNotFound {
return
}
log.Println("Failed to run name typo fix:", err)
return
}
if c.Updated > 0 {
log.Println("Fixed editeddDate field name typo in", c.Updated, "comments")
}
}
+276
View File
@@ -0,0 +1,276 @@
package mongodb
import (
"context"
"fmt"
"github.com/globalsign/mgo/bson"
"time"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
)
var DisableFixes bool
type MongoDB struct {
session *mgo.Session
changes repositories.ChangeRepository
channels *channelRepository
characters repositories.CharacterRepository
tags repositories.TagRepository
logs *logRepository
posts *postRepository
files *fileRepository
stories repositories.StoryRepository
chapters repositories.ChapterRepository
comments repositories.CommentRepository
keys repositories.KeyRepository
users repositories.UserRepository
}
func (m *MongoDB) Changes() repositories.ChangeRepository {
return m.changes
}
func (m *MongoDB) Characters() repositories.CharacterRepository {
return m.characters
}
func (m *MongoDB) Tags() repositories.TagRepository {
return m.tags
}
func (m *MongoDB) Logs() repositories.LogRepository {
return m.logs
}
func (m *MongoDB) Posts() repositories.PostRepository {
return m.posts
}
func (m *MongoDB) Channels() repositories.ChannelRepository {
return m.channels
}
func (m *MongoDB) Files() repositories.FileRepository {
return m.files
}
func (m *MongoDB) Stories() repositories.StoryRepository {
return m.stories
}
func (m *MongoDB) Chapters() repositories.ChapterRepository {
return m.chapters
}
func (m *MongoDB) Comments() repositories.CommentRepository {
return m.comments
}
func (m *MongoDB) Keys() repositories.KeyRepository {
return m.keys
}
func (m *MongoDB) Users() repositories.UserRepository {
return m.users
}
func (m *MongoDB) Close(ctx context.Context) error {
m.session.Close()
return nil
}
// Init initializes the mongodb database
func Init(cfg config.Database) (*MongoDB, error) {
port := cfg.Port
if port <= 0 {
port = 27017
}
session, err := mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{fmt.Sprintf("%s:%d", cfg.Host, port)},
Timeout: 30 * time.Second,
Database: cfg.Db,
Username: cfg.Username,
Password: cfg.Password,
Mechanism: cfg.Mechanism,
Source: cfg.Db,
})
if err != nil {
return nil, err
}
db := session.DB(cfg.Db)
characters, err := newCharacterRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
channels, err := newChannelRepository(db)
if err != nil {
session.Close()
return nil, err
}
changes, err := newChangeRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
logs, err := newLogRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
posts, err := newPostRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
files, err := newFileRepository(db)
if err != nil {
session.Close()
return nil, err
}
stories, err := newStoryRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
chapters, err := newChapterRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
comments, err := newCommentRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
keys, err := newKeyRepository(db, cfg.RestoreIDs)
if err != nil {
session.Close()
return nil, err
}
users, err := newUserRepository(db)
if err != nil {
session.Close()
return nil, err
}
if !DisableFixes {
go posts.fixPositions(logs)
}
return &MongoDB{
session: session,
changes: changes,
characters: characters,
channels: channels,
tags: newTagRepository(db),
stories: stories,
chapters: chapters,
comments: comments,
logs: logs,
posts: posts,
files: files,
keys: keys,
users: users,
}, nil
}
type counter struct {
coll *mgo.Collection
category string
name string
}
func newCounter(db *mgo.Database, category, name string) *counter {
return &counter{
coll: db.C("core.counters"),
category: category,
name: name,
}
}
func (c *counter) WithName(name string) *counter {
return &counter{
coll: c.coll,
category: c.category,
name: name,
}
}
func (c *counter) WithCategory(category string) *counter {
return &counter{
coll: c.coll,
category: category,
name: c.name,
}
}
func (c *counter) With(category, name string) *counter {
return &counter{
coll: c.coll,
category: category,
name: name,
}
}
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"`
Value int `bson:"value"`
}
id := c.category + "." + c.name
doc := counterDoc{}
_, err := c.coll.Find(bson.M{"_id": id}).Apply(mgo.Change{
Update: bson.M{"$inc": bson.M{"value": amount}},
Upsert: true,
ReturnNew: true,
}, &doc)
if err != nil {
return -1, err
}
return doc.Value, nil
}
+105
View File
@@ -0,0 +1,105 @@
package mongodb
import (
"context"
"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"
)
type fileRepository struct {
files *mgo.Collection
}
func (r *fileRepository) Find(ctx context.Context, id string) (*models.File, error) {
file := new(models.File)
err := r.files.FindId(id).One(file)
if err != nil {
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
}
return nil, err
}
return file, nil
}
func (r *fileRepository) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) {
query := bson.M{}
if filter.Author != nil {
query["author"] = *filter.Author
}
if filter.MimeTypes != nil {
query["mimeType"] = bson.M{"$in": filter.MimeTypes}
}
if filter.Public != nil {
query["public"] = *filter.Public
}
files := make([]*models.File, 0, 16)
err := r.files.Find(query).Sort("-date").All(&files)
if err != nil {
return nil, err
}
return files, nil
}
func (r *fileRepository) Insert(ctx context.Context, file models.File) (*models.File, error) {
file.ID = generate.FileID()
err := r.files.Insert(&file)
if err != nil {
return nil, err
}
return &file, nil
}
func (r *fileRepository) Update(ctx context.Context, file models.File, update models.FileUpdate) (*models.File, error) {
updateBson := bson.M{}
if update.Public != nil {
updateBson["public"] = *update.Public
file.Public = *update.Public
}
if update.Name != nil {
updateBson["name"] = *update.Name
file.Name = *update.Name
}
err := r.files.UpdateId(file.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &file, nil
}
func (r *fileRepository) Delete(ctx context.Context, file models.File) error {
return r.files.RemoveId(file.ID)
}
func newFileRepository(db *mgo.Database) (*fileRepository, error) {
files := db.C("file.headers")
err := files.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = files.EnsureIndexKey("mimeType")
if err != nil {
return nil, err
}
err = files.EnsureIndexKey("public")
if err != nil {
return nil, err
}
return &fileRepository{files: files}, nil
}
+75
View File
@@ -0,0 +1,75 @@
package mongodb
import (
"context"
"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"
)
type keyRepository struct {
restoreIDs bool
keys *mgo.Collection
}
func newKeyRepository(db *mgo.Database, restoreIDs bool) (repositories.KeyRepository, error) {
collection := db.C("auth.keys")
err := collection.EnsureIndexKey("user")
if err != nil {
return nil, err
}
return &keyRepository{
keys: collection,
restoreIDs: restoreIDs,
}, nil
}
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
key := models.Key{}
err := r.keys.FindId(id).One(&key)
if err != nil {
return nil, err
}
return &key, err
}
func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error) {
query := bson.M{}
if filter.UserID != nil {
query["user"] = *filter.UserID
}
keys := make([]*models.Key, 0, 4)
err := r.keys.Find(query).All(&keys)
if err != nil {
if err == mgo.ErrNotFound {
return nil, nil
}
return nil, err
}
return keys, nil
}
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
if !r.restoreIDs {
key.ID = generate.KeyID()
}
err := r.keys.Insert(&key)
if err != nil {
return nil, err
}
return &key, nil
}
func (r *keyRepository) Delete(ctx context.Context, key models.Key) error {
return r.keys.RemoveId(key.ID)
}
+215
View File
@@ -0,0 +1,215 @@
package mongodb
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"strconv"
"sync"
)
type logRepository struct {
openMutex sync.Mutex
restoreIds bool
logs *mgo.Collection
posts *mgo.Collection
shortIdCounter *counter
}
func newLogRepository(db *mgo.Database, restoreIds bool) (*logRepository, error) {
logs := db.C("logbot3.logs")
posts := db.C("logbot3.posts")
err := logs.EnsureIndexKey("date")
if err != nil {
return nil, err
}
err = logs.EnsureIndexKey("channel")
if err != nil {
return nil, err
}
err = logs.EnsureIndexKey("characterIds")
if err != nil {
return nil, err
}
err = logs.EnsureIndexKey("event")
if err != nil {
return nil, err
}
err = logs.EnsureIndex(mgo.Index{Key: []string{"channel", "open"}})
if err != nil {
return nil, err
}
err = logs.EnsureIndex(mgo.Index{
Key: []string{"shortId"},
Unique: true,
DropDups: true,
})
if err != nil {
return nil, err
}
return &logRepository{
restoreIds: restoreIds,
logs: logs,
posts: posts,
shortIdCounter: newCounter(db, "auto_increment", "Log"),
}, nil
}
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) {
log := new(models.Log)
err := r.logs.Find(bson.M{"$or": []bson.M{{"_id": id}, {"shortId": id}}}).One(log)
if err != nil {
return nil, err
}
return log, nil
}
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) {
query := bson.M{}
if filter.Search != nil {
searchQuery := bson.M{
"$text": bson.M{"$search": *filter.Search},
"logId": bson.M{"$ne": nil},
}
logIds := make([]string, 0, 64)
err := r.posts.Find(searchQuery).Distinct("logId", &logIds)
if err != nil {
return nil, err
}
query["shortId"] = bson.M{"$in": logIds}
}
if filter.Open != nil {
r.openMutex.Lock()
defer r.openMutex.Unlock()
query["open"] = filter.Open
}
if len(filter.Characters) > 0 {
query["characterIds"] = bson.M{"$all": filter.Characters}
}
if len(filter.Channels) > 0 {
query["channel"] = bson.M{"$in": filter.Channels}
}
if len(filter.Events) > 0 {
query["event"] = bson.M{"$in": filter.Events}
}
if filter.MinDate != nil && filter.MaxDate != nil {
query["date"] = bson.M{"$gte": *filter.MinDate, "$lte": *filter.MaxDate}
} else if filter.MinDate != nil {
query["date"] = bson.M{"$gte": *filter.MinDate}
} else if filter.MaxDate != nil {
query["date"] = bson.M{"$lt": *filter.MaxDate}
}
logs := make([]*models.Log, 0, 32)
err := r.logs.Find(query).Sort("-date").Limit(filter.Limit).All(&logs)
if err != nil {
if err == mgo.ErrNotFound {
return logs, nil
}
return nil, err
}
return logs, nil
}
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) {
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)
} 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}})
if err != nil {
return nil, errors.New("Cannot close other logs: " + err.Error())
}
}
err := r.logs.Insert(&log)
if err != nil {
return nil, err
}
return &log, nil
}
func (r *logRepository) Update(ctx context.Context, log models.Log, update models.LogUpdate) (*models.Log, error) {
updateBson := bson.M{}
if update.Open != nil {
if *update.Open == true {
// 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}})
if err != nil {
return nil, errors.New("Cannot close other logs: " + err.Error())
}
}
updateBson["open"] = *update.Open
log.Open = *update.Open
}
if update.Title != nil {
updateBson["title"] = *update.Title
log.Title = *update.Title
}
if update.Description != nil {
updateBson["description"] = *update.Description
log.Description = *update.Description
}
if update.EventName != nil {
updateBson["event"] = *update.EventName
log.EventName = *update.EventName
}
if update.CharacterIDs != nil {
updateBson["characterIds"] = update.CharacterIDs
log.CharacterIDs = update.CharacterIDs
}
err := r.logs.UpdateId(log.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &log, nil
}
func (r *logRepository) Delete(ctx context.Context, log models.Log) error {
err := r.logs.RemoveId(log.ID)
if err != nil {
return err
}
_, _ = r.posts.RemoveAll(bson.M{"logId": log.ShortID})
return nil
}
+362
View File
@@ -0,0 +1,362 @@
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"
"sync"
"time"
)
type postRepository struct {
restoreIDs bool
logs *mgo.Collection
posts *mgo.Collection
orderMutex sync.Mutex
}
func newPostRepository(db *mgo.Database, restoreIDs bool) (*postRepository, error) {
posts := db.C("logbot3.posts")
err := posts.EnsureIndexKey("logId")
if err != nil {
return nil, err
}
err = posts.EnsureIndexKey("time")
if err != nil {
return nil, err
}
err = posts.EnsureIndexKey("kind")
if err != nil {
return nil, err
}
err = posts.EnsureIndexKey("position")
if err != nil {
return nil, err
}
err = posts.EnsureIndex(mgo.Index{
Key: []string{"$text:text"},
})
if err != nil {
return nil, err
}
return &postRepository{
restoreIDs: restoreIDs,
posts: posts,
logs: db.C("logbot3.logs"),
}, nil
}
func (r *postRepository) Find(ctx context.Context, id string) (*models.Post, error) {
post := new(models.Post)
err := r.posts.FindId(id).One(post)
if err != nil {
return nil, err
}
return post, nil
}
func (r *postRepository) List(ctx context.Context, filter models.PostFilter) ([]*models.Post, error) {
query := bson.M{}
if filter.LogID != nil && *filter.LogID != "" {
logId := *filter.LogID
if !strings.HasPrefix(logId, "L") {
// Resolve long id to short id
log := new(models.Log)
err := r.logs.FindId(logId).Select(bson.M{"logId": 1, "_id": 1}).One(log)
if err != nil {
return nil, err
}
logId = log.ShortID
}
query["logId"] = logId
}
if len(filter.IDs) > 0 {
query["_id"] = bson.M{"$in": filter.IDs}
}
if len(filter.Kinds) > 0 {
query["kind"] = bson.M{"$in": filter.Kinds}
}
if filter.Search != nil {
query["$text"] = bson.M{"$search": *filter.Search}
}
posts := make([]*models.Post, 0, 32)
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
}
return nil, err
}
return posts, nil
}
func (r *postRepository) Insert(ctx context.Context, post models.Post) (*models.Post, error) {
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
lastPost := new(models.Post)
err := r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
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)
if err != nil {
return nil, err
}
return &post, nil
}
func (r *postRepository) InsertMany(ctx context.Context, posts ...*models.Post) ([]*models.Post, error) {
if len(posts) == 0 {
return []*models.Post{}, nil
}
logId := posts[0].LogID
for _, post := range posts {
if post.LogID != logId {
return nil, repositories.ErrParentMismatch
}
if !r.restoreIDs || post.ID == "" {
post.ID = generate.PostID()
}
}
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
lastPost := new(models.Post)
err := r.posts.Find(bson.M{"logId": posts[0].LogID}).Sort("-position").One(lastPost)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
docs := make([]interface{}, len(posts))
for i := range posts {
posts[i].Position = lastPost.Position + 1 + i
docs[i] = posts[i]
}
err = r.posts.Insert(docs...)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
return posts, nil
}
func (r *postRepository) Update(ctx context.Context, post models.Post, update models.PostUpdate) (*models.Post, error) {
updateBson := bson.M{}
if update.Time != nil {
updateBson["time"] = *update.Time
post.Time = *update.Time
}
if update.Kind != nil {
updateBson["kind"] = *update.Kind
post.Kind = *update.Kind
}
if update.Nick != nil {
updateBson["nick"] = *update.Nick
post.Nick = *update.Nick
}
if update.Text != nil {
updateBson["text"] = *update.Text
post.Text = *update.Text
}
err := r.posts.UpdateId(post.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &post, nil
}
func (r *postRepository) Move(ctx context.Context, post models.Post, position int) ([]*models.Post, error) {
// If only MongoDB transactions weren't awful, this function would have been safe.
// Since it isn't, then good luck.
// Validate lower position bound.
if position < 1 {
return nil, repositories.ErrInvalidPosition
}
// Determine the operations on adjacent posts
var resultFilter bson.M
var pushFilter bson.M
var increment int
if post.Position > position {
pushFilter = bson.M{"position": bson.M{
"$gte": position,
"$lt": post.Position,
}}
increment = 1
resultFilter = bson.M{"position": bson.M{
"$lte": post.Position,
"$gte": position,
}}
} else {
pushFilter = bson.M{"position": bson.M{
"$lte": position,
"$gt": post.Position,
}}
increment = -1
resultFilter = bson.M{"position": bson.M{
"$lte": position,
"$gte": post.Position,
}}
}
pushFilter["logId"] = post.LogID
resultFilter["logId"] = post.LogID
// From here on out, sync is required
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
// Detect ninja shenanigans
post2 := new(models.Post)
err := r.posts.FindId(post.ID).One(post2)
if err != nil && post2.Position != post.Position {
return nil, repositories.ErrNotFound
}
// Validate upper position bound
lastPost := new(models.Post)
err = r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
if position > lastPost.Position {
return nil, repositories.ErrInvalidPosition
}
// Move the posts
changeInfo, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": increment}})
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
err = r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": position}})
if err != nil {
// Try to undo it
_, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": -increment}})
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
return nil, err
}
results := make([]*models.Post, 0, changeInfo.Matched+1)
err = r.posts.Find(resultFilter).All(&results)
if err != nil {
return nil, err
}
return results, nil
}
func (r *postRepository) Delete(ctx context.Context, post models.Post) error {
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
err := r.posts.RemoveId(post.ID)
if err != nil {
return err
}
_, _ = r.posts.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
return nil
}
func (r *postRepository) fixPositions(logRepo *logRepository) {
disorders := make([]int, 0, 16)
diffs := make([]int, 0, 16)
startTime := time.Now()
timeout, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
logs, err := logRepo.List(timeout, models.LogFilter{})
if err != nil {
log.Println("Failed to get logs for position fix:", err)
}
log.Println("Starting log position fixing, this should not take longer than 10 seconds.")
for _, l := range logs {
r.orderMutex.Lock()
posts, err := r.List(timeout, models.PostFilter{LogID: &l.ShortID})
if err != nil {
r.orderMutex.Unlock()
continue
}
disorders = disorders[:0]
diffs = diffs[:0]
for i, post := range posts {
if post.Position != (i + 1) {
disorders = append(disorders, i)
diffs = append(diffs, post.Position-(i+1))
}
}
if len(disorders) > 0 {
log.Println(len(disorders), "order errors detected in", l.ID)
ops := 0
for i, post := range posts {
if (i + 1) != posts[i].Position {
ops++
err := r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": i + 1}})
if err != nil {
log.Println(l.ShortID, "fix failed after", ops, "ops:", err)
break
}
}
}
log.Println(l.ShortID, "fixed after", ops, "ops.")
}
r.orderMutex.Unlock()
}
log.Println("Log position fixing finished in", time.Since(startTime))
}
+224
View File
@@ -0,0 +1,224 @@
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
restoreIDs bool
}
func newStoryRepository(db *mgo.Database, restoreIDs bool) (repositories.StoryRepository, error) {
collection := db.C("story.stories")
err := collection.EnsureIndexKey("tags")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("updatedDate")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("fictionalDate")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("listed")
if err != nil {
return nil, err
}
return &storyRepository{
stories: collection,
restoreIDs: restoreIDs,
chapters: db.C("story.chapters"),
comments: db.C("story.comments"),
}, nil
}
func (r *storyRepository) Find(ctx context.Context, id string) (*models.Story, error) {
story := new(models.Story)
err := r.stories.FindId(id).One(story)
if err != nil {
return nil, err
}
return story, nil
}
func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) {
query := bson.M{}
if filter.Author != nil {
query["author"] = *filter.Author
}
if filter.Category != nil {
query["category"] = *filter.Category
}
if filter.Open != nil {
query["open"] = *filter.Open
}
if len(filter.Tags) > 0 {
query["tags"] = bson.M{"$all": filter.Tags}
}
if filter.Unlisted != nil {
query["listed"] = !*filter.Unlisted
} else {
query["listed"] = true
}
if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() {
query["fictionalDate"] = bson.M{
"$gte": filter.EarliestFictionalDate,
"$lte": filter.LatestFictionalDate,
}
} else if !filter.EarliestFictionalDate.IsZero() {
query["fictionalDate"] = bson.M{
"$gte": filter.EarliestFictionalDate,
}
} else if !filter.LatestFictionalDate.IsZero() {
query["fictionalDate"] = bson.M{
"$lte": filter.LatestFictionalDate,
}
}
stories := make([]*models.Story, 0, 32)
err := r.stories.Find(query).Sort("-updatedDate").Limit(filter.Limit).All(&stories)
if err != nil {
if err == mgo.ErrNotFound {
return stories, nil
}
return nil, err
}
return stories, nil
}
func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) {
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 {
return nil, err
}
return &story, nil
}
func (r *storyRepository) Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) {
updateBson := bson.M{}
if update.Name != nil {
updateBson["name"] = *update.Name
story.Name = *update.Name
}
if update.Open != nil {
updateBson["open"] = *update.Open
story.Open = *update.Open
}
if update.Category != nil {
updateBson["category"] = *update.Category
story.Category = *update.Category
}
if update.Author != nil {
updateBson["author"] = *update.Author
story.Author = *update.Author
}
if update.FictionalDate != nil {
updateBson["fictionalDate"] = *update.FictionalDate
story.FictionalDate = *update.FictionalDate
}
if update.UpdatedDate != nil {
updateBson["updatedDate"] = *update.UpdatedDate
story.UpdatedDate = *update.UpdatedDate
}
if update.Listed != nil {
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 {
return nil, err
}
return &story, nil
}
func (r *storyRepository) AddTag(ctx context.Context, story models.Story, tag models.Tag) error {
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$addToSet": bson.M{"tags": tag}})
if err != nil {
return err
}
if ci.Updated == 0 {
return repositories.ErrTagExists
}
return nil
}
func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error {
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$pull": bson.M{"tags": tag}})
if err != nil {
return err
}
if ci.Updated == 0 {
return repositories.ErrTagDoesNotExist
}
return nil
}
func (r *storyRepository) Delete(ctx context.Context, story models.Story) error {
err := r.stories.RemoveId(story.ID)
if err != nil {
return err
}
chapterIds := make([]string, 0, 8)
err = r.chapters.Find(bson.M{"storyId": story.ID}).Distinct("_id", &chapterIds)
if err != nil {
log.Println("Failed to find chapterIds:", err)
return nil
}
if len(chapterIds) > 0 {
c1, err := r.chapters.RemoveAll(bson.M{"storyId": story.ID})
if err != nil {
log.Println("Failed to remove chapters:", err)
return nil
}
c2, err := r.comments.RemoveAll(bson.M{"chapterId": bson.M{"$in": chapterIds}})
if err != nil {
log.Println("Failed to remove comments:", err)
return nil
}
log.Printf("Removed story %s (%d chapters, %d comments)", story.ID, c1.Removed, c2.Removed)
}
return nil
}
+78
View File
@@ -0,0 +1,78 @@
package mongodb
import (
"context"
"sort"
"strings"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"go.mongodb.org/mongo-driver/bson"
)
type tagRepository struct {
stories *mgo.Collection
}
func newTagRepository(db *mgo.Database) repositories.TagRepository {
return &tagRepository{
stories: db.C("story.stories"),
}
}
func (r *tagRepository) Find(ctx context.Context, kind models.TagKind, name string) (*models.Tag, error) {
tags := make([]*models.Tag, 0, 1)
err := r.stories.Find(bson.M{"listed": true, "tags": bson.M{"kind": kind, "name": name}}).Distinct("tag", &tags)
if err != nil {
return nil, err
} else if len(tags) == 0 {
return nil, repositories.ErrNotFound
}
for _, tag := range tags {
if tag.Kind == kind && tag.Name == name {
return tag, nil
}
}
return nil, repositories.ErrNotFound
}
func (r *tagRepository) List(ctx context.Context, filter models.TagFilter) ([]*models.Tag, error) {
tags := make([]*models.Tag, 0, 64)
query := bson.M{"listed": true, "tags": bson.M{"$ne": nil}}
if filter.Kind != nil {
query["tags.kind"] = *filter.Kind
}
err := r.stories.Find(query).Distinct("tags", &tags)
if err != nil {
return nil, err
}
if filter.Kind != nil {
// While it's unsorted, delete any incorrect tags with replacement.
for i := 0; i < len(tags); i++ {
if tags[i].Kind != *filter.Kind {
tags[i] = tags[len(tags)-1]
tags = tags[:len(tags)-1]
i--
}
}
}
sort.Slice(tags, func(i, j int) bool {
if filter.Kind == nil {
kindCmp := strings.Compare(string(tags[i].Kind), string(tags[j].Kind))
if kindCmp != 0 {
return kindCmp < 0
}
}
return strings.Compare(tags[i].Name, tags[j].Name) < 0
})
return tags, nil
}
+52
View File
@@ -0,0 +1,52 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
type userRepository struct {
users *mgo.Collection
}
func newUserRepository(db *mgo.Database) (repositories.UserRepository, error) {
collection := db.C("core.users")
return &userRepository{users: collection}, nil
}
func (r *userRepository) Find(ctx context.Context, id string) (*models.User, error) {
user := new(models.User)
err := r.users.FindId(id).One(user)
if err != nil {
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
}
return nil, 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 {
return nil, err
}
return &user, 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
}
+113
View File
@@ -0,0 +1,113 @@
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.CharacterID,
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(),
CharacterName: comment.ChapterID,
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
+303
View File
@@ -0,0 +1,303 @@
package postgres
import (
"context"
"database/sql"
"errors"
"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.DeletePost(ctx, post.ID)
if err != nil {
return err
}
err = q.ShiftPostsAfter(ctx, psqlcore.ShiftPostsAfterParams{
ShiftOffset: -1,
LogShortID: post.LogID,
FromPosition: int32(post.Position + 1),
})
if err != nil {
return err
}
_ = q.GenerateLogTSVector(ctx, post.LogID)
return tx.Commit()
}
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;
+36
View File
@@ -0,0 +1,36 @@
-- 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,
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
}
+62
View File
@@ -0,0 +1,62 @@
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, "\"") {
if len(token) > 1 {
token = token[1:]
}
startQuotes = true
}
if strings.HasSuffix(token, "\"") {
token = token[:len(token)-1]
startQuotes = false
clearQuotes = true
}
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, " ")
}
+24
View File
@@ -0,0 +1,24 @@
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`},
{`"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]))
})
}
}
+25
View File
@@ -0,0 +1,25 @@
module git.aiterp.net/rpdata/api
go 1.12
require (
github.com/99designs/gqlgen v0.9.0
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/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/prometheus/client_golang v0.9.3
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e // indirect
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/sync v0.0.0-20190423024810-112230192c58
gopkg.in/yaml.v2 v2.4.0
)
replace golang.org/x/net => golang.org/x/net v0.0.0-20180416171110-a35a21de978d
+497
View File
@@ -0,0 +1,497 @@
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/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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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.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/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/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/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/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
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/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/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 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/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=
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
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=
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-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-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/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/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-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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/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=
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
echo "# Generated by stiching together the files under schema/ – DO NOT EDIT" >generated.gql
cat schema/root.gql schema/**/*.gql >>generated.gql

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