From 0768cfd0bd627b38d2dbc378af7b95e0518b6b44 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 16 Sep 2018 00:13:12 +0200 Subject: [PATCH] models: Painstakingly converted half the model package to a models & repostiry stucture. All binaries are probably dead at this point. --- graph2/gqlgen.yml | 22 +++++----- graph2/graph.go | 6 +++ graph2/queries/channel.go | 11 ++--- graph2/queries/character.go | 15 +++---- graph2/queries/log.go | 28 +++++++++---- graph2/queries/post.go | 29 ++++++++++--- graph2/queries/tags.go | 7 ++-- graph2/schema/root.gql | 5 +-- graph2/schema/types/Log.gql | 2 +- graph2/schema/types/Post.gql | 8 ++++ graph2/types/log.go | 40 ++++++++++++++++++ internal/loader/channel.go | 17 ++++---- internal/loader/character.go | 24 ++++++----- internal/loader/loader.go | 6 +-- models/channel.go | 10 +++++ models/channels/add.go | 33 +++++++++++++++ models/channels/db.go | 19 +++++++++ models/channels/find.go | 11 +++++ models/channels/list.go | 46 ++++++++++++++++++++ models/character.go | 20 +++++++++ models/characters/add.go | 48 +++++++++++++++++++++ models/characters/db.go | 55 ++++++++++++++++++++++++ models/characters/find.go | 16 +++++++ models/characters/list.go | 61 +++++++++++++++++++++++++++ models/log.go | 16 +++++++ models/logs/db.go | 65 ++++++++++++++++++++++++++++ models/logs/find.go | 16 +++++++ models/logs/list.go | 70 +++++++++++++++++++++++++++++++ models/post.go | 14 +++++++ models/posts/db.go | 56 +++++++++++++++++++++++++ models/posts/find.go | 14 +++++++ models/posts/list.go | 57 +++++++++++++++++++++++++ models/posts/search.go | 1 + {model => models}/scalars/date.go | 0 models/tag-kind.go | 47 +++++++++++++++++++++ models/tag.go | 7 ++++ models/tags/db.go | 14 +++++++ models/tags/list.go | 26 ++++++++++++ 38 files changed, 878 insertions(+), 64 deletions(-) create mode 100644 graph2/types/log.go create mode 100644 models/channel.go create mode 100644 models/channels/add.go create mode 100644 models/channels/db.go create mode 100644 models/channels/find.go create mode 100644 models/channels/list.go create mode 100644 models/character.go create mode 100644 models/characters/add.go create mode 100644 models/characters/db.go create mode 100644 models/characters/find.go create mode 100644 models/characters/list.go create mode 100644 models/log.go create mode 100644 models/logs/db.go create mode 100644 models/logs/find.go create mode 100644 models/logs/list.go create mode 100644 models/post.go create mode 100644 models/posts/db.go create mode 100644 models/posts/find.go create mode 100644 models/posts/list.go create mode 100644 models/posts/search.go rename {model => models}/scalars/date.go (100%) create mode 100644 models/tag-kind.go create mode 100644 models/tag.go create mode 100644 models/tags/db.go create mode 100644 models/tags/list.go diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml index 70a22d0..8ec91a7 100644 --- a/graph2/gqlgen.yml +++ b/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 \ No newline at end of file + model: git.aiterp.net/rpdata/api/models/scalars.Date \ No newline at end of file diff --git a/graph2/graph.go b/graph2/graph.go index 9f8a4bb..6fd8b46 100644 --- a/graph2/graph.go +++ b/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 +} diff --git a/graph2/queries/channel.go b/graph2/queries/channel.go index 68b116a..77138a2 100644 --- a/graph2/queries/channel.go +++ b/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) } diff --git a/graph2/queries/character.go b/graph2/queries/character.go index 6ef64c1..668762d 100644 --- a/graph2/queries/character.go +++ b/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) } diff --git a/graph2/queries/log.go b/graph2/queries/log.go index 8eb3c14..9f0fa73 100644 --- a/graph2/queries/log.go +++ b/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...) + } } } diff --git a/graph2/queries/post.go b/graph2/queries/post.go index 5c40623..33fa904 100644 --- a/graph2/queries/post.go +++ b/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) } diff --git a/graph2/queries/tags.go b/graph2/queries/tags.go index a8d7fd7..2beffe9 100644 --- a/graph2/queries/tags.go +++ b/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() } diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql index 2f8d59b..4238f6f 100644 --- a/graph2/schema/root.gql +++ b/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 diff --git a/graph2/schema/types/Log.gql b/graph2/schema/types/Log.gql index cddffd7..290415e 100644 --- a/graph2/schema/types/Log.gql +++ b/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! diff --git a/graph2/schema/types/Post.gql b/graph2/schema/types/Post.gql index 4fd6f0a..bcc4908 100644 --- a/graph2/schema/types/Post.gql +++ b/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 } \ No newline at end of file diff --git a/graph2/types/log.go b/graph2/types/log.go new file mode 100644 index 0000000..51bce4b --- /dev/null +++ b/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 diff --git a/internal/loader/channel.go b/internal/loader/channel.go index 5677e04..e2a8e29 100644 --- a/internal/loader/channel.go +++ b/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}) } } diff --git a/internal/loader/character.go b/internal/loader/character.go index 85ae51d..1150163 100644 --- a/internal/loader/character.go +++ b/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}) } } diff --git a/internal/loader/loader.go b/internal/loader/loader.go index 61b7e1b..d243d84 100644 --- a/internal/loader/loader.go +++ b/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), } diff --git a/models/channel.go b/models/channel.go new file mode 100644 index 0000000..9f1ad17 --- /dev/null +++ b/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"` +} diff --git a/models/channels/add.go b/models/channels/add.go new file mode 100644 index 0000000..1103ce0 --- /dev/null +++ b/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 +} diff --git a/models/channels/db.go b/models/channels/db.go new file mode 100644 index 0000000..860c96e --- /dev/null +++ b/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") + }) +} diff --git a/models/channels/find.go b/models/channels/find.go new file mode 100644 index 0000000..50ef371 --- /dev/null +++ b/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 +} diff --git a/models/channels/list.go b/models/channels/list.go new file mode 100644 index 0000000..75345f8 --- /dev/null +++ b/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 +} diff --git a/models/character.go b/models/character.go new file mode 100644 index 0000000..13b1357 --- /dev/null +++ b/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] +} diff --git a/models/characters/add.go b/models/characters/add.go new file mode 100644 index 0000000..2abf75f --- /dev/null +++ b/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 +} diff --git a/models/characters/db.go b/models/characters/db.go new file mode 100644 index 0000000..c6c6722 --- /dev/null +++ b/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) + } + }) +} diff --git a/models/characters/find.go b/models/characters/find.go new file mode 100644 index 0000000..7e67214 --- /dev/null +++ b/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}) +} diff --git a/models/characters/list.go b/models/characters/list.go new file mode 100644 index 0000000..28a14bd --- /dev/null +++ b/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) +} diff --git a/models/log.go b/models/log.go new file mode 100644 index 0000000..5171ea8 --- /dev/null +++ b/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"` +} diff --git a/models/logs/db.go b/models/logs/db.go new file mode 100644 index 0000000..23b4845 --- /dev/null +++ b/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) + } + }) +} diff --git a/models/logs/find.go b/models/logs/find.go new file mode 100644 index 0000000..9a40f0f --- /dev/null +++ b/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}, + }, + }) +} diff --git a/models/logs/list.go b/models/logs/list.go new file mode 100644 index 0000000..3732a1c --- /dev/null +++ b/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 +} diff --git a/models/post.go b/models/post.go new file mode 100644 index 0000000..4769ca4 --- /dev/null +++ b/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"` +} diff --git a/models/posts/db.go b/models/posts/db.go new file mode 100644 index 0000000..b5153b6 --- /dev/null +++ b/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) + } + }) +} diff --git a/models/posts/find.go b/models/posts/find.go new file mode 100644 index 0000000..c7c8ed4 --- /dev/null +++ b/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}) +} diff --git a/models/posts/list.go b/models/posts/list.go new file mode 100644 index 0000000..b69fb87 --- /dev/null +++ b/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 +} diff --git a/models/posts/search.go b/models/posts/search.go new file mode 100644 index 0000000..c427a4f --- /dev/null +++ b/models/posts/search.go @@ -0,0 +1 @@ +package posts diff --git a/model/scalars/date.go b/models/scalars/date.go similarity index 100% rename from model/scalars/date.go rename to models/scalars/date.go diff --git a/models/tag-kind.go b/models/tag-kind.go new file mode 100644 index 0000000..4c692a9 --- /dev/null +++ b/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), "\"") +} diff --git a/models/tag.go b/models/tag.go new file mode 100644 index 0000000..8560cb8 --- /dev/null +++ b/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"` +} diff --git a/models/tags/db.go b/models/tags/db.go new file mode 100644 index 0000000..f29ea9c --- /dev/null +++ b/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") + }) +} diff --git a/models/tags/list.go b/models/tags/list.go new file mode 100644 index 0000000..b709101 --- /dev/null +++ b/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 +}