connecting labels when retrieving articles

This commit is contained in:
Roland Osborne 2022-02-04 13:26:52 -08:00
parent 7a001a4075
commit 8e903dc1c1
11 changed files with 370 additions and 49 deletions

View File

@ -2439,15 +2439,7 @@ paths:
content:
application/json:
schema:
type: object
required:
- type
- data
properties:
type:
type: string
data:
type: string
$ref: '#/components/schemas/Subject'
/content/labels/{labelId}:
put:
@ -2477,15 +2469,7 @@ paths:
content:
application/json:
schema:
type: object
required:
- type
- data
properties:
type:
type: string
data:
type: string
$ref: '#/components/schemas/Subject'
delete:
tags:
- content
@ -4405,17 +4389,24 @@ components:
type: object
required:
- labelId
- labelRevision
- type
- data
- created
- modified
- revision
- labelData
properties:
labelId:
type: string
labelRevision:
type: integer
format: int64
revision:
type: string
labelData:
$ref: '#/components/schemas/LabelData'
LabelData:
type: object
required:
- type
- data
- created
- updated
properties:
type:
type: string
data:
@ -4423,6 +4414,9 @@ components:
created:
type: integer
format: int64
updated:
type: integer
format: int64
groups: # present only in account holder responses
type: array
items:

View File

@ -0,0 +1,71 @@
package databag
import (
"net/http"
"errors"
"gorm.io/gorm"
"github.com/google/uuid"
"databag/internal/store"
)
func AddLabel(w http.ResponseWriter, r *http.Request) {
account, code, err := BearerAppToken(r, false)
if err != nil {
ErrResponse(w, code, err)
return
}
var subject Subject
if err := ParseRequest(r, w, &subject); err != nil {
ErrResponse(w, http.StatusBadRequest, err)
return
}
slot := &store.LabelSlot{}
label := &store.Label{}
err = store.DB.Transaction(func(tx *gorm.DB) error {
data := &store.LabelData{
Data: subject.Data,
}
if res := tx.Save(data).Error; res != nil {
return res
}
label.LabelDataID = data.ID
label.LabelData = *data
label.DataType = subject.DataType
if res := tx.Save(label).Error; res != nil {
return res
}
if res := tx.Where("account_id = ? AND label_id = 0", account.ID).First(slot).Error; res != nil {
if errors.Is(res, gorm.ErrRecordNotFound) {
slot.LabelSlotId = uuid.New().String()
slot.AccountID = account.ID
} else {
return res
}
}
slot.LabelID = label.ID
slot.Revision = account.LabelRevision + 1
slot.Label = label
if res := tx.Save(slot).Error; res != nil {
return res
}
if res := tx.Model(&account).Update("label_revision", account.LabelRevision + 1).Error; res != nil {
return res
}
return nil
})
if err != nil {
ErrResponse(w, http.StatusInternalServerError, err)
return
}
SetStatus(account)
WriteResponse(w, getLabelModel(slot, true, true))
}

View File

