Browse Source

models: Painstakingly converted half the model package to a models & repostiry stucture. All binaries are probably dead at this point.

1.0
Gisle Aune 6 years ago
parent
commit
0768cfd0bd
  1. 22
      graph2/gqlgen.yml
  2. 6
      graph2/graph.go
  3. 11
      graph2/queries/channel.go
  4. 15
      graph2/queries/character.go
  5. 28
      graph2/queries/log.go
  6. 29
      graph2/queries/post.go
  7. 7
      graph2/queries/tags.go
  8. 5
      graph2/schema/root.gql
  9. 2
      graph2/schema/types/Log.gql
  10. 8
      graph2/schema/types/Post.gql
  11. 40
      graph2/types/log.go
  12. 17
      internal/loader/channel.go
  13. 24
      internal/loader/character.go
  14. 6
      internal/loader/loader.go
  15. 10
      models/channel.go
  16. 33
      models/channels/add.go
  17. 19
      models/channels/db.go
  18. 11
      models/channels/find.go
  19. 46
      models/channels/list.go
  20. 20
      models/character.go
  21. 48
      models/characters/add.go
  22. 55
      models/characters/db.go
  23. 16
      models/characters/find.go
  24. 61
      models/characters/list.go
  25. 16
      models/log.go
  26. 65
      models/logs/db.go
  27. 16
      models/logs/find.go
  28. 70
      models/logs/list.go
  29. 14
      models/post.go
  30. 56
      models/posts/db.go
  31. 14
      models/posts/find.go
  32. 57
      models/posts/list.go
  33. 1
      models/posts/search.go
  34. 0
      models/scalars/date.go
  35. 47
      models/tag-kind.go
  36. 7
      models/tag.go
  37. 14
      models/tags/db.go
  38. 26
      models/tags/list.go

22
graph2/gqlgen.yml

@ -10,22 +10,24 @@ model:
models:
Tag:
model: git.aiterp.net/rpdata/api/model/story.Tag
model: git.aiterp.net/rpdata/api/models.Tag
TagKind:
model: git.aiterp.net/rpdata/api/model/story.TagKind
model: git.aiterp.net/rpdata/api/models.TagKind
Character:
model: git.aiterp.net/rpdata/api/model/character.Character
model: git.aiterp.net/rpdata/api/models.Character
CharactersFilter:
model: git.aiterp.net/rpdata/api/model/character.Filter
model: git.aiterp.net/rpdata/api/models/characters.Filter
Channel:
model: git.aiterp.net/rpdata/api/model/channel.Channel
model: git.aiterp.net/rpdata/api/models.Channel
ChannelsFilter:
model: git.aiterp.net/rpdata/api/model/channel.Filter
model: git.aiterp.net/rpdata/api/models/channels.Filter
Post:
model: git.aiterp.net/rpdata/api/model/log.Post
model: git.aiterp.net/rpdata/api/models.Post
PostsFilter:
model: git.aiterp.net/rpdata/api/models/posts.Filter
Log:
model: git.aiterp.net/rpdata/api/model/log.Log
model: git.aiterp.net/rpdata/api/models.Log
LogsFilter:
model: git.aiterp.net/rpdata/api/model/log.Filter
model: git.aiterp.net/rpdata/api/models/logs.Filter
Date:
model: git.aiterp.net/rpdata/api/model/scalars.Date
model: git.aiterp.net/rpdata/api/models/scalars.Date

6
graph2/graph.go

