Generated
+5
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="GoNilness" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"git.aiterp.net/rpdata/api/internal/loader"
|
"git.aiterp.net/rpdata/api/internal/loader"
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
"git.aiterp.net/rpdata/api/internal/store"
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
"git.aiterp.net/rpdata/api/models/logs"
|
||||||
"git.aiterp.net/rpdata/api/services"
|
"git.aiterp.net/rpdata/api/services"
|
||||||
"github.com/99designs/gqlgen/handler"
|
"github.com/99designs/gqlgen/handler"
|
||||||
@@ -52,13 +51,13 @@ func main() {
|
|||||||
log.Println("Characters updated")
|
log.Println("Characters updated")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go logListedChanges()
|
go logListedChanges(services.Changes)
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":8081", nil))
|
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func logListedChanges() {
|
func logListedChanges(changes *services.ChangeService) {
|
||||||
sub := changes.Subscribe(context.Background(), nil, true)
|
sub := changes.Subscribe(context.Background(), models.ChangeFilter{PassAll: true})
|
||||||
|
|
||||||
for change := range sub {
|
for change := range sub {
|
||||||
log.Printf("Change: Author=%#+v Model=%#+v Op=%#+v", change.Author, change.Model, change.Op)
|
log.Printf("Change: Author=%#+v Model=%#+v Op=%#+v", change.Author, change.Model, change.Op)
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type changeRepository struct {
|
||||||
|
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 len(filter.Keys) > 0 {
|
||||||
|
query["keys"] = bson.M{"$in": filter.Keys}
|
||||||
|
}
|
||||||
|
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).All(&changes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
|
||||||
|
next, err := r.idCounter.Increment(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
change.ID = "Change_" + strconv.Itoa(next)
|
||||||
|
|
||||||
|
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) (repositories.ChangeRepository, error) {
|
||||||
|
collection := db.C("common.changes")
|
||||||
|
|
||||||
|
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{
|
||||||
|
changes: collection,
|
||||||
|
idCounter: newCounter(db, "auto_increment", "Change"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -38,8 +38,15 @@ func Init(cfg config.Database) (bundle *repositories.Bundle, closeFn func() erro
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changes, err := newChangeRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
bundle = &repositories.Bundle{
|
bundle = &repositories.Bundle{
|
||||||
Characters: characters,
|
Characters: characters,
|
||||||
|
Changes: changes,
|
||||||
Tags: newTagRepository(db),
|
Tags: newTagRepository(db),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +80,22 @@ func (c *counter) WithName(name string) *counter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) Increment(amount int) (int, error) {
|
func (c *counter) Increment(amount int) (int, error) {
|
||||||
type counterDoc struct {
|
type counterDoc struct {
|
||||||
ID string `bson:"_id"`
|
ID string `bson:"_id"`
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package graph2
|
|||||||
import (
|
import (
|
||||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
"git.aiterp.net/rpdata/api/models/channels"
|
"git.aiterp.net/rpdata/api/models/channels"
|
||||||
"git.aiterp.net/rpdata/api/models/files"
|
"git.aiterp.net/rpdata/api/models/files"
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
"git.aiterp.net/rpdata/api/models/logs"
|
||||||
@@ -71,7 +70,7 @@ func complexity() (cr graphcore.ComplexityRoot) {
|
|||||||
cr.Query.Files = func(childComplexity int, filter *files.Filter) int {
|
cr.Query.Files = func(childComplexity int, filter *files.Filter) int {
|
||||||
return childComplexity + listComplexity
|
return childComplexity + listComplexity
|
||||||
}
|
}
|
||||||
cr.Query.Changes = func(childComplexity int, filter *changes.Filter) int {
|
cr.Query.Changes = func(childComplexity int, filter *models.ChangeFilter) int {
|
||||||
return childComplexity + listComplexity
|
return childComplexity + listComplexity
|
||||||
}
|
}
|
||||||
cr.Query.Token = func(childComplexity int) int {
|
cr.Query.Token = func(childComplexity int) int {
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ models:
|
|||||||
ChangeKeyInput: # It's the same as ChangeKey
|
ChangeKeyInput: # It's the same as ChangeKey
|
||||||
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
||||||
ChangesFilter:
|
ChangesFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/changes.Filter
|
model: git.aiterp.net/rpdata/api/models.ChangeFilter
|
||||||
Token:
|
Token:
|
||||||
model: git.aiterp.net/rpdata/api/models.Token
|
model: git.aiterp.net/rpdata/api/models.Token
|
||||||
User:
|
User:
|
||||||
|
|||||||
@@ -2,39 +2,25 @@ package resolvers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Queries
|
/// Queries
|
||||||
|
|
||||||
func (r *queryResolver) Changes(ctx context.Context, filter *changes.Filter) ([]*models.Change, error) {
|
func (r *queryResolver) Changes(ctx context.Context, filter *models.ChangeFilter) ([]*models.Change, error) {
|
||||||
changes, err := changes.List(filter)
|
if filter == nil {
|
||||||
if err != nil {
|
filter = &models.ChangeFilter{}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changes2 := make([]*models.Change, len(changes))
|
return r.s.Changes.List(ctx, *filter)
|
||||||
for i := range changes {
|
|
||||||
changes2[i] = &changes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes2, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscriptions
|
/// Subscriptions
|
||||||
|
|
||||||
func (r *subscriptionResolver) Changes(ctx context.Context, keys []*models.ChangeKey) (<-chan *models.Change, error) {
|
func (r *subscriptionResolver) Changes(ctx context.Context, filter *models.ChangeFilter) (<-chan *models.Change, error) {
|
||||||
if len(keys) == 0 {
|
if filter == nil {
|
||||||
return nil, errors.New("At least one key is required for a subscription")
|
filter = &models.ChangeFilter{}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys2 := make([]models.ChangeKey, len(keys))
|
return r.s.Changes.Subscribe(ctx, *filter), nil
|
||||||
for i := range keys {
|
|
||||||
keys2[i] = *keys[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes.Subscribe(ctx, keys2, false), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ type Subscription {
|
|||||||
"""
|
"""
|
||||||
Changes subscribes to the changes matching the following keys.
|
Changes subscribes to the changes matching the following keys.
|
||||||
"""
|
"""
|
||||||
changes(keys: [ChangeKeyInput!]!): Change!
|
changes(filter: ChangesFilter): Change!
|
||||||
}
|
}
|
||||||
|
|
||||||
# A Time represents a RFC3339 encoded date with up to millisecond precision.
|
# A Time represents a RFC3339 encoded date with up to millisecond precision.
|
||||||
|
|||||||
@@ -73,9 +73,12 @@ input ChangesFilter {
|
|||||||
"The keys to query for."
|
"The keys to query for."
|
||||||
keys: [ChangeKeyInput!]
|
keys: [ChangeKeyInput!]
|
||||||
|
|
||||||
|
"Only show changes by this author"
|
||||||
|
author: String
|
||||||
|
|
||||||
"Only show changes more recent than this date."
|
"Only show changes more recent than this date."
|
||||||
earliestDate: Time
|
earliestDate: Time
|
||||||
|
|
||||||
"Limit the amount of results."
|
"Limit the amount of results. This even goes for subscriptions!"
|
||||||
limit: Int
|
limit: Int
|
||||||
}
|
}
|
||||||
+18
-14
@@ -114,20 +114,24 @@ func CheckToken(tokenString string) (token models.Token, err error) {
|
|||||||
return models.Token{}, ErrDeletedUser
|
return models.Token{}, ErrDeletedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptedPermissions := make([]string, 0, 8)
|
acceptedPermissions := make([]string, 0, len(user.Permissions))
|
||||||
for _, permission := range permissions {
|
if len(permissions) > 0 {
|
||||||
found := false
|
for _, permission := range permissions {
|
||||||
|
found := false
|
||||||
|
|
||||||
for _, userPermission := range user.Permissions {
|
for _, userPermission := range user.Permissions {
|
||||||
if permission == userPermission {
|
if permission == userPermission {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
acceptedPermissions = append(acceptedPermissions, permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if found {
|
acceptedPermissions = append(acceptedPermissions, user.Permissions...)
|
||||||
acceptedPermissions = append(acceptedPermissions, permission)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
|
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
|
||||||
@@ -153,10 +157,10 @@ func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err
|
|||||||
permissions = append(permissions, permission)
|
permissions = append(permissions, permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(permissions) == 0 {
|
if len(permissions) == 0 {
|
||||||
return "", nil, ErrInvalidClaims
|
return "", nil, ErrInvalidClaims
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Notifier is a synchronization primitive for waking upp all listeners at once.
|
||||||
|
type Notifier struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
ch chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast wakes all listeners if there are any.
|
||||||
|
func (notifier *Notifier) Broadcast() {
|
||||||
|
notifier.mutex.Lock()
|
||||||
|
if notifier.ch != nil {
|
||||||
|
close(notifier.ch)
|
||||||
|
notifier.ch = nil
|
||||||
|
}
|
||||||
|
notifier.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// C gets the channel that'll close on the next notification.
|
||||||
|
func (notifier *Notifier) C() <-chan struct{} {
|
||||||
|
notifier.mutex.Lock()
|
||||||
|
if notifier.ch == nil {
|
||||||
|
notifier.ch = make(chan struct{})
|
||||||
|
}
|
||||||
|
ch := notifier.ch
|
||||||
|
notifier.mutex.Unlock()
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the next `Broadcast` call, or the context's termination.
|
||||||
|
func (notifier *Notifier) Wait(ctx context.Context) error {
|
||||||
|
select {
|
||||||
|
case <-notifier.C():
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
+122
-6
@@ -1,6 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Change represents a change in the rpdata history through the API.
|
// Change represents a change in the rpdata history through the API.
|
||||||
type Change struct {
|
type Change struct {
|
||||||
@@ -22,15 +25,55 @@ type Change struct {
|
|||||||
Comments []Comment `bson:"comments"`
|
Comments []Comment `bson:"comments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeKey is a key for a change that can be used when subscribing to them.
|
// AddObject adds the model into the appropriate array.
|
||||||
type ChangeKey struct {
|
func (change *Change) AddObject(object interface{}) bool {
|
||||||
Model ChangeModel `bson:"model"`
|
if v := reflect.ValueOf(object); v.Kind() == reflect.Ptr {
|
||||||
ID string `bson:"id"`
|
return change.AddObject(v.Elem().Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
switch object := object.(type) {
|
||||||
|
case Log:
|
||||||
|
change.Logs = append(change.Logs, object)
|
||||||
|
case []Log:
|
||||||
|
change.Logs = append(change.Logs, object...)
|
||||||
|
case Character:
|
||||||
|
change.Characters = append(change.Characters, object)
|
||||||
|
case []Character:
|
||||||
|
change.Characters = append(change.Characters, object...)
|
||||||
|
case Channel:
|
||||||
|
change.Channels = append(change.Channels, object)
|
||||||
|
case []Channel:
|
||||||
|
change.Channels = append(change.Channels, object...)
|
||||||
|
case Post:
|
||||||
|
change.Posts = append(change.Posts, object)
|
||||||
|
case []Post:
|
||||||
|
change.Posts = append(change.Posts, object...)
|
||||||
|
case Story:
|
||||||
|
change.Stories = append(change.Stories, object)
|
||||||
|
case []Story:
|
||||||
|
change.Stories = append(change.Stories, object...)
|
||||||
|
case Tag:
|
||||||
|
change.Tags = append(change.Tags, object)
|
||||||
|
case []Tag:
|
||||||
|
change.Tags = append(change.Tags, object...)
|
||||||
|
case Chapter:
|
||||||
|
change.Chapters = append(change.Chapters, object)
|
||||||
|
case []Chapter:
|
||||||
|
change.Chapters = append(change.Chapters, object...)
|
||||||
|
case Comment:
|
||||||
|
change.Comments = append(change.Comments, object)
|
||||||
|
case []Comment:
|
||||||
|
change.Comments = append(change.Comments, object...)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Objects makes a combined, mixed array of all the models stored in this change.
|
// Objects makes a combined, mixed array of all the models stored in this change.
|
||||||
func (change *Change) Objects() []interface{} {
|
func (change *Change) Objects() []interface{} {
|
||||||
data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Channels)+len(change.Posts)+len(change.Stories)+len(change.Tags)+len(change.Chapters)+len(change.Comments))
|
data := make([]interface{}, 0, 8)
|
||||||
|
|
||||||
for _, log := range change.Logs {
|
for _, log := range change.Logs {
|
||||||
data = append(data, &log)
|
data = append(data, &log)
|
||||||
@@ -59,3 +102,76 @@ func (change *Change) Objects() []interface{} {
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (change *Change) PassesFilter(filter ChangeFilter) bool {
|
||||||
|
if filter.PassAll {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Author != nil && change.Author != *filter.Author {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For unlisted changes, pass it only if the filter refers to the specific index.
|
||||||
|
if !change.Listed {
|
||||||
|
hasSpecificKey := false
|
||||||
|
|
||||||
|
KeyFindLoop:
|
||||||
|
for _, key := range filter.Keys {
|
||||||
|
if key.ID == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, changeKey := range change.Keys {
|
||||||
|
if changeKey.Model == key.Model && changeKey.ID == key.ID {
|
||||||
|
hasSpecificKey = true
|
||||||
|
break KeyFindLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasSpecificKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.EarliestDate != nil && filter.EarliestDate.Before(change.Date) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Keys) > 0 {
|
||||||
|
foundKey := false
|
||||||
|
|
||||||
|
KeyFindLoop2:
|
||||||
|
for _, key := range filter.Keys {
|
||||||
|
for _, changeKey := range change.Keys {
|
||||||
|
if changeKey == key {
|
||||||
|
foundKey = true
|
||||||
|
break KeyFindLoop2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeKey is a key for a change that can be used when subscribing to them.
|
||||||
|
type ChangeKey struct {
|
||||||
|
Model ChangeModel `bson:"model"`
|
||||||
|
ID string `bson:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeFilter is a filter for listing changes.
|
||||||
|
type ChangeFilter struct {
|
||||||
|
Keys []ChangeKey
|
||||||
|
EarliestDate *time.Time
|
||||||
|
Author *string
|
||||||
|
Limit *int
|
||||||
|
|
||||||
|
PassAll bool // DO NOT EXPOSE
|
||||||
|
}
|
||||||
|
|||||||
+1
-17
@@ -1,14 +1,11 @@
|
|||||||
package changes
|
package changes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
"git.aiterp.net/rpdata/api/internal/store"
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"github.com/globalsign/mgo"
|
"github.com/globalsign/mgo"
|
||||||
"github.com/globalsign/mgo/bson"
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var collection *mgo.Collection
|
var collection *mgo.Collection
|
||||||
@@ -24,18 +21,5 @@ func list(query bson.M, limit int) ([]models.Change, error) {
|
|||||||
func init() {
|
func init() {
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
store.HandleInit(func(db *mgo.Database) {
|
||||||
collection = db.C("common.changes")
|
collection = db.C("common.changes")
|
||||||
|
|
||||||
collection.EnsureIndexKey("date")
|
|
||||||
collection.EnsureIndexKey("author")
|
|
||||||
collection.EnsureIndexKey("keys")
|
|
||||||
|
|
||||||
err := collection.EnsureIndex(mgo.Index{
|
|
||||||
Name: "expiry",
|
|
||||||
Key: []string{"date"},
|
|
||||||
ExpireAfter: time.Hour * 2400, // 100 days
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*models.Change, error)
|
||||||
|
List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error)
|
||||||
|
Insert(ctx context.Context, change models.Change) (*models.Change, error)
|
||||||
|
Remove(ctx context.Context, change models.Change) error
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import "errors"
|
|||||||
// A Bundle is a set of repositories.
|
// A Bundle is a set of repositories.
|
||||||
type Bundle struct {
|
type Bundle struct {
|
||||||
Characters CharacterRepository
|
Characters CharacterRepository
|
||||||
|
Changes ChangeRepository
|
||||||
Tags TagRepository
|
Tags TagRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/auth"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/notifier"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeService struct {
|
||||||
|
changes repositories.ChangeRepository
|
||||||
|
|
||||||
|
mutex sync.RWMutex
|
||||||
|
buffer []models.Change
|
||||||
|
offset uint64
|
||||||
|
notifier notifier.Notifier
|
||||||
|
submitQueue chan *models.Change
|
||||||
|
loopStarted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChangeService) Find(ctx context.Context, id string) (*models.Change, error) {
|
||||||
|
return s.changes.Find(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChangeService) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) {
|
||||||
|
return s.changes.List(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChangeService) Submit(ctx context.Context, model models.ChangeModel, op string, listed bool, keys []models.ChangeKey, objects ...interface{}) {
|
||||||
|
token := auth.TokenFromContext(ctx)
|
||||||
|
if token == nil {
|
||||||
|
panic("no token!")
|
||||||
|
}
|
||||||
|
|
||||||
|
change := &models.Change{
|
||||||
|
Model: model,
|
||||||
|
Op: op,
|
||||||
|
Author: token.UserID,
|
||||||
|
Listed: listed,
|
||||||
|
Keys: keys,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range objects {
|
||||||
|
if !change.AddObject(obj) {
|
||||||
|
log.Printf("Cannot add object of type %T to change", obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
if !s.loopStarted {
|
||||||
|
s.loopStarted = true
|
||||||
|
s.submitQueue = make(chan *models.Change, 64)
|
||||||
|
go s.loop()
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.submitQueue <- change
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChangeService) Subscribe(ctx context.Context, filter models.ChangeFilter) <-chan *models.Change {
|
||||||
|
channel := make(chan *models.Change)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(channel)
|
||||||
|
|
||||||
|
s.mutex.RLock()
|
||||||
|
pos := s.offset + uint64(len(s.buffer))
|
||||||
|
slice := make([]models.Change, 32)
|
||||||
|
s.mutex.RUnlock()
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
s.mutex.RLock()
|
||||||
|
nextPos := s.offset + uint64(len(s.buffer))
|
||||||
|
length := nextPos - pos
|
||||||
|
if length > 0 {
|
||||||
|
index := pos - s.offset
|
||||||
|
|
||||||
|
pos = nextPos
|
||||||
|
copy(slice, s.buffer[index:])
|
||||||
|
}
|
||||||
|
ch := s.notifier.C()
|
||||||
|
s.mutex.RUnlock()
|
||||||
|
|
||||||
|
for _, change := range slice[:length] {
|
||||||
|
if change.PassesFilter(filter) {
|
||||||
|
channel <- &change
|
||||||
|
|
||||||
|
count++
|
||||||
|
if filter.Limit != nil && count == *filter.Limit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChangeService) loop() {
|
||||||
|
for change := range s.submitQueue {
|
||||||
|
timeout, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||||
|
|
||||||
|
change, err := s.changes.Insert(timeout, *change)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to submit change:")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.buffer = append(s.buffer, *change)
|
||||||
|
if len(s.buffer) > 16 {
|
||||||
|
copy(s.buffer, s.buffer[8:])
|
||||||
|
s.buffer = s.buffer[:len(s.buffer)-8]
|
||||||
|
s.offset += 8
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
s.notifier.Broadcast()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-17
@@ -6,7 +6,6 @@ import (
|
|||||||
"git.aiterp.net/rpdata/api/internal/auth"
|
"git.aiterp.net/rpdata/api/internal/auth"
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
"git.aiterp.net/rpdata/api/repositories"
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
"git.aiterp.net/rpdata/api/services/loaders"
|
"git.aiterp.net/rpdata/api/services/loaders"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -14,8 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CharacterService struct {
|
type CharacterService struct {
|
||||||
characters repositories.CharacterRepository
|
characters repositories.CharacterRepository
|
||||||
loader *loaders.CharacterLoader
|
loader *loaders.CharacterLoader
|
||||||
|
changeService *ChangeService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find uses the loader to find the character by the ID.
|
// Find uses the loader to find the character by the ID.
|
||||||
@@ -101,8 +101,7 @@ func (s *CharacterService) Create(ctx context.Context, nick, name, shortName, au
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: New change submit system
|
s.changeService.Submit(ctx, "Character", "add", true, changekeys.Listed(character), character)
|
||||||
go changes.Submit("Character", "add", token.UserID, true, changekeys.Listed(character), character)
|
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
@@ -130,9 +129,7 @@ func (s *CharacterService) Update(ctx context.Context, id string, name, shortNam
|
|||||||
s.loader.Clear(character.ID)
|
s.loader.Clear(character.ID)
|
||||||
s.loader.Prime(character.ID, character)
|
s.loader.Prime(character.ID, character)
|
||||||
|
|
||||||
//TODO: New change submit system
|
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
||||||
token := auth.TokenFromContext(ctx)
|
|
||||||
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
|
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
@@ -153,9 +150,7 @@ func (s *CharacterService) AddNick(ctx context.Context, id string, nick string)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: New change submit system
|
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
||||||
token := auth.TokenFromContext(ctx)
|
|
||||||
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
|
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
@@ -176,9 +171,7 @@ func (s *CharacterService) RemoveNick(ctx context.Context, id string, nick strin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: New change submit system
|
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
||||||
token := auth.TokenFromContext(ctx)
|
|
||||||
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
|
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
@@ -199,9 +192,7 @@ func (s *CharacterService) Delete(ctx context.Context, id string) (*models.Chara
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: New change submit system
|
s.changeService.Submit(ctx, "Character", "remove", true, changekeys.Listed(character), character)
|
||||||
token := auth.TokenFromContext(ctx)
|
|
||||||
go changes.Submit("Character", "remove", token.UserID, true, changekeys.Listed(character), character)
|
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,21 @@ import (
|
|||||||
type Bundle struct {
|
type Bundle struct {
|
||||||
Tags *TagService
|
Tags *TagService
|
||||||
Characters *CharacterService
|
Characters *CharacterService
|
||||||
|
Changes *ChangeService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBundle creates a new bundle.
|
// NewBundle creates a new bundle.
|
||||||
func NewBundle(repos *repositories.Bundle) *Bundle {
|
func NewBundle(repos *repositories.Bundle) *Bundle {
|
||||||
bundle := &Bundle{}
|
bundle := &Bundle{}
|
||||||
|
|
||||||
|
bundle.Changes = &ChangeService{
|
||||||
|
changes: repos.Changes,
|
||||||
|
}
|
||||||
bundle.Tags = &TagService{tags: repos.Tags}
|
bundle.Tags = &TagService{tags: repos.Tags}
|
||||||
bundle.Characters = &CharacterService{
|
bundle.Characters = &CharacterService{
|
||||||
characters: repos.Characters,
|
characters: repos.Characters,
|
||||||
loader: loaders.CharacterLoaderFromRepository(repos.Characters),
|
loader: loaders.CharacterLoaderFromRepository(repos.Characters),
|
||||||
|
changeService: bundle.Changes,
|
||||||
}
|
}
|
||||||
|
|
||||||
return bundle
|
return bundle
|
||||||
|
|||||||
Reference in New Issue
Block a user