@ -23,11 +23,6 @@ func AddArticleTag(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func AddLabel(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
}
func ClearArticleGroup(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
@ -123,21 +118,11 @@ func SetArticleGroup(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func SetArticleLabel(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
}
func SetArticleSubject(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
}
func SetLabelGroup(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
}
func UpdateLabel(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)

View File

@ -113,11 +113,11 @@ func isShared(slot *store.ArticleSlot, guid string) bool {
}
func getAccountArticles(account *store.Account, revision int64, articles *[]store.ArticleSlot) error {
return store.DB.Preload("Article.Groups.GroupSlot").Preload("Article.Labels.Groups.GroupSlot").Where("account_id = ? AND revision > ?", account.ID, revision).Find(articles).Error
return store.DB.Preload("Article.Labels.LabelSlot").Preload("Article.Groups.GroupSlot").Preload("Article.Labels.Groups.GroupSlot").Where("account_id = ? AND revision > ?", account.ID, revision).Find(articles).Error
}
func getContactArticles(card *store.Card, revision int64, articles *[]store.ArticleSlot) error {
return store.DB.Preload("Article.Groups.Cards").Preload("Article.Labels.Groups.Cards").Where("account_id = ? AND revision > ?", card.Account.ID, revision).Find(articles).Error
return store.DB.Preload("Article.Labels.LabelSlot").Preload("Article.Groups.Cards").Preload("Article.Labels.Groups.Cards").Where("account_id = ? AND revision > ?", card.Account.ID, revision).Find(articles).Error
}

View File

@ -27,6 +27,12 @@ func RemoveArticle(w http.ResponseWriter, r *http.Request) {
if slot.Article == nil {
return nil
}
if res := tx.Model(slot.Article).Association("Groups").Clear(); res != nil {
return res
}
if res := tx.Model(slot.Article).Association("Labels").Clear(); res != nil {
return res
}
if res := tx.Delete(slot.Article).Error; res != nil {
return res
}

View File

@ -0,0 +1,76 @@
package databag
import (
"errors"
"net/http"
"gorm.io/gorm"
"github.com/gorilla/mux"
"databag/internal/store"
)
func SetArticleLabel(w http.ResponseWriter, r *http.Request) {
account, code, err := BearerAppToken(r, false);
if err != nil {
ErrResponse(w, code, err)
return
}
// scan parameters
params := mux.Vars(r)
articleId := params["articleId"]
labelId := params["labelId"]
labelSlot := &store.LabelSlot{}
if err := store.DB.Preload("Label.LabelSlot").Where("account_id = ? AND label_slot_id = ?", account.ID, labelId).First(&labelSlot).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
ErrResponse(w, http.StatusInternalServerError, err)
} else {
ErrResponse(w, http.StatusNotFound, err)
}
return
}
if labelSlot.Label == nil {
ErrResponse(w, http.StatusNotFound, errors.New("referenced empty label slot"))
return
}
articleSlot := &store.ArticleSlot{}
if err := store.DB.Preload("Article.Labels.LabelSlot").Preload("Article.Groups.GroupSlot").Where("account_id = ? AND article_slot_id = ?", account.ID, articleId).First(&articleSlot).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
ErrResponse(w, http.StatusInternalServerError, err)
} else {
ErrResponse(w, http.StatusNotFound, err)
}
return
}
if articleSlot.Article == nil {
ErrResponse(w, http.StatusNotFound, errors.New("referenced empty article slot"))
return
}
err = store.DB.Transaction(func(tx *gorm.DB) error {
if res := tx.Model(articleSlot.Article).Association("Labels").Append(labelSlot.Label); res != nil {
return res
}
if res := tx.Model(articleSlot).Update("revision", account.ContentRevision + 1).Error; res != nil {
return res
}
if res := tx.Model(account).Update("content_revision", account.ContentRevision + 1).Error; res != nil {
return res
}
return nil
})
if err != nil {
ErrResponse(w, http.StatusInternalServerError, err)
return
}
SetContentNotification(account)
SetStatus(account)
WriteResponse(w, getArticleModel(articleSlot, false, true))
}

View File

@ -0,0 +1,82 @@
package databag
import (
"errors"
"net/http"
"gorm.io/gorm"
"github.com/gorilla/mux"
"databag/internal/store"
)
func SetLabelGroup(w http.ResponseWriter, r *http.Request) {
account, code, err := BearerAppToken(r, false);
if err != nil {
ErrResponse(w, code, err)
return
}
// scan parameters
params := mux.Vars(r)
groupId := params["groupId"]
labelId := params["labelId"]
labelSlot := &store.LabelSlot{}
if err := store.DB.Preload("Label").Where("account_id = ? AND label_slot_id = ?", account.ID, labelId).First(&labelSlot).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
ErrResponse(w, http.StatusInternalServerError, err)
} else {
ErrResponse(w, http.StatusNotFound, err)
}
return
}
if labelSlot.Label == nil {
ErrResponse(w, http.StatusNotFound, errors.New("referenced empty label slot"))
return
}
groupSlot := &store.GroupSlot{}
if err := store.DB.Preload("Group.GroupSlot").Where("account_id = ? AND group_slot_id = ?", account.ID, groupId).First(&groupSlot).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
ErrResponse(w, http.StatusInternalServerError, err)
} else {
ErrResponse(w, http.StatusNotFound, err)
}
return
}
if groupSlot.Group == nil {
ErrResponse(w, http.StatusNotFound, errors.New("referenced empty group slot"))
return
}
err = store.DB.Transaction(func(tx *gorm.DB) error {
if res := tx.Model(labelSlot.Label).Association("Groups").Append(groupSlot.Group); res != nil {
return res
}
if res := tx.Model(labelSlot).Update("revision", account.LabelRevision + 1).Error; res != nil {
return res
}
if res := tx.Model(account).Update("label_revision", account.LabelRevision + 1).Error; res != nil {
return res
}
if res := tx.Model(account).Update("view_revision", account.ViewRevision + 1).Error; res != nil {
return res
}
return nil
})
if err != nil {
ErrResponse(w, http.StatusInternalServerError, err)
return
}
SetViewNotification(account)
SetLabelNotification(account)
SetStatus(account)
WriteResponse(w, getLabelModel(labelSlot, true, true))
}

