diff --git a/net/server/internal/api_addChannelTopicTag.go b/net/server/internal/api_addChannelTopicTag.go new file mode 100644 index 00000000..82246179 --- /dev/null +++ b/net/server/internal/api_addChannelTopicTag.go @@ -0,0 +1,113 @@ +package databag + +import ( + "time" + "errors" + "net/http" + "github.com/gorilla/mux" + "gorm.io/gorm" + "github.com/google/uuid" + "databag/internal/store" +) + +func AddChannelTopicTag(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + topicId := params["topicId"] + + var subject Subject + if err := ParseRequest(r, w, &subject); err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + + channelSlot, guid, err, code := getChannelSlot(r, false) + if err != nil { + ErrResponse(w, code, err) + return + } + act := &channelSlot.Account + + // load topic + var topicSlot store.TopicSlot + if err = store.DB.Preload("Topic.Tags").Where("channel_id = ? AND topic_slot_id = ?", channelSlot.Channel.ID, topicId).First(&topicSlot).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + ErrResponse(w, http.StatusNotFound, err) + } else { + ErrResponse(w, http.StatusInternalServerError, err) + } + return + } + if topicSlot.Topic == nil { + ErrResponse(w, http.StatusNotFound, errors.New("referenced empty topic")) + return + } + + // save tag + tagSlot := &store.TagSlot{} + err = store.DB.Transaction(func(tx *gorm.DB) error { + + tagSlot.TagSlotId = uuid.New().String() + tagSlot.AccountID = act.ID + tagSlot.Revision = act.ChannelRevision + 1 + if res := tx.Save(tagSlot).Error; res != nil { + return res + } + + tag := &store.Tag{} + tag.ChannelID = channelSlot.Channel.ID + tag.TopicID = topicSlot.Topic.ID + tag.TagSlotID = tagSlot.ID + tag.Guid = guid + tag.DataType = subject.DataType + tag.Data = subject.Data + if res := tx.Save(tag).Error; res != nil { + return res + } + + if res := tx.Model(&topicSlot.Topic).Update("tag_count", len(topicSlot.Topic.Tags) + 1).Error; res != nil { + return res + } + if res := tx.Model(&topicSlot.Topic).Update("tag_updated", time.Now().Unix()).Error; res != nil { + return res + } + if res := tx.Model(&topicSlot.Topic).Update("tag_revision", act.ChannelRevision + 1).Error; res != nil { + return res + } + if res := tx.Model(&topicSlot).Update("revision", act.ChannelRevision + 1).Error; res != nil { + return res + } + if res := tx.Model(&channelSlot).Update("revision", act.ChannelRevision + 1).Error; res != nil { + return res + } + if res := tx.Model(act).Update("channel_revision", act.ChannelRevision + 1).Error; res != nil { + return res + } + return nil + }) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + // determine affected contact list + cards := make(map[string]store.Card) + for _, card := range channelSlot.Channel.Cards { + cards[card.Guid] = card + } + for _, group := range channelSlot.Channel.Groups { + for _, card := range group.Cards { + cards[card.Guid] = card + } + } + + // notify + SetStatus(act) + for _, card := range cards { + SetContactChannelNotification(act, &card) + } + + WriteResponse(w, getTagModel(tagSlot)) +} + diff --git a/net/server/internal/api_content.go b/net/server/internal/api_content.go index e21e97a0..66b4f5e7 100644 --- a/net/server/internal/api_content.go +++ b/net/server/internal/api_content.go @@ -13,16 +13,6 @@ import ( "net/http" ) -func AddChannelTopicTag(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} - -func GetChannelTopicTagSubjectField(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} - func GetChannelTopicTags(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) diff --git a/net/server/internal/api_getChannelTopicTagSubjectField.go b/net/server/internal/api_getChannelTopicTagSubjectField.go new file mode 100644 index 00000000..b43ce7f3 --- /dev/null +++ b/net/server/internal/api_getChannelTopicTagSubjectField.go @@ -0,0 +1,60 @@ +package databag + +import ( + "strings" + "time" + "bytes" + "errors" + "net/http" + "gorm.io/gorm" + "github.com/gorilla/mux" + "databag/internal/store" + "encoding/base64" + "github.com/valyala/fastjson" +) + +func GetChannelTopicTagSubjectField(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + topicId := params["topicId"] + tagId := params["tagId"] + field := params["field"] + elements := strings.Split(field, ".") + + channelSlot, _, err, code := getChannelSlot(r, false) + if err != nil { + ErrResponse(w, code, err) + return + } + + // load tag + var tagSlot store.TagSlot + if err = store.DB.Preload("Tag.Topic.TopicSlot").Where("channel_id = ? AND tag_slot_id = ?", channelSlot.Channel.ID, tagId).First(&tagSlot).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + code = http.StatusNotFound + } else { + code = http.StatusInternalServerError + } + return + } + if tagSlot.Tag == nil { + ErrResponse(w, http.StatusNotFound, errors.New("referenced missing tag")) + return + } + if tagSlot.Tag.Topic.TopicSlot.TopicSlotId != topicId { + ErrResponse(w, http.StatusNotFound, errors.New("invalid topic tag")) + return + } + + // decode data + strData := fastjson.GetString([]byte(tagSlot.Tag.Data), elements...) + binData, err := base64.StdEncoding.DecodeString(strData) + if err != nil { + ErrResponse(w, http.StatusNotFound, err) + return + } + + // response with content + http.ServeContent(w, r, field, time.Unix(tagSlot.Tag.Updated, 0), bytes.NewReader(binData)) +} diff --git a/net/server/internal/modelUtil.go b/net/server/internal/modelUtil.go index e1ccee1c..72f9f472 100644 --- a/net/server/internal/modelUtil.go +++ b/net/server/internal/modelUtil.go @@ -291,4 +291,25 @@ func getTopicModel(slot *store.TopicSlot) *Topic { } } +func getTagModel(slot *store.TagSlot) *Tag { + + if slot.Tag == nil { + return &Tag{ + Id: slot.TagSlotId, + Revision: slot.Revision, + } + } + + return &Tag{ + Id: slot.TagSlotId, + Revision: slot.Revision, + Data: &TagData{ + Guid: slot.Tag.Guid, + DataType: slot.Tag.DataType, + Data: slot.Tag.Data, + Created: slot.Tag.Created, + Updated: slot.Tag.Updated, + }, + } +} diff --git a/net/server/internal/store/schema.go b/net/server/internal/store/schema.go index abad913b..33a7f23d 100644 --- a/net/server/internal/store/schema.go +++ b/net/server/internal/store/schema.go @@ -227,7 +227,7 @@ type Topic struct { Created int64 `gorm:"autoCreateTime"` Updated int64 `gorm:"autoUpdateTime"` TagCount int32 `gorm:"not null"` - TagUpdated int64 `gorm:"autoUpdateTime"` + TagUpdated int64 TagRevision int64 `gorm:"not null"` Assets []Asset Tags []Tag @@ -256,23 +256,26 @@ type Asset struct { type TagSlot struct { ID uint - TagSlotId string `gorm:"not null;index:topicslot,unique"` - AccountID uint `gorm:"not null;index:topicslot,unique"` + TagSlotId string `gorm:"not null;index:tagslot,unique"` + AccountID uint `gorm:"not null;index:tagslot,unique"` Revision int64 `gorm:"not null"` - TagID uint `gorm:"not null;default:0"` Tag *Tag Account Account } type Tag struct { ID uint `gorm:"primaryKey;not null;unique;autoIncrement"` - TopicID uint `gorm:"not null;index:tag,unique"` - Guid string + TagSlotID uint `gorm:"not null;index:tagtagslot,unique"` + ChannelID uint `gorm:"not null;index:channeltag"` + TopicID uint `gorm:"not null;index:topictag"` + Guid string `gorm:"not null"` DataType string `gorm:"index"` Data string Created int64 `gorm:"autoCreateTime"` Updated int64 `gorm:"autoUpdateTime"` + Channel *Channel Topic *Topic + TagSlot TagSlot } diff --git a/net/server/internal/ucTopicShare_test.go b/net/server/internal/ucTopicShare_test.go index 6f6a284f..97713e89 100644 --- a/net/server/internal/ucTopicShare_test.go +++ b/net/server/internal/ucTopicShare_test.go @@ -101,6 +101,12 @@ func TestTopicShare(t *testing.T) { assert.NoError(t, ApiTestMsg(GetChannelTopics, "GET", "/content/channels/{channelId}/topics", ¶ms, nil, APP_TOKENAPP, set.A.Token, topics, nil)) + // add a tag to topic + tag := Tag{} + subject = &Subject{ DataType: "tagdatatype", Data: "subjectfromA" } + assert.NoError(t, ApiTestMsg(AddChannelTopicTag, "POST", "/content/channels/{channelId}/topcis/{topicId}", + ¶ms, subject, APP_TOKENAPP, set.A.Token, tag, nil)) + // get list of assets assets = []Asset{} assert.NoError(t, ApiTestMsg(GetChannelTopicAssets, "GET", "/content/channels/{channelId}/topics/{topicId}",