@ -2,12 +2,14 @@ package graph2
import (
"git.aiterp.net/rpdata/api/graph2/queries"
"git.aiterp.net/rpdata/api/graph2/types"
graphql "github.com/99designs/gqlgen/graphql"
)
//go:generate ./combine.sh
//go:generate gorunpkg github.com/99designs/gqlgen -v
// New creates a new GraphQL schema.
func New() graphql.ExecutableSchema {
return NewExecutableSchema(Config{
Resolvers: &rootResolver{},
@ -19,3 +21,7 @@ type rootResolver struct{}
func (r *rootResolver) Query() QueryResolver {
return &queries.Resolver
}
func (r *rootResolver) Log() LogResolver {
return &types.LogResolver
}

11
graph2/queries/channel.go

@ -3,13 +3,14 @@ package queries
import (
"context"
"git.aiterp.net/rpdata/api/model/channel"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/channels"
)
func (r *resolver) Channel(ctx context.Context, name string) (channel.Channel, error) {
return channel.FindName(name)
func (r *resolver) Channel(ctx context.Context, name string) (models.Channel, error) {
return channels.FindName(name)
}
func (r *resolver) Channels(ctx context.Context, filter *channel.Filter) ([]channel.Channel, error) {
return channel.List(filter)
func (r *resolver) Channels(ctx context.Context, filter *channels.Filter) ([]models.Channel, error) {
return channels.List(filter)
}

15
graph2/queries/character.go

@ -4,19 +4,20 @@ import (
"context"
"errors"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/characters"
)
func (r *resolver) Character(ctx context.Context, id *string, nick *string) (character.Character, error) {
func (r *resolver) Character(ctx context.Context, id *string, nick *string) (models.Character, error) {
if id != nil {
return character.FindID(*id)
return characters.FindID(*id)
} else if nick != nil {
return character.FindNick(*nick)
return characters.FindNick(*nick)
} else {
return character.Character{}, errors.New("You must specify either an ID or a nick")
return models.Character{}, errors.New("You must specify either an ID or a nick")
}
}
func (r *resolver) Characters(ctx context.Context, filter *character.Filter) ([]character.Character, error) {
return character.List(filter)
func (r *resolver) Characters(ctx context.Context, filter *characters.Filter) ([]models.Character, error) {
return characters.List(filter)
}

28
graph2/queries/log.go

@ -3,30 +3,42 @@ package queries
import (
"context"
"errors"
"strings"
"git.aiterp.net/rpdata/api/internal/loader"
"git.aiterp.net/rpdata/api/model/log"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/logs"
"github.com/99designs/gqlgen/graphql"
)
func (r *resolver) Log(ctx context.Context, id string) (log.Log, error) {
return log.FindID(id)
func (r *resolver) Log(ctx context.Context, id string) (models.Log, error) {
return logs.FindID(id)
}
func (r *resolver) Logs(ctx context.Context, filter *log.Filter) ([]log.Log, error) {
logs, err := log.List(filter)
func (r *resolver) Logs(ctx context.Context, filter *logs.Filter) ([]models.Log, error) {
logs, err := logs.List(filter)
if err != nil {
return nil, err
}
if len(logs) >= 100 {
reqCtx := graphql.GetRequestContext(ctx)
maybeCharacters := strings.Contains(reqCtx.RawQuery, "characters")
maybeChannels := strings.Contains(reqCtx.RawQuery, "channels")
if len(logs) >= 100 && (maybeCharacters || maybeChannels) {
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
for _, log := range logs {
loader.PrimeCharacters("id", log.CharacterIDs...)
loader.PrimeChannels("name", log.ChannelName)
if maybeChannels {
loader.PrimeChannels("name", log.ChannelName)
}
if maybeCharacters {
loader.PrimeCharacters("id", log.CharacterIDs...)
}
}
}

29
graph2/queries/post.go

@ -2,14 +2,33 @@ package queries
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/model/log"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/posts"
)
func (r *resolver) Post(ctx context.Context, id string) (log.Post, error) {
return log.FindPostID(id)
func (r *resolver) Post(ctx context.Context, id string) (models.Post, error) {
return posts.FindID(id)
}
func (r *resolver) Posts(ctx context.Context, ids []string) ([]log.Post, error) {
return log.ListPostIDs(ids...)
func (r *resolver) Posts(ctx context.Context, filter *posts.Filter) ([]models.Post, error) {
// Some sanity checks to avoid querying an insame amount of posts.
if filter == nil {
filter = &posts.Filter{Limit: 256}
} else {
if (filter.Limit <= 0 || filter.Limit > 256) && filter.LogID == nil {
return nil, errors.New("a limit of 0 (no limit) or >256 without a logId is not allowed")
}
if len(filter.Kind) > 32 {
return nil, errors.New("You cannot specify more than 32 kinds")
}
if len(filter.ID) > 32 {
return nil, errors.New("You cannot specify more than 32 IDs")
}
}
return posts.List(filter)
}

7
graph2/queries/tags.go

@ -3,9 +3,10 @@ package queries
import (
"context"
"git.aiterp.net/rpdata/api/model/story"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/tags"
)
func (r *resolver) Tags(ctx context.Context) ([]story.Tag, error) {
return story.ListTags()
func (r *resolver) Tags(ctx context.Context) ([]models.Tag, error) {
return tags.List()
}

5
graph2/schema/root.gql

@ -20,9 +20,8 @@ type Query {
# Find post by ID.
post(id: String!): Post!
# Find posts by IDs. It's meant to allow other parts of the UI to link to a cluster of posts, e.g. for a room description for the
# Mapp should it ever become a thing. This does not have a filter, since it's meant to be queried in the logs' response's selection set.
posts(ids: [String!]!): [Post!]!
# Find posts
posts(filter: PostsFilter): [Post!]!
# Find log by ID

2
graph2/schema/types/Log.gql

@ -21,7 +21,7 @@ type Log {
# The log's event, which is the same as the tags in the previous logbot site.
# Empty string means that it's no event.
event: String!
eventName: String!
# The description of a session, which is empty if unset.
description: String!

8
graph2/schema/types/Post.gql

@ -65,4 +65,12 @@ input MovePostInput {
# Target index
toPosition: Int!
}
# Filter for posts query
input PostsFilter {
id: [String!]
kind: [String!]
logId: String
limit: Int
}

40
graph2/types/log.go

@ -0,0 +1,40 @@
package types
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/loader"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/posts"
)
type logResolver struct{}
func (r *logResolver) Channel(ctx context.Context, log *models.Log) (models.Channel, error) {
loader := loader.FromContext(ctx)
if loader == nil {
return models.Channel{}, errors.New("no loader")
}
return loader.Channel("name", log.ChannelName)
}
func (r *logResolver) Characters(ctx context.Context, log *models.Log) ([]models.Character, error) {
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
return loader.Characters("id", log.CharacterIDs...)
}
func (r *logResolver) Posts(ctx context.Context, log *models.Log, kinds []string) ([]models.Post, error) {
return posts.List(&posts.Filter{
LogID: &log.ShortID,
Kind: kinds,
})
}
// LogResolver is a resolver
var LogResolver logResolver

17
internal/loader/channel.go

@ -5,18 +5,19 @@ import (
"errors"
"strings"
"git.aiterp.net/rpdata/api/model/channel"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/channels"
"github.com/graph-gophers/dataloader"
)
// Channel gets a character by key
func (loader *Loader) Channel(key, value string) (channel.Channel, error) {
func (loader *Loader) Channel(key, value string) (models.Channel, error) {
if !strings.HasPrefix(key, "Channel.") {
key = "Channel." + key
}
if loader.loaders[key] == nil {
return channel.Channel{}, errors.New("unsupported key")
return models.Channel{}, errors.New("unsupported key")
}
loader.loadPrimed(key)
@ -24,10 +25,10 @@ func (loader *Loader) Channel(key, value string) (channel.Channel, error) {
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
res, err := thunk()
if err != nil {
return channel.Channel{}, err
return models.Channel{}, err
}
channel, ok := res.(channel.Channel)
channel, ok := res.(models.Channel)
if !ok {
return channel, errors.New("incorrect type")
}
@ -48,10 +49,10 @@ func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.R
var results []*dataloader.Result
names := keys.Keys()
channels, err := channel.ListNames(names...)
channels, err := channels.ListNames(names...)
if err != nil {
for range names {
results = append(results, &dataloader.Result{Data: channel.Channel{}, Error: err})
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
}
return results
@ -69,7 +70,7 @@ func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.R
}
if !found {
results = append(results, &dataloader.Result{Data: channel.Channel{}, Error: err})
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
}
}

24
internal/loader/character.go

@ -6,11 +6,13 @@ import (
"strings"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/characters"
"github.com/graph-gophers/dataloader"
)
// Character gets a character by key
func (loader *Loader) Character(key, value string) (character.Character, error) {
func (loader *Loader) Character(key, value string) (models.Character, error) {
if !strings.HasPrefix(key, "Character.") {
key = "Character." + key
}
@ -18,25 +20,25 @@ func (loader *Loader) Character(key, value string) (character.Character, error)
loader.loadPrimed(key)
if loader.loaders[key] == nil {
return character.Character{}, errors.New("unsupported key")
return models.Character{}, errors.New("unsupported key")
}
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
res, err := thunk()
if err != nil {
return character.Character{}, err
return models.Character{}, err
}
char, ok := res.(character.Character)
char, ok := res.(models.Character)
if !ok {
return character.Character{}, errors.New("incorrect type")
return models.Character{}, errors.New("incorrect type")
}
return char, nil
}
// Characters gets characters by key
func (loader *Loader) Characters(key string, values ...string) ([]character.Character, error) {
func (loader *Loader) Characters(key string, values ...string) ([]models.Character, error) {
if !strings.HasPrefix(key, "Character.") {
key = "Character." + key
}
@ -55,10 +57,10 @@ func (loader *Loader) Characters(key string, values ...string) ([]character.Char
}
}
chars := make([]character.Character, len(res))
chars := make([]models.Character, len(res))
for i := range res {
char, ok := res[i].(character.Character)
char, ok := res[i].(models.Character)
if !ok {
return nil, errors.New("incorrect type")
}
@ -84,7 +86,7 @@ func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.R
results := make([]*dataloader.Result, 0, len(keys))
ids := keys.Keys()
characters, err := character.ListIDs(ids...)
characters, err := characters.List(&characters.Filter{IDs: ids})
if err != nil {
for range ids {
results = append(results, &dataloader.Result{Error: err})
@ -105,7 +107,7 @@ func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.R
}
if !found {
results = append(results, &dataloader.Result{Data: character.Character{}, Error: ErrNotFound})
results = append(results, &dataloader.Result{Data: models.Character{}, Error: ErrNotFound})
}
}
@ -137,7 +139,7 @@ func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader
}
if !found {
results = append(results, &dataloader.Result{Data: character.Character{}, Error: err})
results = append(results, &dataloader.Result{Data: models.Character{}, Error: err})
}
}

6
internal/loader/loader.go

@ -29,9 +29,9 @@ func New() *Loader {
return &Loader{
ctx: context.Background(),
loaders: map[string]*dataloader.Loader{
"Character.id": dataloader.NewBatchedLoader(characterIDBatch, dataloader.WithWait(time.Millisecond)),
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond)),
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond)),
"Character.id": dataloader.NewBatchedLoader(characterIDBatch, dataloader.WithWait(time.Millisecond*2)),
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond*2)),
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond*2)),
},
primedKeys: make(map[string]map[string]bool),
}

10
models/channel.go

@ -0,0 +1,10 @@
package models
// A Channel represents information abount an IRC RP channel, and whether it should be logged
type Channel struct {
Name string `bson:"_id"`
Logged bool `bson:"logged"`
Hub bool `bson:"hub"`
EventName string `bson:"event,omitempty"`
LocationName string `bson:"location,omitempty"`
}

33
models/channels/add.go

@ -0,0 +1,33 @@
package channels
import (
"errors"
"strings"
"git.aiterp.net/rpdata/api/models"
)
// ErrInvalidName is an error for an invalid channel name.
var ErrInvalidName = errors.New("Invalid channel name")
// Add creates a new channel.
func Add(name string, logged, hub bool, event, location string) (models.Channel, error) {
if len(name) < 3 && !strings.HasPrefix(name, "#") {
return models.Channel{}, ErrInvalidName
}
channel := models.Channel{
Name: name,
Logged: logged,
Hub: hub,
EventName: event,
LocationName: location,
}
err := collection.Insert(channel)
if err != nil {
return models.Channel{}, err
}
return channel, nil
}

19
models/channels/db.go

@ -0,0 +1,19 @@
package channels
import (
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
)
var collection *mgo.Collection
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.channels")
collection.EnsureIndexKey("logged")
collection.EnsureIndexKey("hub")
collection.EnsureIndexKey("event")
collection.EnsureIndexKey("location")
})
}

11
models/channels/find.go

@ -0,0 +1,11 @@
package channels
import "git.aiterp.net/rpdata/api/models"
// FindName finds a channel by its id (its name).
func FindName(name string) (models.Channel, error) {
channel := models.Channel{}
err := collection.FindId(name).One(&channel)
return channel, err
}

46
models/channels/list.go

@ -0,0 +1,46 @@
package channels
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Filter for searching
type Filter struct {
Logged *bool `json:"logged"`
EventName string `json:"eventName"`
LocationName string `json:"locationName"`
}
// List finds channels, if logged is true it will be limited to logged
// channels
func List(filter *Filter) ([]models.Channel, error) {
query := bson.M{}
if filter != nil {
if filter.Logged != nil {
query["logged"] = *filter.Logged
}
if filter.EventName != "" {
query["eventName"] = filter.EventName
}
if filter.LocationName != "" {
query["locationName"] = filter.LocationName
}
}
channels := make([]models.Channel, 0, 128)
err := collection.Find(query).All(&channels)
return channels, err
}
// ListNames finds channels by the names provided
func ListNames(names ...string) ([]models.Channel, error) {
query := bson.M{"_id": bson.M{"$in": names}}
channels := make([]models.Channel, 0, 32)
err := collection.Find(query).All(&channels)
return channels, err
}

20
models/character.go

@ -0,0 +1,20 @@
package models
// Character is a common data model representing an RP character or NPC.
type Character struct {
ID string `json:"id" bson:"_id"`
Nicks []string `json:"nicks" bson:"nicks"`
Name string `json:"name" bson:"name"`
ShortName string `json:"shortName" bson:"shortName"`
Author string `json:"author" bson:"author"`
Description string `json:"description" bson:"description"`
}
// Nick gets the character's nick.
func (character *Character) Nick() *string {
if len(character.Nicks[0]) == 0 {
return nil
}
return &character.Nicks[0]
}

48
models/characters/add.go

@ -0,0 +1,48 @@
package characters
import (
"errors"
"strconv"
"strings"
"git.aiterp.net/rpdata/api/model/counter"
"git.aiterp.net/rpdata/api/models"
)
// Add creates a Character and pushes it to the database. It does some validation
// on nick, name, shortName and author. It will generate a shortname from the first
// name if a blank one is provided.
func Add(nick, name, shortName, author, description string) (models.Character, error) {
if len(nick) < 1 || len(name) < 1 || len(author) < 1 {
return models.Character{}, errors.New("Nick, name, or author name too short or empty")
}
if shortName == "" {
shortName = strings.SplitN(name, " ", 2)[0]
}
char, err := FindNick(nick)
if err == nil && char.ID != "" {
return models.Character{}, errors.New("Nick is occupied")
}
nextID, err := counter.Next("auto_increment", "models.Character")
if err != nil {
return models.Character{}, err
}
character := models.Character{
ID: "C" + strconv.Itoa(nextID),
Nicks: []string{nick},
Name: name,
ShortName: shortName,
Author: author,
Description: description,
}
err = collection.Insert(character)
if err != nil {
return models.Character{}, err
}
return character, nil
}

55
models/characters/db.go

@ -0,0 +1,55 @@
package characters
import (
"log"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
)
var collection *mgo.Collection
func find(query interface{}) (models.Character, error) {
character := models.Character{}
err := collection.Find(query).One(&character)
if err != nil {
return models.Character{}, err
}
return character, nil
}
func list(query interface{}) ([]models.Character, error) {
characters := make([]models.Character, 0, 64)
err := collection.Find(query).All(&characters)
if err != nil {
return nil, err
}
return characters, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.characters")
collection.EnsureIndexKey("name")
collection.EnsureIndexKey("shortName")
collection.EnsureIndexKey("author")
err := collection.EnsureIndex(mgo.Index{
Key: []string{"nicks"},
Unique: true,
DropDups: true,
})
if err != nil {
log.Fatalln("init common.characters:", err)
}
err = collection.EnsureIndex(mgo.Index{
Key: []string{"$text:description"},
})
if err != nil {
log.Fatalln("init common.characters:", err)
}
})
}

16
models/characters/find.go

@ -0,0 +1,16 @@
package characters
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// FindID finds a character by id.
func FindID(id string) (models.Character, error) {
return find(bson.M{"_id": id})
}
// FindNick finds a character by nick
func FindNick(nick string) (models.Character, error) {
return find(bson.M{"nicks": nick})
}

61
models/characters/list.go

@ -0,0 +1,61 @@
package characters
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Filter is used to filter the list of characters
type Filter struct {
IDs []string `json:"ids"`
Nicks []string `json:"nicks"`
Names []string `json:"names"`
Author *string `json:"author"`
Search *string `json:"search"`
Logged *bool `json:"logged"`
}
// List lists all characters
func List(filter *Filter) ([]models.Character, error) {
query := bson.M{}
if filter != nil {
if len(filter.IDs) > 1 {
query["_id"] = bson.M{"$in": filter.IDs}
} else if len(filter.IDs) == 1 {
query["_id"] = filter.IDs[0]
}
if len(filter.Nicks) > 1 {
query["nicks"] = bson.M{"$in": filter.Nicks}
} else if len(filter.Nicks) == 1 {
query["nicks"] = filter.Nicks[0]
}
if len(filter.Names) > 1 {
query["$or"] = bson.M{
"name": bson.M{"$in": filter.Names},
"shortName": bson.M{"$in": filter.Names},
}
} else if len(filter.Names) == 1 {
query["$or"] = bson.M{
"name": filter.Names[0],
"shortName": filter.Names[0],
}
}
if filter.Logged != nil {
query["logged"] = *filter.Logged
}
if filter.Author != nil {
query["author"] = *filter.Author
}
if filter.Search != nil {
query["$text"] = bson.M{"$search": *filter.Search}
}
}
return list(query)
}

16
models/log.go

@ -0,0 +1,16 @@
package models
import "time"
// Log is the header/session for a log file.
type Log struct {
ID string `bson:"_id"`
ShortID string `bson:"shortId"`
Date time.Time `bson:"date"`
ChannelName string `bson:"channel"`
EventName string `bson:"event,omitempty"`
Title string `bson:"title,omitempty"`
Description string `bson:"description,omitempty"`
Open bool `bson:"open"`
CharacterIDs []string `bson:"characterIds"`
}

65
models/logs/db.go

@ -0,0 +1,65 @@
package logs
import (
"fmt"
"log"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
)
var collection *mgo.Collection
var postCollection *mgo.Collection
func find(query interface{}) (models.Log, error) {
log := models.Log{}
err := collection.Find(query).One(&log)
if err != nil {
return models.Log{}, err
}
return log, nil
}
func list(query interface{}, limit int) ([]models.Log, error) {
logs := make([]models.Log, 0, 64)
err := collection.Find(query).Limit(limit).Sort("-date").All(&logs)
if err != nil {
return nil, err
}
return logs, nil
}
func iter(query interface{}, limit int) *mgo.Iter {
return collection.Find(query).Sort("-date").Limit(limit).Batch(8).Iter()
}
func makeLogID(date time.Time, channel string) string {
return fmt.Sprintf("%s%03d_%s", date.UTC().Format("2006-01-02_150405"), (date.Nanosecond() / int(time.Millisecond/time.Nanosecond)), channel[1:])
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("logbot3.logs")
postCollection = db.C("logbo3.posts")
collection.EnsureIndexKey("date")
collection.EnsureIndexKey("channel")
collection.EnsureIndexKey("characterIds")
collection.EnsureIndexKey("event")
collection.EnsureIndex(mgo.Index{
Key: []string{"channel", "open"},
})
err := collection.EnsureIndex(mgo.Index{
Key: []string{"shortId"},
Unique: true,
DropDups: true,
})
if err != nil {
log.Fatalln("init logbot3.logs:", err)
}
})
}

16
models/logs/find.go

@ -0,0 +1,16 @@
package logs
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// FindID finds a log either by it's ID or short ID.
func FindID(id string) (models.Log, error) {
return find(bson.M{
"$or": []bson.M{
bson.M{"_id": id},
bson.M{"shortId": id},
},
})
}

70
models/logs/list.go

@ -0,0 +1,70 @@
package logs
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Filter for the List() function
type Filter struct {
Search *string
Characters *[]string
Channels *[]string
Events *[]string
Open *bool
Limit int
}
// List lists all logs
func List(filter *Filter) ([]models.Log, error) {
query := bson.M{}
limit := 0
if filter != nil {
// Run a text search
if filter.Search != nil {
searchResults, err := search(*filter.Search)
if err != nil {
return nil, err
}
// Posts always use shortId to refer to the log
query["shortId"] = bson.M{"$in": searchResults}
}
// Find logs including any of the specified events and channels
if filter.Channels != nil {
query["channel"] = bson.M{"$in": *filter.Channels}
}
if filter.Events != nil {
query["event"] = bson.M{"$in": *filter.Events}
}
// Find logs including all of the specified character IDs.
if filter.Characters != nil {
query["characterIds"] = bson.M{"$all": *filter.Characters}
}
// Limit to only open logs
if filter.Open != nil {
query["open"] = *filter.Open
}
// Set the limit from the filter
limit = filter.Limit
}
return list(query, limit)
}
func search(text string) ([]string, error) {
query := bson.M{
"$text": bson.M{"$search": text},
"logId": bson.M{"$ne": nil},
}
ids := make([]string, 0, 64)
err := postCollection.Find(query).Distinct("logId", ids)
return ids, err
}

14
models/post.go

@ -0,0 +1,14 @@
package models
import "time"
// A Post is a part of a log file.
type Post struct {
ID string `bson:"_id"`
LogID string `bson:"logId"`
Time time.Time `bson:"time"`
Kind string `bson:"kind"`
Nick string `bson:"nick"`
Text string `bson:"text"`
Position int `bson:"position"`
}

56
models/posts/db.go

@ -0,0 +1,56 @@
package posts
import (
"log"
"sync"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
)
var collection *mgo.Collection
var mutex sync.RWMutex
func find(query interface{}) (models.Post, error) {
post := models.Post{}
err := collection.Find(query).One(&post)
if err != nil {
return models.Post{}, err
}
return post, nil
}
func list(query interface{}, limit int, sort ...string) ([]models.Post, error) {
size := 64
if limit > 0 {
size = limit
}
posts := make([]models.Post, 0, size)
err := collection.Find(query).Limit(limit).Sort(sort...).All(&posts)
if err != nil {
return nil, err
}
return posts, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("logbot3.posts")
collection.EnsureIndexKey("logId")
collection.EnsureIndexKey("time")
collection.EnsureIndexKey("kind")
collection.EnsureIndexKey("position")
err := collection.EnsureIndex(mgo.Index{
Key: []string{"$text:text"},
})
if err != nil {
log.Fatalln("init logbot3.logs:", err)
}
})
}

14
models/posts/find.go

@ -0,0 +1,14 @@
package posts
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// FindID finds a log post by ID.
func FindID(id string) (models.Post, error) {
mutex.RLock()
defer mutex.RUnlock()
return find(bson.M{"_id": id})
}

57
models/posts/list.go

@ -0,0 +1,57 @@
package posts
import (
"strings"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Filter is used to generate a query to the database.
type Filter struct {
ID []string
Kind []string
LogID *string
Search *string
Limit int
}
// List lists the posts according to the filter
func List(filter *Filter) ([]models.Post, error) {
mutex.RLock()
defer mutex.RUnlock()
limit := 256
query := bson.M{}
if filter != nil {
if filter.LogID != nil {
query["logId"] = filter.LogID
}
if len(filter.ID) > 1 {
query["_id"] = bson.M{"$in": filter.ID}
} else if len(filter.ID) == 1 {
query["_id"] = filter.ID[0]
}
if len(filter.Kind) > 1 {
for i := range filter.Kind {
filter.Kind[i] = strings.ToLower(filter.Kind[i])
}
query["kind"] = bson.M{"$in": filter.Kind}
} else if len(filter.Kind) == 1 {
query["kind"] = strings.ToLower(filter.Kind[0])
}
limit = filter.Limit
}
posts, err := list(query, limit, "position")
if err != nil {
return nil, err
}
return posts, nil
}

1
models/posts/search.go

@ -0,0 +1 @@
package posts

0
model/scalars/date.go → models/scalars/date.go

47
models/tag-kind.go

@ -0,0 +1,47 @@
package models
import (
"fmt"
"io"
)
// TagKind represents the kind of tags.
type TagKind string
const (
// TagKindOrganization is a tag kind, see GraphQL documentation.
TagKindOrganization TagKind = "Organization"
// TagKindCharacter is a tag kind, see GraphQL documentation.
TagKindCharacter TagKind = "Character"
// TagKindLocation is a tag kind, see GraphQL documentation.
TagKindLocation TagKind = "Location"
// TagKindEvent is a tag kind, see GraphQL documentation.
TagKindEvent TagKind = "Event"
// TagKindSeries is a tag kind, see GraphQL documentation.
TagKindSeries TagKind = "Series"
)
// UnmarshalGQL unmarshals
func (e *TagKind) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = TagKind(str)
switch *e {
case TagKindOrganization, TagKindCharacter, TagKindLocation, TagKindEvent, TagKindSeries:
return nil
default:
return fmt.Errorf("%s is not a valid TagKind", str)
}
}
// MarshalGQL turns it into a JSON string
func (e TagKind) MarshalGQL(w io.Writer) {
fmt.Fprint(w, "\""+string(e), "\"")
}

7
models/tag.go

@ -0,0 +1,7 @@
package models
// A Tag associates a story with other content, like other stories, logs and more.
type Tag struct {
Kind TagKind `bson:"kind"`
Name string `bson:"name"`
}

14
models/tags/db.go

@ -0,0 +1,14 @@
package tags
import (
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
)
var storyCollection *mgo.Collection
func init() {
store.HandleInit(func(db *mgo.Database) {
storyCollection = db.C("story.stories")
})
}

26
models/tags/list.go

@ -0,0 +1,26 @@
package tags
import (
"sort"
"strings"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// List lists all tags
func List() ([]models.Tag, error) {
tags := make([]models.Tag, 0, 64)
err := storyCollection.Find(bson.M{"listed": true, "tags": bson.M{"$ne": nil}}).Distinct("tags", &tags)
sort.Slice(tags, func(i, j int) bool {
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, err
}
Loading…
Cancel
Save