View File

@ -9,6 +9,7 @@ func getCardModel(slot *store.CardSlot) *Card {
if slot.Card == nil {
return &Card{
CardId: slot.CardSlotId,
Revision: slot.Revision,
}
}
@ -61,6 +62,7 @@ func getGroupModel(slot *store.GroupSlot) *Group {
if slot.Group == nil {
return &Group{
GroupId: slot.GroupSlotId,
Revision: slot.Revision,
}
}
@ -76,6 +78,36 @@ func getGroupModel(slot *store.GroupSlot) *Group {
}
}
func getLabelModel(slot *store.LabelSlot, includeData bool, includeGroups bool) *Label {
if !includeData || slot.Label == nil {
return &Label{
LabelId: slot.LabelSlotId,
Revision: slot.Revision,
}
}
var groups *[]string
if includeGroups {
groups = &[]string{}
for _, group := range slot.Label.Groups {
*groups = append(*groups, group.GroupSlot.GroupSlotId)
}
}
return &Label{
LabelId: slot.LabelSlotId,
Revision: slot.Revision,
LabelData: &LabelData{
DataType: slot.Label.DataType,
Data: slot.Label.LabelData.Data,
Created: slot.Label.Created,
Updated: slot.Label.Updated,
Groups: groups,
},
}
}
func getArticleModel(slot *store.ArticleSlot, contact bool, shared bool) *Article {
if !shared || slot.Article == nil {
@ -85,14 +117,14 @@ func getArticleModel(slot *store.ArticleSlot, contact bool, shared bool) *Articl
}
}
var groups []string;
var groups []string
if !contact {
for _, group := range slot.Article.Groups {
groups = append(groups, group.GroupSlot.GroupSlotId)
}
}
var labels []string;
var labels []string
for _, label := range slot.Article.Labels {
labels = append(labels, label.LabelSlot.LabelSlotId)
}

View File

@ -174,7 +174,7 @@ type LabelData struct {
Data string `json:"data"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
Groups []string `json:"groups,omitempty"`
Groups *[]string `json:"groups,omitempty"`
}
type NodeConfig struct {

View File

@ -230,6 +230,7 @@ type Article struct {
TagRevision int64 `gorm:"not null"`
Groups []Group `gorm:"many2many:article_groups;"`
Labels []Label `gorm:"many2many:article_labels;"`
ArticleSlot ArticleSlot
}
type ArticleAsset struct {

View File

@ -11,10 +11,16 @@ func TestAddArticle(t *testing.T) {
var err error
var rev *Revision
var ver *Revision
var article Article
var article *Article
var articles *[]Article
var articleAccess *ArticleAccess
var cards []Card
var label *Label
var subject *Subject
var vars *map[string]string
var contentRevision int64
var viewRevision int64
var labelRevision int64
// setup testing group
set, err = AddTestGroup("addarticle")
@ -25,9 +31,11 @@ func TestAddArticle(t *testing.T) {
// create article
articleAccess = &ArticleAccess{ Groups: []string{set.A.B.GroupId} }
assert.NoError(t, SendEndpointTest(AddArticle, "POST", "/content/articles", nil, articleAccess, APP_TOKENAPP, set.A.Token, &article))
article = &Article{}
assert.NoError(t, SendEndpointTest(AddArticle, "POST", "/content/articles", nil, articleAccess, APP_TOKENAPP, set.A.Token, article))
assert.NoError(t, SendEndpointTest(AddArticle, "POST", "/content/articles", nil, articleAccess, APP_TOKENAPP, set.A.Token, &article))
article = &Article{}
assert.NoError(t, SendEndpointTest(AddArticle, "POST", "/content/articles", nil, articleAccess, APP_TOKENAPP, set.A.Token, article))
assert.NoError(t, SendEndpointTest(RemoveArticle, "DELETE", "/content/articls/" + article.ArticleId, &map[string]string{"articleId": article.ArticleId }, nil, APP_TOKENAPP, set.A.Token, nil))
@ -67,4 +75,70 @@ func TestAddArticle(t *testing.T) {
assert.NoError(t, SendEndpointTest(GetArticles, "GET", "/content/articles?contentRevision=0&viewRevision=" + strconv.FormatInt(cards[0].CardData.NotifiedView, 10), nil, nil, APP_TOKENCONTACT, set.B.A.Token, articles))
assert.Equal(t, 2, len(*articles))
ver = GetTestRevision(set.C.Revisions)
// add another article
article = &Article{}
articleAccess = &ArticleAccess{}
assert.NoError(t, SendEndpointTest(AddArticle, "POST", "/content/articles", nil, articleAccess, APP_TOKENAPP, set.A.Token, article))
// capture updated card on new article
rev = GetTestRevision(set.C.Revisions)
assert.NoError(t, SendEndpointTest(GetCards, "GET", "/contact/cards?cardRevision=" + strconv.FormatInt(ver.Card, 10), nil, nil, APP_TOKENAPP, set.C.Token, &cards))
assert.Equal(t, 1, len(cards))
viewRevision = cards[0].CardData.NotifiedView
contentRevision = cards[0].CardData.NotifiedContent
labelRevision = cards[0].CardData.NotifiedLabel
ver = rev
// create new label
label = &Label{}
subject = &Subject{ DataType: "labeltype", Data: "labeldata" }
assert.NoError(t, SendEndpointTest(AddLabel, "POST", "/content/labels", nil, subject, APP_TOKENAPP, set.A.Token, label))
vars = &map[string]string{
"labelId": label.LabelId,
"groupId": set.A.C.GroupId,
}
label = &Label{}
assert.NoError(t, SendEndpointTest(SetLabelGroup, "POST", "/content/labels/{labelId}/groups/{groupId}", vars, nil, APP_TOKENAPP, set.A.Token, label))
// capture updated card on new assigned label
rev = GetTestRevision(set.C.Revisions)
assert.NoError(t, SendEndpointTest(GetCards, "GET", "/contact/cards?cardRevision=" + strconv.FormatInt(ver.Card, 10), nil, nil, APP_TOKENAPP, set.C.Token, &cards))
assert.Equal(t, 1, len(cards))
assert.NotEqual(t, viewRevision, cards[0].CardData.NotifiedView)
assert.NotEqual(t, labelRevision, cards[0].CardData.NotifiedLabel)
viewRevision = cards[0].CardData.NotifiedView
contentRevision = cards[0].CardData.NotifiedContent
labelRevision = cards[0].CardData.NotifiedLabel
ver = rev
// assign label to article
vars = &map[string]string{
"labelId": label.LabelId,
"articleId": article.ArticleId,
}
article = &Article{}
assert.NoError(t, SendEndpointTest(SetArticleLabel, "POST", "/content/articles/{articleId}/labels/{labelId}", vars, nil, APP_TOKENAPP, set.A.Token, article))
// capture updated card on assigned article
rev = GetTestRevision(set.C.Revisions)
assert.NoError(t, SendEndpointTest(GetCards, "GET", "/contact/cards?cardRevision=" + strconv.FormatInt(ver.Card, 10), nil, nil, APP_TOKENAPP, set.C.Token, &cards))
assert.Equal(t, 1, len(cards))
assert.NotEqual(t, contentRevision, cards[0].CardData.NotifiedContent)
ver = rev
// confirm c can see new article
articles = &[]Article{}
assert.NoError(t, SendEndpointTest(GetArticles, "GET", "/content/articles", nil, nil, APP_TOKENCONTACT, set.C.A.Token, articles))
assert.Equal(t, 1, len(*articles))
assert.Equal(t, (*articles)[0].ArticleId, article.ArticleId)
assert.Equal(t, 1, len((*articles)[0].ArticleData.Labels))
assert.Equal(t, (*articles)[0].ArticleData.Labels[0], label.LabelId)
// confirm b cannot see new article
articles = &[]Article{}
assert.NoError(t, SendEndpointTest(GetArticles, "GET", "/content/articles", nil, nil, APP_TOKENCONTACT, set.B.A.Token, articles))
assert.Equal(t, 1, len(*articles))
assert.NotEqual(t, article.ArticleId, (*articles)[0].ArticleId)
}