@@ -49,6 +49,16 @@ models:
|
|||||||
model: git.aiterp.net/rpdata/api/models.File
|
model: git.aiterp.net/rpdata/api/models.File
|
||||||
FilesFilter:
|
FilesFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/files.Filter
|
model: git.aiterp.net/rpdata/api/models/files.Filter
|
||||||
|
Change:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Change
|
||||||
|
ChangeModel:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChangeModel
|
||||||
|
ChangeKey:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
||||||
|
ChangeKeyInput: # It's the same as ChangeKey
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
||||||
|
ChangesFilter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models/changes.Filter
|
||||||
Token:
|
Token:
|
||||||
model: git.aiterp.net/rpdata/api/models.Token
|
model: git.aiterp.net/rpdata/api/models.Token
|
||||||
User:
|
User:
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ func (r *rootResolver) File() FileResolver {
|
|||||||
return &types.FileResolver
|
return &types.FileResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Change() ChangeResolver {
|
||||||
|
return &types.ChangeResolver
|
||||||
|
}
|
||||||
|
|
||||||
func (r *rootResolver) Token() TokenResolver {
|
func (r *rootResolver) Token() TokenResolver {
|
||||||
return &types.TokenResolver
|
return &types.TokenResolver
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/models/changes"
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Queries
|
||||||
|
|
||||||
|
func (r *resolver) Changes(ctx context.Context, filter *changes.Filter) ([]models.Change, error) {
|
||||||
|
return changes.List(filter)
|
||||||
|
}
|
||||||
@@ -54,6 +54,10 @@ type Query {
|
|||||||
files(filter: FilesFilter): [File!]!
|
files(filter: FilesFilter): [File!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find changes
|
||||||
|
changes(filter: ChangesFilter): [Change!]!
|
||||||
|
|
||||||
|
|
||||||
# Get information about the token, useful for debugging.
|
# Get information about the token, useful for debugging.
|
||||||
token: Token!
|
token: Token!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""
|
||||||
|
A change represents a a change in the history.
|
||||||
|
"""
|
||||||
|
type Change {
|
||||||
|
"A unique ID of the change."
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
"The model of the changed object."
|
||||||
|
model: ChangeModel!
|
||||||
|
|
||||||
|
"The operation performed."
|
||||||
|
op: String!
|
||||||
|
|
||||||
|
"The author that performed the change."
|
||||||
|
author: String!
|
||||||
|
|
||||||
|
"Whether the change will be listed without a matching key."
|
||||||
|
listed: Boolean!
|
||||||
|
|
||||||
|
"The keys indexing this change."
|
||||||
|
keys: [ChangeKey!]!
|
||||||
|
|
||||||
|
"The date when the change happened."
|
||||||
|
date: Date
|
||||||
|
|
||||||
|
"The changed objects."
|
||||||
|
objects: [ChangeObject!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter
|
||||||
|
|
||||||
|
"""
|
||||||
|
A change key is a key associated with a change, used for filtering
|
||||||
|
and subscription. A log post edit has keys for both the log and post,
|
||||||
|
making it suitable to use it to patch an active log file.
|
||||||
|
"""
|
||||||
|
type ChangeKey {
|
||||||
|
"The model the key is for."
|
||||||
|
model: ChangeModel!
|
||||||
|
|
||||||
|
"The model's id, or * if it's a key indicating a model list should be updated."
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
See ChangeKey
|
||||||
|
"""
|
||||||
|
input ChangeKeyInput {
|
||||||
|
"The model the key is for."
|
||||||
|
model: ChangeModel!
|
||||||
|
|
||||||
|
"The model's id, or * if it's a key indicating a model list should be updated."
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
A model related to the change.
|
||||||
|
"""
|
||||||
|
enum ChangeModel {
|
||||||
|
Character
|
||||||
|
Channel
|
||||||
|
Log
|
||||||
|
Post
|
||||||
|
Story
|
||||||
|
Tag
|
||||||
|
Chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional filter for the changes query.
|
||||||
|
"""
|
||||||
|
input ChangesFilter {
|
||||||
|
"The keys to query for."
|
||||||
|
keys: [ChangeKeyInput!]
|
||||||
|
|
||||||
|
"Only show changes more recent than this date."
|
||||||
|
earliestDate: Date
|
||||||
|
|
||||||
|
"Limit the amount of results."
|
||||||
|
limit: Int
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/input"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type changeResolver struct{}
|
||||||
|
|
||||||
|
func (r *changeResolver) Objects(ctx context.Context, obj *models.Change) ([]input.ChangeObject, error) {
|
||||||
|
objects := obj.Objects()
|
||||||
|
|
||||||
|
results := make([]input.ChangeObject, len(objects))
|
||||||
|
for i := range objects {
|
||||||
|
results[i] = objects[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeResolver is a resolver
|
||||||
|
var ChangeResolver changeResolver
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChangeModel describes a model related to the change.
|
||||||
|
type ChangeModel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ChangeModelCharacter is a value of ChangeModel
|
||||||
|
ChangeModelCharacter ChangeModel = "Character"
|
||||||
|
// ChangeModelChannel is a value of ChangeModel
|
||||||
|
ChangeModelChannel ChangeModel = "Channel"
|
||||||
|
// ChangeModelLog is a value of ChangeModel
|
||||||
|
ChangeModelLog ChangeModel = "Log"
|
||||||
|
// ChangeModelPost is a value of ChangeModel
|
||||||
|
ChangeModelPost ChangeModel = "Post"
|
||||||
|
// ChangeModelStory is a value of ChangeModel
|
||||||
|
ChangeModelStory ChangeModel = "Story"
|
||||||
|
// ChangeModelTag is a value of ChangeModel
|
||||||
|
ChangeModelTag ChangeModel = "Tag"
|
||||||
|
// ChangeModelChapter is a value of ChangeModel
|
||||||
|
ChangeModelChapter ChangeModel = "Chapter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid returns true if the underlying string is one of the correct values.
|
||||||
|
func (e ChangeModel) IsValid() bool {
|
||||||
|
switch e {
|
||||||
|
case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ChangeModel) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalGQL unmarshals the underlying graphql value.
|
||||||
|
func (e *ChangeModel) UnmarshalGQL(v interface{}) error {
|
||||||
|
str, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enums must be strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = ChangeModel(str)
|
||||||
|
if !e.IsValid() {
|
||||||
|
return fmt.Errorf("%s is not a valid ChangeModel", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalGQL marshals the underlying graphql value.
|
||||||
|
func (e ChangeModel) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
|
}
|
||||||
+5
-5
@@ -5,7 +5,7 @@ import "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 {
|
||||||
ID string `bson:"_id"`
|
ID string `bson:"_id"`
|
||||||
Model string `bson:"model"`
|
Model ChangeModel `bson:"model"`
|
||||||
Op string `bson:"op"`
|
Op string `bson:"op"`
|
||||||
Author string `bson:"author"`
|
Author string `bson:"author"`
|
||||||
Listed bool `bson:"listed"`
|
Listed bool `bson:"listed"`
|
||||||
@@ -23,12 +23,12 @@ type Change struct {
|
|||||||
|
|
||||||
// ChangeKey is a key for a change that can be used when subscribing to them.
|
// ChangeKey is a key for a change that can be used when subscribing to them.
|
||||||
type ChangeKey struct {
|
type ChangeKey struct {
|
||||||
Model string `bson:"model"`
|
Model ChangeModel `bson:"model"`
|
||||||
ID string `bson:"id"`
|
ID string `bson:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data 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) Data() []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))
|
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))
|
||||||
|
|
||||||
for _, log := range change.Logs {
|
for _, log := range change.Logs {
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import "git.aiterp.net/rpdata/api/models"
|
|||||||
|
|
||||||
// All makes a changelog that's (model, "*"). It's a helper to standardize it.
|
// All makes a changelog that's (model, "*"). It's a helper to standardize it.
|
||||||
func All(model string) models.ChangeKey {
|
func All(model string) models.ChangeKey {
|
||||||
return models.ChangeKey{Model: model, ID: "*"}
|
return models.ChangeKey{Model: models.ChangeModel(model), ID: "*"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ func Listed(objects ...interface{}) []models.ChangeKey {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(keys, All(keys[0].Model))
|
return append(keys, All(keys[0].Model.String()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,5 +34,5 @@ func One(object interface{}) models.ChangeKey {
|
|||||||
panic("Unsupported model")
|
panic("Unsupported model")
|
||||||
}
|
}
|
||||||
|
|
||||||
return models.ChangeKey{Model: model, ID: id}
|
return models.ChangeKey{Model: models.ChangeModel(model), ID: id}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,21 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
"git.aiterp.net/rpdata/api/internal/store"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"github.com/globalsign/mgo"
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
var collection *mgo.Collection
|
var collection *mgo.Collection
|
||||||
var submitMutex sync.Mutex
|
var submitMutex sync.Mutex
|
||||||
|
|
||||||
|
func list(query bson.M, limit int) ([]models.Change, error) {
|
||||||
|
changes := make([]models.Change, 0, 64)
|
||||||
|
err := collection.Find(query).Limit(limit).Sort("date").All(&changes)
|
||||||
|
|
||||||
|
return changes, err
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package changes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter is a filter for changes.List.
|
||||||
|
type Filter struct {
|
||||||
|
Keys []models.ChangeKey
|
||||||
|
EarliestDate *time.Time
|
||||||
|
Limit *int
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists changes.
|
||||||
|
func List(filter *Filter) ([]models.Change, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
limit := 0
|
||||||
|
|
||||||
|
if filter != nil {
|
||||||
|
if filter.Limit != nil {
|
||||||
|
limit = *filter.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Keys != nil {
|
||||||
|
query["keys"] = bson.M{"$in": filter.Keys}
|
||||||
|
} else {
|
||||||
|
query["listed"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.EarliestDate != nil {
|
||||||
|
query["date"] = bson.M{"$gt": *filter.EarliestDate}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query["listed"] = true
|
||||||
|
limit = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
return list(query, limit)
|
||||||
|
}
|
||||||
@@ -18,9 +18,31 @@ func Submit(model, op, author string, listed bool, keys []models.ChangeKey, obje
|
|||||||
return models.Change{}, err
|
return models.Change{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !models.ChangeModel(model).IsValid() {
|
||||||
|
panic("Invalid model")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Silently discard * keys on unlisted changes.
|
||||||
|
if !listed {
|
||||||
|
keysCopy := make([]models.ChangeKey, len(keys))
|
||||||
|
copy(keysCopy, keys)
|
||||||
|
keys = keysCopy
|
||||||
|
|
||||||
|
deletes := make([]int, 0, 1)
|
||||||
|
for i, key := range keys {
|
||||||
|
if key.ID == "*" {
|
||||||
|
deletes = append(deletes, i-len(deletes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, index := range deletes {
|
||||||
|
keys = append(keys[:index], keys[index+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
change := models.Change{
|
change := models.Change{
|
||||||
ID: "Change_" + strconv.Itoa(id),
|
ID: "Change_" + strconv.Itoa(id),
|
||||||
Model: model,
|
Model: models.ChangeModel(model),
|
||||||
Date: time.Now(),
|
Date: time.Now(),
|
||||||
Op: op,
|
Op: op,
|
||||||
Author: author,
|
Author: author,
|
||||||
|
|||||||
Reference in New Issue
Block a user