Added a SQL-Preprocessor - this way we can unmarshal recursive structures (LEFT JOIN etc)
This commit is contained in:
parent
bbf7962e29
commit
dbc014f819
@ -5,6 +5,8 @@ PORT=9090
|
|||||||
NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD)
|
NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD)
|
||||||
HASH=$(shell git rev-parse HEAD)
|
HASH=$(shell git rev-parse HEAD)
|
||||||
|
|
||||||
|
.PHONY: test swagger
|
||||||
|
|
||||||
build: swagger fmt
|
build: swagger fmt
|
||||||
mkdir -p _build
|
mkdir -p _build
|
||||||
rm -f ./_build/scn_backend
|
rm -f ./_build/scn_backend
|
||||||
@ -29,7 +31,6 @@ docker: build
|
|||||||
-t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \
|
-t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \
|
||||||
.
|
.
|
||||||
|
|
||||||
.PHONY: swagger
|
|
||||||
swagger:
|
swagger:
|
||||||
which swag || go install github.com/swaggo/swag/cmd/swag@latest
|
which swag || go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
swag init -generalInfo api/router.go --propertyStrategy snakecase --output ./swagger/ --outputTypes "json,yaml"
|
swag init -generalInfo api/router.go --propertyStrategy snakecase --output ./swagger/ --outputTypes "json,yaml"
|
||||||
@ -67,7 +68,6 @@ fmt:
|
|||||||
go fmt ./...
|
go fmt ./...
|
||||||
swag fmt
|
swag fmt
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test:
|
test:
|
||||||
go test ./test/...
|
go test ./test/...
|
||||||
|
|
||||||
|
@ -23,8 +23,6 @@
|
|||||||
|
|
||||||
-------------------------------------------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
- (?) return subscribtions in list-channels (?)
|
|
||||||
|
|
||||||
- (?) ack/read deliveries && return ack-count (? or not, how to query?)
|
- (?) ack/read deliveries && return ack-count (? or not, how to query?)
|
||||||
|
|
||||||
- (?) "login" on website and list/search/filter messages
|
- (?) "login" on website and list/search/filter messages
|
||||||
|
@ -490,13 +490,14 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
// ListChannels swaggerdoc
|
// ListChannels swaggerdoc
|
||||||
//
|
//
|
||||||
// @Summary List channels of a user (subscribed/owned)
|
// @Summary List channels of a user (subscribed/owned/all)
|
||||||
// @Description The possible values for 'selector' are:
|
// @Description The possible values for 'selector' are:
|
||||||
// @Description - "owned" Return all channels of the user
|
// @Description - "owned" Return all channels of the user
|
||||||
// @Description - "subscribed" Return all channels that the user is subscribing to
|
// @Description - "subscribed" Return all channels that the user is subscribing to
|
||||||
// @Description - "all" Return channels that the user owns or is subscribing
|
// @Description - "all" Return channels that the user owns or is subscribing
|
||||||
// @Description - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed)
|
// @Description - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed)
|
||||||
// @Description - "all_any" Return channels that the user owns or is subscribing (even unconfirmed)
|
// @Description - "all_any" Return channels that the user owns or is subscribing (even unconfirmed)
|
||||||
|
//
|
||||||
// @ID api-channels-list
|
// @ID api-channels-list
|
||||||
// @Tags API-v2
|
// @Tags API-v2
|
||||||
//
|
//
|
||||||
@ -517,7 +518,7 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
Selector *string `json:"selector" form:"selector" enums:"owned,subscribed_any,all_any,subscribed,all"`
|
Selector *string `json:"selector" form:"selector" enums:"owned,subscribed_any,all_any,subscribed,all"`
|
||||||
}
|
}
|
||||||
type response struct {
|
type response struct {
|
||||||
Channels []models.ChannelJSON `json:"channels"`
|
Channels []models.ChannelWithSubscriptionJSON `json:"channels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var u uri
|
var u uri
|
||||||
@ -534,40 +535,52 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
sel := strings.ToLower(langext.Coalesce(q.Selector, "owned"))
|
sel := strings.ToLower(langext.Coalesce(q.Selector, "owned"))
|
||||||
|
|
||||||
var res []models.ChannelJSON
|
var res []models.ChannelWithSubscriptionJSON
|
||||||
|
|
||||||
if sel == "owned" {
|
if sel == "owned" {
|
||||||
channels, err := h.database.ListChannelsByOwner(ctx, u.UserID)
|
|
||||||
|
channels, err := h.database.ListChannelsByOwner(ctx, u.UserID, u.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
||||||
}
|
}
|
||||||
res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(true) })
|
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(true) })
|
||||||
|
|
||||||
} else if sel == "subscribed_any" {
|
} else if sel == "subscribed_any" {
|
||||||
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, false)
|
|
||||||
|
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
||||||
}
|
}
|
||||||
res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) })
|
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
|
||||||
|
|
||||||
} else if sel == "all_any" {
|
} else if sel == "all_any" {
|
||||||
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, false)
|
|
||||||
|
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
||||||
}
|
}
|
||||||
res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) })
|
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
|
||||||
|
|
||||||
} else if sel == "subscribed" {
|
} else if sel == "subscribed" {
|
||||||
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, true)
|
|
||||||
|
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, langext.Ptr(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
||||||
}
|
}
|
||||||
res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) })
|
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
|
||||||
|
|
||||||
} else if sel == "all" {
|
} else if sel == "all" {
|
||||||
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, true)
|
|
||||||
|
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, langext.Ptr(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
||||||
}
|
}
|
||||||
res = langext.ArrMap(channels, func(v models.Channel) models.ChannelJSON { return v.JSON(false) })
|
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
|
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Channels: res}))
|
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Channels: res}))
|
||||||
@ -575,14 +588,14 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
// GetChannel swaggerdoc
|
// GetChannel swaggerdoc
|
||||||
//
|
//
|
||||||
// @Summary List all channels of a user
|
// @Summary Get a single channel
|
||||||
// @ID api-channels-get
|
// @ID api-channels-get
|
||||||
// @Tags API-v2
|
// @Tags API-v2
|
||||||
//
|
//
|
||||||
// @Param uid path int true "UserID"
|
// @Param uid path int true "UserID"
|
||||||
// @Param cid path int true "ChannelID"
|
// @Param cid path int true "ChannelID"
|
||||||
//
|
//
|
||||||
// @Success 200 {object} models.ChannelJSON
|
// @Success 200 {object} models.ChannelWithSubscriptionJSON
|
||||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||||
// @Failure 404 {object} ginresp.apiError "channel not found"
|
// @Failure 404 {object} ginresp.apiError "channel not found"
|
||||||
@ -626,19 +639,20 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Param uid path int true "UserID"
|
// @Param uid path int true "UserID"
|
||||||
// @Param post_body body handler.CreateChannel.body false " "
|
// @Param post_body body handler.CreateChannel.body false " "
|
||||||
//
|
//
|
||||||
// @Success 200 {object} models.ChannelJSON
|
// @Success 200 {object} models.ChannelWithSubscriptionJSON
|
||||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||||
// @Failure 409 {object} ginresp.apiError "channel already exists"
|
// @Failure 409 {object} ginresp.apiError "channel already exists"
|
||||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||||
//
|
//
|
||||||
// @Router /api/users/{uid}/channels/ [POST]
|
// @Router /api/users/{uid}/channels [POST]
|
||||||
func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type uri struct {
|
type uri struct {
|
||||||
UserID models.UserID `uri:"uid"`
|
UserID models.UserID `uri:"uid"`
|
||||||
}
|
}
|
||||||
type body struct {
|
type body struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Subscribe *bool `json:"subscribe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var u uri
|
var u uri
|
||||||
@ -684,7 +698,21 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true)))
|
if langext.Coalesce(b.Subscribe, true) {
|
||||||
|
|
||||||
|
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, true)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.WithSubscription(langext.Ptr(sub)).JSON(true)))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.WithSubscription(nil).JSON(true)))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateChannel swaggerdoc
|
// UpdateChannel swaggerdoc
|
||||||
@ -699,7 +727,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Param subscribe_key body string false "Send `true` to create a new subscribe_key"
|
// @Param subscribe_key body string false "Send `true` to create a new subscribe_key"
|
||||||
// @Param send_key body string false "Send `true` to create a new send_key"
|
// @Param send_key body string false "Send `true` to create a new send_key"
|
||||||
//
|
//
|
||||||
// @Success 200 {object} models.ChannelJSON
|
// @Success 200 {object} models.ChannelWithSubscriptionJSON
|
||||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||||
// @Failure 404 {object} ginresp.apiError "channel not found"
|
// @Failure 404 {object} ginresp.apiError "channel not found"
|
||||||
@ -754,12 +782,12 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
|
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON(true)))
|
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListChannelMessages swaggerdoc
|
// ListChannelMessages swaggerdoc
|
||||||
@ -865,11 +893,20 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
// ListUserSubscriptions swaggerdoc
|
// ListUserSubscriptions swaggerdoc
|
||||||
//
|
//
|
||||||
// @Summary List all channels of a user
|
// @Summary List all subscriptions of a user (incoming/owned)
|
||||||
|
// // @Description The possible values for 'selector' are:
|
||||||
|
// // @Description - "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)
|
||||||
|
// // @Description - "owner_confirmed" Confirmed subscriptions with the user as owner
|
||||||
|
// // @Description - "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner
|
||||||
|
// // @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
|
||||||
|
// // @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
|
||||||
|
// // @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
|
||||||
|
// //
|
||||||
// @ID api-user-subscriptions-list
|
// @ID api-user-subscriptions-list
|
||||||
// @Tags API-v2
|
// @Tags API-v2
|
||||||
//
|
//
|
||||||
// @Param uid path int true "UserID"
|
// @Param uid path int true "UserID"
|
||||||
|
// @Param selector query string true "Filter subscribptions (default: owner_all)" Enums(owner_all, owner_confirmed, owner_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
|
||||||
//
|
//
|
||||||
// @Success 200 {object} handler.ListUserSubscriptions.response
|
// @Success 200 {object} handler.ListUserSubscriptions.response
|
||||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||||
@ -881,12 +918,16 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
type uri struct {
|
type uri struct {
|
||||||
UserID models.UserID `uri:"uid"`
|
UserID models.UserID `uri:"uid"`
|
||||||
}
|
}
|
||||||
|
type query struct {
|
||||||
|
Selector *string `json:"selector" form:"selector" enums:"owner_all,owner_confirmed,owner_unconfirmed,incoming_all,incoming_confirmed,incoming_unconfirmed"`
|
||||||
|
}
|
||||||
type response struct {
|
type response struct {
|
||||||
Subscriptions []models.SubscriptionJSON `json:"subscriptions"`
|
Subscriptions []models.SubscriptionJSON `json:"subscriptions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var u uri
|
var u uri
|
||||||
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
|
var q query
|
||||||
|
ctx, errResp := h.app.StartRequest(g, &u, &q, nil, nil)
|
||||||
if errResp != nil {
|
if errResp != nil {
|
||||||
return *errResp
|
return *errResp
|
||||||
}
|
}
|
||||||
@ -896,14 +937,62 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
return *permResp
|
return *permResp
|
||||||
}
|
}
|
||||||
|
|
||||||
clients, err := h.database.ListSubscriptionsByOwner(ctx, u.UserID)
|
sel := strings.ToLower(langext.Coalesce(q.Selector, "owner_all"))
|
||||||
|
|
||||||
|
var res []models.Subscription
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if sel == "owner_all" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() })
|
} else if sel == "owner_confirmed" {
|
||||||
|
|
||||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Subscriptions: res}))
|
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(true))
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sel == "owner_unconfirmed" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(false))
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sel == "incoming_all" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sel == "incoming_confirmed" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true))
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sel == "incoming_unconfirmed" {
|
||||||
|
|
||||||
|
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false))
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonres := langext.ArrMap(res, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() })
|
||||||
|
|
||||||
|
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Subscriptions: jsonres}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListChannelSubscriptions swaggerdoc
|
// ListChannelSubscriptions swaggerdoc
|
||||||
|
@ -93,18 +93,21 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name stri
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) ListChannelsByOwner(ctx TxContext, userid models.UserID) ([]models.Channel, error) {
|
func (db *Database) ListChannelsByOwner(ctx TxContext, userid models.UserID, subUserID models.UserID) ([]models.ChannelWithSubscription, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE owner_user_id = :ouid", sq.PP{"ouid": userid})
|
rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub ON channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid", sq.PP{
|
||||||
|
"ouid": userid,
|
||||||
|
"subuid": subUserID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := models.DecodeChannels(rows)
|
data, err := models.DecodeChannelsWithSubscription(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -112,25 +115,27 @@ func (db *Database) ListChannelsByOwner(ctx TxContext, userid models.UserID) ([]
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) ListChannelsBySubscriber(ctx TxContext, userid models.UserID, confirmed bool) ([]models.Channel, error) {
|
func (db *Database) ListChannelsBySubscriber(ctx TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
confCond := ""
|
confCond := ""
|
||||||
if confirmed {
|
if confirmed != nil && *confirmed {
|
||||||
confCond = " AND sub.confirmed = 1"
|
confCond = " AND sub.confirmed = 1"
|
||||||
|
} else if confirmed != nil && !*confirmed {
|
||||||
|
confCond = " AND sub.confirmed = 0"
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := tx.Query(ctx, "SELECT * FROM channels LEFT JOIN subscriptions sub on channels.channel_id = sub.channel_id WHERE sub.subscriber_user_id = :suid "+confCond, sq.PP{
|
rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE sub.subscription_id IS NOT NULL "+confCond, sq.PP{
|
||||||
"suid": userid,
|
"subuid": userid,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := models.DecodeChannels(rows)
|
data, err := models.DecodeChannelsWithSubscription(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -138,25 +143,28 @@ func (db *Database) ListChannelsBySubscriber(ctx TxContext, userid models.UserID
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, confirmed bool) ([]models.Channel, error) {
|
func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
confCond := "OR sub.subscriber_user_id = ?"
|
confCond := ""
|
||||||
if confirmed {
|
if confirmed != nil && *confirmed {
|
||||||
confCond = "OR (sub.subscriber_user_id = ? AND sub.confirmed = 1)"
|
confCond = "OR sub.confirmed = 1"
|
||||||
|
} else if confirmed != nil && !*confirmed {
|
||||||
|
confCond = "OR sub.confirmed = 0"
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := tx.Query(ctx, "SELECT * FROM channels LEFT JOIN subscriptions sub on channels.channel_id = sub.channel_id WHERE owner_user_id = :ouid "+confCond, sq.PP{
|
rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid "+confCond, sq.PP{
|
||||||
"ouid": userid,
|
"ouid": userid,
|
||||||
|
"subuid": userid,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := models.DecodeChannels(rows)
|
data, err := models.DecodeChannelsWithSubscription(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -164,26 +172,27 @@ func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, co
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) GetChannel(ctx TxContext, userid models.UserID, channelid models.ChannelID) (models.Channel, error) {
|
func (db *Database) GetChannel(ctx TxContext, userid models.UserID, channelid models.ChannelID) (models.ChannelWithSubscription, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.Channel{}, err
|
return models.ChannelWithSubscription{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE owner_user_id = :ouid AND channel_id = :cid LIMIT 1", sq.PP{
|
rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid AND channels.channel_id = :cid LIMIT 1", sq.PP{
|
||||||
"ouid": userid,
|
"ouid": userid,
|
||||||
"cid": channelid,
|
"cid": channelid,
|
||||||
|
"subuid": userid,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.Channel{}, err
|
return models.ChannelWithSubscription{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := models.DecodeChannel(rows)
|
channel, err := models.DecodeChannelWithSubscription(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.Channel{}, err
|
return models.ChannelWithSubscription{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
return channel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) IncChannelMessageCounter(ctx TxContext, channel models.Channel) error {
|
func (db *Database) IncChannelMessageCounter(ctx TxContext, channel models.Channel) error {
|
||||||
|
@ -2,6 +2,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
server "blackforestbytes.com/simplecloudnotifier"
|
server "blackforestbytes.com/simplecloudnotifier"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/db/dbtools"
|
||||||
"blackforestbytes.com/simplecloudnotifier/db/schema"
|
"blackforestbytes.com/simplecloudnotifier/db/schema"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@ -9,7 +10,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||||
"time"
|
"time"
|
||||||
@ -40,7 +40,9 @@ func NewDatabase(conf server.Config) (*Database, error) {
|
|||||||
|
|
||||||
scndb := &Database{qqdb}
|
scndb := &Database{qqdb}
|
||||||
|
|
||||||
qqdb.SetListener(scndb)
|
qqdb.AddListener(dbtools.DBLogger{})
|
||||||
|
|
||||||
|
qqdb.AddListener(dbtools.NewDBPreprocessor(scndb.db))
|
||||||
|
|
||||||
return scndb, nil
|
return scndb, nil
|
||||||
}
|
}
|
||||||
@ -83,35 +85,3 @@ func (db *Database) Ping(ctx context.Context) error {
|
|||||||
func (db *Database) BeginTx(ctx context.Context) (sq.Tx, error) {
|
func (db *Database) BeginTx(ctx context.Context) (sq.Tx, error) {
|
||||||
return db.db.BeginTransaction(ctx, sql.LevelDefault)
|
return db.db.BeginTransaction(ctx, sql.LevelDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) OnQuery(txID *uint16, sql string, _ *sq.PP) {
|
|
||||||
if txID == nil {
|
|
||||||
log.Debug().Msg(fmt.Sprintf("[SQL-QUERY] %s", fmtSQLPrint(sql)))
|
|
||||||
} else {
|
|
||||||
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-QUERY] %s", *txID, fmtSQLPrint(sql)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) OnExec(txID *uint16, sql string, _ *sq.PP) {
|
|
||||||
if txID == nil {
|
|
||||||
log.Debug().Msg(fmt.Sprintf("[SQL-EXEC] %s", fmtSQLPrint(sql)))
|
|
||||||
} else {
|
|
||||||
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-EXEC] %s", *txID, fmtSQLPrint(sql)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) OnPing() {
|
|
||||||
log.Debug().Msg("[SQL-PING]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) OnTxBegin(txid uint16) {
|
|
||||||
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-START]", txid))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) OnTxCommit(txid uint16) {
|
|
||||||
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-COMMIT]", txid))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) OnTxRollback(txid uint16) {
|
|
||||||
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-ROLLBACK]", txid))
|
|
||||||
}
|
|
||||||
|
90
scnserver/db/dbtools/logger.go
Normal file
90
scnserver/db/dbtools/logger.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package dbtools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBLogger struct{}
|
||||||
|
|
||||||
|
func (l DBLogger) PrePing(ctx context.Context) error {
|
||||||
|
log.Debug().Msg("[SQL-PING]")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PreTxBegin(ctx context.Context, txid uint16) error {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-START]", txid))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PreTxCommit(txid uint16) error {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-COMMIT]", txid))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PreTxRollback(txid uint16) error {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-ROLLBACK]", txid))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PreQuery(ctx context.Context, txID *uint16, sql *string, params *sq.PP) error {
|
||||||
|
if txID == nil {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("[SQL-QUERY] %s", fmtSQLPrint(*sql)))
|
||||||
|
} else {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-QUERY] %s", *txID, fmtSQLPrint(*sql)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PreExec(ctx context.Context, txID *uint16, sql *string, params *sq.PP) error {
|
||||||
|
if txID == nil {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("[SQL-EXEC] %s", fmtSQLPrint(*sql)))
|
||||||
|
} else {
|
||||||
|
log.Debug().Msg(fmt.Sprintf("[SQL-TX<%d>-EXEC] %s", *txID, fmtSQLPrint(*sql)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PostPing(result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PostTxBegin(txid uint16, result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PostTxCommit(txid uint16, result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PostTxRollback(txid uint16, result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PostQuery(txID *uint16, sqlOriginal string, sqlReal string, params sq.PP) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l DBLogger) PostExec(txID *uint16, sqlOriginal string, sqlReal string, params sq.PP) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtSQLPrint(sql string) string {
|
||||||
|
if strings.Contains(sql, ";") {
|
||||||
|
return "(...multi...)"
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = strings.ReplaceAll(sql, "\r", "")
|
||||||
|
sql = strings.ReplaceAll(sql, "\n", " ")
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
238
scnserver/db/dbtools/preprocessor.go
Normal file
238
scnserver/db/dbtools/preprocessor.go
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
package dbtools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/sq"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// This is..., not good...
|
||||||
|
//
|
||||||
|
// for sq.ScanAll to work with (left-)joined tables _need_ to get column names aka "alias.column"
|
||||||
|
// But sqlite (and all other db server) only return "column" if we don't manually specify `alias.column as "alias.columnname"`
|
||||||
|
// But always specifying all columns (and their alias) would be __very__ cumbersome...
|
||||||
|
//
|
||||||
|
// The "solution" is this preprocessor, which translates queries of the form `SELECT tab1.*, tab2.* From tab1` into `SELECT tab1.col1 AS "tab1.col1", tab1.col2 AS "tab1.col2" ....`
|
||||||
|
//
|
||||||
|
// Prerequisites:
|
||||||
|
// - all aliased tables must be written as `tablename AS alias` (the variant without the AS keyword is invalid)
|
||||||
|
// - a star only expands to the (single) table in FROM. Use *, table2.* if there exists a second (joined) table
|
||||||
|
// - No weird SQL syntax, this "parser" is not very robust...
|
||||||
|
//
|
||||||
|
|
||||||
|
type DBPreprocessor struct {
|
||||||
|
db sq.DB
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
cacheColumns map[string][]string
|
||||||
|
cacheQuery map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var regexAlias = regexp.MustCompile("([A-Za-z_\\-0-9]+)\\s+AS\\s+([A-Za-z_\\-0-9]+)")
|
||||||
|
|
||||||
|
func NewDBPreprocessor(db sq.DB) *DBPreprocessor {
|
||||||
|
return &DBPreprocessor{
|
||||||
|
db: db,
|
||||||
|
lock: sync.Mutex{},
|
||||||
|
cacheColumns: make(map[string][]string),
|
||||||
|
cacheQuery: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PrePing(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PreTxBegin(ctx context.Context, txid uint16) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PreTxCommit(txid uint16) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PreTxRollback(txid uint16) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PreQuery(ctx context.Context, txID *uint16, sql *string, params *sq.PP) error {
|
||||||
|
sqlOriginal := *sql
|
||||||
|
|
||||||
|
pp.lock.Lock()
|
||||||
|
v, ok := pp.cacheQuery[sqlOriginal]
|
||||||
|
pp.lock.Unlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
*sql = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(sqlOriginal, "SELECT ") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idxFrom := strings.Index(sqlOriginal, " FROM ")
|
||||||
|
if idxFrom < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fromTableName := strings.Split(strings.TrimSpace(sqlOriginal[idxFrom+len(" FROM"):]), " ")[0]
|
||||||
|
|
||||||
|
sels := strings.TrimSpace(sqlOriginal[len("SELECT "):idxFrom])
|
||||||
|
|
||||||
|
split := strings.Split(sels, ",")
|
||||||
|
|
||||||
|
newsel := make([]string, 0)
|
||||||
|
|
||||||
|
aliasMap := make(map[string]string)
|
||||||
|
for _, v := range regexAlias.FindAllStringSubmatch(sqlOriginal, idxFrom+len(" FROM")) {
|
||||||
|
aliasMap[strings.TrimSpace(v[2])] = strings.TrimSpace(v[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expr := range split {
|
||||||
|
|
||||||
|
expr = strings.TrimSpace(expr)
|
||||||
|
|
||||||
|
if expr == "*" {
|
||||||
|
|
||||||
|
columns, err := pp.getTableColumns(ctx, fromTableName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, colname := range columns {
|
||||||
|
newsel = append(newsel, fmt.Sprintf("%s.%s AS \"%s\"", fromTableName, colname, colname))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if strings.HasSuffix(expr, ".*") {
|
||||||
|
|
||||||
|
tableName := expr[0 : len(expr)-2]
|
||||||
|
|
||||||
|
if tableRealName, ok := aliasMap[tableName]; ok {
|
||||||
|
|
||||||
|
columns, err := pp.getTableColumns(ctx, tableRealName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, colname := range columns {
|
||||||
|
newsel = append(newsel, fmt.Sprintf("%s.%s AS \"%s.%s\"", tableName, colname, tableName, colname))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if tableName == fromTableName {
|
||||||
|
|
||||||
|
columns, err := pp.getTableColumns(ctx, tableName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, colname := range columns {
|
||||||
|
newsel = append(newsel, fmt.Sprintf("%s.%s AS \"%s\"", tableName, colname, colname))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
columns, err := pp.getTableColumns(ctx, tableName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, colname := range columns {
|
||||||
|
newsel = append(newsel, fmt.Sprintf("%s.%s AS \"%s.%s\"", tableName, colname, tableName, colname))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
newSQL := "SELECT " + strings.Join(newsel, ", ") + sqlOriginal[idxFrom:]
|
||||||
|
|
||||||
|
pp.lock.Lock()
|
||||||
|
pp.cacheQuery[sqlOriginal] = newSQL
|
||||||
|
pp.lock.Unlock()
|
||||||
|
|
||||||
|
log.Debug().Msgf("Preprocessed SQL statement from '%s' --to--> '%s'", sqlOriginal, newSQL)
|
||||||
|
|
||||||
|
*sql = newSQL
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PreExec(ctx context.Context, txID *uint16, sql *string, params *sq.PP) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PostPing(result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PostTxBegin(txid uint16, result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PostTxCommit(txid uint16, result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PostTxRollback(txid uint16, result error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PostQuery(txID *uint16, sqlOriginal string, sqlReal string, params sq.PP) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) PostExec(txID *uint16, sqlOriginal string, sqlReal string, params sq.PP) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *DBPreprocessor) getTableColumns(ctx context.Context, tablename string) ([]string, error) {
|
||||||
|
pp.lock.Lock()
|
||||||
|
v, ok := pp.cacheColumns[tablename]
|
||||||
|
pp.lock.Unlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type res struct {
|
||||||
|
CID int64 `db:"cid"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Type string `db:"type"`
|
||||||
|
NotNull int `db:"notnull"`
|
||||||
|
DFLT *string `db:"dflt_value"`
|
||||||
|
PK int `db:"pk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := pp.db.Query(ctx, "PRAGMA table_info('"+tablename+"');", sq.PP{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resrows, err := sq.ScanAll[res](rows, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := langext.ArrMap(resrows, func(v res) string { return v.Name })
|
||||||
|
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return nil, errors.New("no columns in table '" + tablename + "' (table does not exist?)")
|
||||||
|
}
|
||||||
|
|
||||||
|
pp.lock.Lock()
|
||||||
|
pp.cacheColumns[tablename] = columns
|
||||||
|
pp.lock.Unlock()
|
||||||
|
|
||||||
|
return columns, nil
|
||||||
|
}
|
@ -62,13 +62,46 @@ func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.C
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID models.UserID) ([]models.Subscription, error) {
|
func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
|
||||||
tx, err := ctx.GetOrCreateTransaction(db)
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_owner_user_id = :ouid", sq.PP{"ouid": ownerUserID})
|
cond := ""
|
||||||
|
if confirmed != nil && *confirmed {
|
||||||
|
cond = " AND confirmed = 1"
|
||||||
|
} else if confirmed != nil && !*confirmed {
|
||||||
|
cond = " AND confirmed = 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE channel_owner_user_id = :ouid"+cond, sq.PP{"ouid": ownerUserID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := models.DecodeSubscriptions(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) ListSubscriptionsBySubscriber(ctx TxContext, subscriberUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
|
||||||
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cond := ""
|
||||||
|
if confirmed != nil && *confirmed {
|
||||||
|
cond = " AND confirmed = 1"
|
||||||
|
} else if confirmed != nil && !*confirmed {
|
||||||
|
cond = " AND confirmed = 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid"+cond, sq.PP{"suid": subscriberUserID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ func (db *Database) UpdateUserKeys(ctx TxContext, userid models.UserID, sendKey
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(ctx, "UPDATE users SET send_key = :sk, read_key = :rk, admin_key = :ak WHERE user_id = ?", sq.PP{
|
_, err = tx.Exec(ctx, "UPDATE users SET send_key = :sk, read_key = :rk, admin_key = :ak WHERE user_id = :uid", sq.PP{
|
||||||
"sk": sendKey,
|
"sk": sendKey,
|
||||||
"rk": readKey,
|
"rk": readKey,
|
||||||
"ak": adminKey,
|
"ak": adminKey,
|
||||||
|
@ -2,7 +2,6 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,14 +23,3 @@ func time2DBOpt(t *time.Time) *int64 {
|
|||||||
}
|
}
|
||||||
return langext.Ptr(t.UnixMilli())
|
return langext.Ptr(t.UnixMilli())
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtSQLPrint(sql string) string {
|
|
||||||
if strings.Contains(sql, ";") {
|
|
||||||
return "(...multi...)"
|
|
||||||
}
|
|
||||||
|
|
||||||
sql = strings.ReplaceAll(sql, "\r", "")
|
|
||||||
sql = strings.ReplaceAll(sql, "\n", " ")
|
|
||||||
|
|
||||||
return sql
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/rs/zerolog v1.28.0
|
github.com/rs/zerolog v1.28.0
|
||||||
github.com/swaggo/swag v1.8.7
|
github.com/swaggo/swag v1.8.7
|
||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.37
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.41
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -112,6 +112,14 @@ gogs.mikescher.com/BlackForestBytes/goext v0.0.36 h1:iOUYz2NEiObCCdBnkt8DPi1N8gH
|
|||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.36/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.36/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.37 h1:6XP5UOqiougzH0Xtzs5tIU4c0sAXmdMPCvGhRxqVwLU=
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.37 h1:6XP5UOqiougzH0Xtzs5tIU4c0sAXmdMPCvGhRxqVwLU=
|
||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.37/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.37/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.38 h1:P9tf0tHexcH6Q3u9pRwi4iQsOb65Lc80JvzD49f/EIY=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.38/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.39 h1:96QUjPMoCzcZpA2SYUU4XXytWJq3dcsnfska88tMikU=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.39/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.40 h1:wh5+IRmcMAbwJ8cK6JZvg71IiMUWuJoaKAR5dbt7FLU=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.40/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.41 h1:3p/MtkHZ2gulSdizXql3VnFf2v7WpeOBCmTi0rQYCQw=
|
||||||
|
gogs.mikescher.com/BlackForestBytes/goext v0.0.41/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||||
|
@ -31,6 +31,29 @@ func (c Channel) JSON(includeKey bool) ChannelJSON {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
|
||||||
|
return ChannelWithSubscription{
|
||||||
|
Channel: c,
|
||||||
|
Subscription: sub,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelWithSubscription struct {
|
||||||
|
Channel
|
||||||
|
Subscription *Subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChannelWithSubscription) JSON(includeChannelKey bool) ChannelWithSubscriptionJSON {
|
||||||
|
var sub *SubscriptionJSON = nil
|
||||||
|
if c.Subscription != nil {
|
||||||
|
sub = langext.Ptr(c.Subscription.JSON())
|
||||||
|
}
|
||||||
|
return ChannelWithSubscriptionJSON{
|
||||||
|
ChannelJSON: c.Channel.JSON(includeChannelKey),
|
||||||
|
Subscription: sub,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelJSON struct {
|
type ChannelJSON struct {
|
||||||
ChannelID ChannelID `json:"channel_id"`
|
ChannelID ChannelID `json:"channel_id"`
|
||||||
OwnerUserID UserID `json:"owner_user_id"`
|
OwnerUserID UserID `json:"owner_user_id"`
|
||||||
@ -42,6 +65,11 @@ type ChannelJSON struct {
|
|||||||
MessagesSent int `json:"messages_sent"`
|
MessagesSent int `json:"messages_sent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChannelWithSubscriptionJSON struct {
|
||||||
|
ChannelJSON
|
||||||
|
Subscription *SubscriptionJSON `json:"subscription"`
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelDB struct {
|
type ChannelDB struct {
|
||||||
ChannelID ChannelID `db:"channel_id"`
|
ChannelID ChannelID `db:"channel_id"`
|
||||||
OwnerUserID UserID `db:"owner_user_id"`
|
OwnerUserID UserID `db:"owner_user_id"`
|
||||||
@ -67,6 +95,22 @@ func (c ChannelDB) Model() Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChannelWithSubscriptionDB struct {
|
||||||
|
ChannelDB
|
||||||
|
Subscription *SubscriptionDB `db:"sub"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChannelWithSubscriptionDB) Model() ChannelWithSubscription {
|
||||||
|
var sub *Subscription = nil
|
||||||
|
if c.Subscription != nil {
|
||||||
|
sub = langext.Ptr(c.Subscription.Model())
|
||||||
|
}
|
||||||
|
return ChannelWithSubscription{
|
||||||
|
Channel: c.ChannelDB.Model(),
|
||||||
|
Subscription: sub,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func DecodeChannel(r *sqlx.Rows) (Channel, error) {
|
func DecodeChannel(r *sqlx.Rows) (Channel, error) {
|
||||||
data, err := sq.ScanSingle[ChannelDB](r, true)
|
data, err := sq.ScanSingle[ChannelDB](r, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -82,3 +126,19 @@ func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
|
|||||||
}
|
}
|
||||||
return langext.ArrMap(data, func(v ChannelDB) Channel { return v.Model() }), nil
|
return langext.ArrMap(data, func(v ChannelDB) Channel { return v.Model() }), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error) {
|
||||||
|
data, err := sq.ScanSingle[ChannelWithSubscriptionDB](r, true)
|
||||||
|
if err != nil {
|
||||||
|
return ChannelWithSubscription{}, err
|
||||||
|
}
|
||||||
|
return data.Model(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeChannelsWithSubscription(r *sqlx.Rows) ([]ChannelWithSubscription, error) {
|
||||||
|
data, err := sq.ScanAll[ChannelWithSubscriptionDB](r, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return langext.ArrMap(data, func(v ChannelWithSubscriptionDB) ChannelWithSubscription { return v.Model() }), nil
|
||||||
|
}
|
||||||
|
@ -45,10 +45,10 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) {
|
|||||||
|
|
||||||
joinClause := ""
|
joinClause := ""
|
||||||
if f.ConfirmedSubscriptionBy != nil {
|
if f.ConfirmedSubscriptionBy != nil {
|
||||||
joinClause += " LEFT JOIN subscriptions subs on messages.channel_id = subs.channel_id "
|
joinClause += " LEFT JOIN subscriptions AS subs on messages.channel_id = subs.channel_id "
|
||||||
}
|
}
|
||||||
if f.SearchString != nil {
|
if f.SearchString != nil {
|
||||||
joinClause += " JOIN messages_fts mfts on (mfts.rowid = messages.scn_message_id) "
|
joinClause += " JOIN messages_fts AS mfts on (mfts.rowid = messages.scn_message_id) "
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlClauses := make([]string, 0)
|
sqlClauses := make([]string, 0)
|
||||||
|
@ -1155,7 +1155,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"API-v2"
|
"API-v2"
|
||||||
],
|
],
|
||||||
"summary": "List channels of a user (subscribed/owned)",
|
"summary": "List channels of a user (subscribed/owned/all)",
|
||||||
"operationId": "api-channels-list",
|
"operationId": "api-channels-list",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -1206,9 +1206,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"/api/users/{uid}/channels/": {
|
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"API-v2"
|
"API-v2"
|
||||||
@ -1236,7 +1234,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/models.ChannelJSON"
|
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@ -1271,7 +1269,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"API-v2"
|
"API-v2"
|
||||||
],
|
],
|
||||||
"summary": "List all channels of a user",
|
"summary": "Get a single channel",
|
||||||
"operationId": "api-channels-get",
|
"operationId": "api-channels-get",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -1293,7 +1291,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/models.ChannelJSON"
|
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@ -1364,7 +1362,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/models.ChannelJSON"
|
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@ -1740,7 +1738,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"API-v2"
|
"API-v2"
|
||||||
],
|
],
|
||||||
"summary": "List all channels of a user",
|
"summary": "List all subscriptions of a user (incoming/owned)",
|
||||||
"operationId": "api-user-subscriptions-list",
|
"operationId": "api-user-subscriptions-list",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -1749,6 +1747,21 @@
|
|||||||
"name": "uid",
|
"name": "uid",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"owner_all",
|
||||||
|
"owner_confirmed",
|
||||||
|
"owner_unconfirmed",
|
||||||
|
"incoming_all",
|
||||||
|
"incoming_confirmed",
|
||||||
|
"incoming_unconfirmed"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter subscribptions (default: owner_all)",
|
||||||
|
"name": "selector",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -2380,6 +2393,9 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"subscribe": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2529,7 +2545,7 @@
|
|||||||
"channels": {
|
"channels": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/models.ChannelJSON"
|
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2804,7 +2820,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models.ChannelJSON": {
|
"models.ChannelWithSubscriptionJSON": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"channel_id": {
|
"channel_id": {
|
||||||
@ -2827,6 +2843,9 @@
|
|||||||
"description": "can be nil, depending on endpoint",
|
"description": "can be nil, depending on endpoint",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"subscription": {
|
||||||
|
"$ref": "#/definitions/models.SubscriptionJSON"
|
||||||
|
},
|
||||||
"timestamp_created": {
|
"timestamp_created": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -55,6 +55,8 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
subscribe:
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
handler.CreateSubscription.body:
|
handler.CreateSubscription.body:
|
||||||
properties:
|
properties:
|
||||||
@ -151,7 +153,7 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
channels:
|
channels:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/models.ChannelJSON'
|
$ref: '#/definitions/models.ChannelWithSubscriptionJSON'
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
handler.ListClients.response:
|
handler.ListClients.response:
|
||||||
@ -334,7 +336,7 @@ definitions:
|
|||||||
suppress_send:
|
suppress_send:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
models.ChannelJSON:
|
models.ChannelWithSubscriptionJSON:
|
||||||
properties:
|
properties:
|
||||||
channel_id:
|
channel_id:
|
||||||
type: integer
|
type: integer
|
||||||
@ -350,6 +352,8 @@ definitions:
|
|||||||
subscribe_key:
|
subscribe_key:
|
||||||
description: can be nil, depending on endpoint
|
description: can be nil, depending on endpoint
|
||||||
type: string
|
type: string
|
||||||
|
subscription:
|
||||||
|
$ref: '#/definitions/models.SubscriptionJSON'
|
||||||
timestamp_created:
|
timestamp_created:
|
||||||
type: string
|
type: string
|
||||||
timestamp_lastsent:
|
timestamp_lastsent:
|
||||||
@ -1328,10 +1332,9 @@ paths:
|
|||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.apiError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: List channels of a user (subscribed/owned)
|
summary: List channels of a user (subscribed/owned/all)
|
||||||
tags:
|
tags:
|
||||||
- API-v2
|
- API-v2
|
||||||
/api/users/{uid}/channels/:
|
|
||||||
post:
|
post:
|
||||||
operationId: api-channels-create
|
operationId: api-channels-create
|
||||||
parameters:
|
parameters:
|
||||||
@ -1349,7 +1352,7 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.ChannelJSON'
|
$ref: '#/definitions/models.ChannelWithSubscriptionJSON'
|
||||||
"400":
|
"400":
|
||||||
description: supplied values/parameters cannot be parsed / are invalid
|
description: supplied values/parameters cannot be parsed / are invalid
|
||||||
schema:
|
schema:
|
||||||
@ -1387,7 +1390,7 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.ChannelJSON'
|
$ref: '#/definitions/models.ChannelWithSubscriptionJSON'
|
||||||
"400":
|
"400":
|
||||||
description: supplied values/parameters cannot be parsed / are invalid
|
description: supplied values/parameters cannot be parsed / are invalid
|
||||||
schema:
|
schema:
|
||||||
@ -1404,7 +1407,7 @@ paths:
|
|||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.apiError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: List all channels of a user
|
summary: Get a single channel
|
||||||
tags:
|
tags:
|
||||||
- API-v2
|
- API-v2
|
||||||
patch:
|
patch:
|
||||||
@ -1434,7 +1437,7 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.ChannelJSON'
|
$ref: '#/definitions/models.ChannelWithSubscriptionJSON'
|
||||||
"400":
|
"400":
|
||||||
description: supplied values/parameters cannot be parsed / are invalid
|
description: supplied values/parameters cannot be parsed / are invalid
|
||||||
schema:
|
schema:
|
||||||
@ -1693,6 +1696,18 @@ paths:
|
|||||||
name: uid
|
name: uid
|
||||||
required: true
|
required: true
|
||||||
type: integer
|
type: integer
|
||||||
|
- description: 'Filter subscribptions (default: owner_all)'
|
||||||
|
enum:
|
||||||
|
- owner_all
|
||||||
|
- owner_confirmed
|
||||||
|
- owner_unconfirmed
|
||||||
|
- incoming_all
|
||||||
|
- incoming_confirmed
|
||||||
|
- incoming_unconfirmed
|
||||||
|
in: query
|
||||||
|
name: selector
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
@ -1710,7 +1725,7 @@ paths:
|
|||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.apiError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: List all channels of a user
|
summary: List all subscriptions of a user (incoming/owned)
|
||||||
tags:
|
tags:
|
||||||
- API-v2
|
- API-v2
|
||||||
post:
|
post:
|
||||||
|
@ -28,8 +28,8 @@ func TestCreateChannel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
chan0 := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
|
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
|
||||||
tt.AssertEqual(t, "chan-count", 0, len(chan0.Channels))
|
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "name")
|
||||||
}
|
}
|
||||||
|
|
||||||
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
|
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
|
||||||
@ -39,7 +39,7 @@ func TestCreateChannel(t *testing.T) {
|
|||||||
{
|
{
|
||||||
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
|
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
|
||||||
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
|
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
|
||||||
tt.AssertEqual(t, "chan.name", "test", clist.Channels[0]["name"])
|
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "name")
|
||||||
}
|
}
|
||||||
|
|
||||||
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
|
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
|
||||||
@ -48,9 +48,7 @@ func TestCreateChannel(t *testing.T) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
|
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
|
||||||
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels))
|
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "name")
|
||||||
tt.AssertArrAny(t, "chan.has('asdf')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "asdf" })
|
|
||||||
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,12 +135,37 @@ func TestChannelNameNormalization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListChannels(t *testing.T) {
|
|
||||||
t.SkipNow() //TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListChannelsOwned(t *testing.T) {
|
func TestListChannelsOwned(t *testing.T) {
|
||||||
t.SkipNow() //TODO
|
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
data := tt.InitDefaultData(t, ws)
|
||||||
|
|
||||||
|
type chanlist struct {
|
||||||
|
Channels []gin.H `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
testdata := map[int][]string{
|
||||||
|
0: {"main", "chattingchamber", "unicdhll", "promotions", "reminders"},
|
||||||
|
1: {"promotions"},
|
||||||
|
2: {},
|
||||||
|
3: {},
|
||||||
|
4: {},
|
||||||
|
5: {},
|
||||||
|
6: {},
|
||||||
|
7: {},
|
||||||
|
8: {},
|
||||||
|
9: {},
|
||||||
|
10: {},
|
||||||
|
11: {},
|
||||||
|
12: {},
|
||||||
|
13: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testdata {
|
||||||
|
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
|
||||||
|
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "name")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListChannelsSubscribedAny(t *testing.T) {
|
func TestListChannelsSubscribedAny(t *testing.T) {
|
||||||
|
@ -2,6 +2,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@ -199,3 +200,22 @@ func AssertMultiNonEmpty(t *testing.T, key string, args ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AssertMappedSet[T langext.OrderedConstraint](t *testing.T, key string, expected []T, values []gin.H, objkey string) {
|
||||||
|
|
||||||
|
actual := langext.ArrMap(values, func(v gin.H) T { return v[objkey].(T) })
|
||||||
|
|
||||||
|
langext.Sort(actual)
|
||||||
|
langext.Sort(expected)
|
||||||
|
|
||||||
|
if !langext.ArrEqualsExact(actual, expected) {
|
||||||
|
t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual)
|
||||||
|
|
||||||
|
t.Errorf("Actual := [%v]\n", actual)
|
||||||
|
t.Errorf("Expected := [%v]\n", expected)
|
||||||
|
|
||||||
|
t.Error(string(debug.Stack()))
|
||||||
|
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -86,6 +86,8 @@ var userExamples = []userex{
|
|||||||
{9, true, "UniqueUnicorn", "Galaxy Quest", "2023.1", "ANDROID", "FCM_TOK_EX_010", ""},
|
{9, true, "UniqueUnicorn", "Galaxy Quest", "2023.1", "ANDROID", "FCM_TOK_EX_010", ""},
|
||||||
{10, false, "", "", "", "", "", ""},
|
{10, false, "", "", "", "", "", ""},
|
||||||
{11, false, "", "", "", "", "", "ANDROID|v2|PURCHASED:PRO_TOK_002"},
|
{11, false, "", "", "", "", "", "ANDROID|v2|PURCHASED:PRO_TOK_002"},
|
||||||
|
{12, true, "ChanTester1", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_012", ""},
|
||||||
|
{13, true, "ChanTester2", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_013", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientExamples = []clientex{
|
var clientExamples = []clientex{
|
||||||
@ -264,6 +266,15 @@ var messageExamples = []msgex{
|
|||||||
{11, "Promotions", "192.168.0.1", P1, AKEY, "Announcing Our Annual Black Friday Sale", "Mark your calendars and get ready for the biggest sale of the year. Our annual Black Friday sale is coming soon and you won't want to miss out on the amazing deals and discounts.", 0},
|
{11, "Promotions", "192.168.0.1", P1, AKEY, "Announcing Our Annual Black Friday Sale", "Mark your calendars and get ready for the biggest sale of the year. Our annual Black Friday sale is coming soon and you won't want to miss out on the amazing deals and discounts.", 0},
|
||||||
{11, "Promotions", "", PX, AKEY, "Join Our VIP Club and Enjoy Exclusive Benefits", "Sign up for our VIP club and enjoy exclusive benefits like early access to sales, special offers, and personalized service. Don't miss out on this exclusive opportunity.", timeext.FromHours(2.32)},
|
{11, "Promotions", "", PX, AKEY, "Join Our VIP Club and Enjoy Exclusive Benefits", "Sign up for our VIP club and enjoy exclusive benefits like early access to sales, special offers, and personalized service. Don't miss out on this exclusive opportunity.", timeext.FromHours(2.32)},
|
||||||
{11, "Promotions", "", P2, SKEY, "Summer Clearance: Save Up to 75% on Your Favorite Products", "It's time for our annual summer clearance sale! Save up to 75% on your favorite products, from clothing and accessories to home decor and more.", timeext.FromHours(1.87)},
|
{11, "Promotions", "", P2, SKEY, "Summer Clearance: Save Up to 75% on Your Favorite Products", "It's time for our annual summer clearance sale! Save up to 75% on your favorite products, from clothing and accessories to home decor and more.", timeext.FromHours(1.87)},
|
||||||
|
|
||||||
|
{12, "", "", P0, SKEY, "New Product Launch", "We are excited to announce the launch of our new product, the XYZ widget", 0},
|
||||||
|
{12, "chan_self_subscribed", "", P0, SKEY, "Important Update", "We have released a critical update", 0},
|
||||||
|
{12, "chan_self_unsub", "", P0, SKEY, "Reminder: Upcoming Maintenance", "", 0},
|
||||||
|
|
||||||
|
{13, "", "", P0, SKEY, "New Feature Available", "ability to schedule appointments", 0},
|
||||||
|
{13, "chan_other_nosub", "", P0, SKEY, "Account Suspended", "Please contact us", 0},
|
||||||
|
{13, "chan_other_request", "", P0, SKEY, "Invitation to Beta Test", "", 0},
|
||||||
|
{13, "chan_other_accepted", "", P0, SKEY, "New Blog Post", "Congratulations on your promotion! We are proud", 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefData struct {
|
type DefData struct {
|
||||||
@ -286,6 +297,8 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
|
|||||||
|
|
||||||
users := make([]Userdat, 0, len(userExamples))
|
users := make([]Userdat, 0, len(userExamples))
|
||||||
|
|
||||||
|
// Create Users
|
||||||
|
|
||||||
for _, uex := range userExamples {
|
for _, uex := range userExamples {
|
||||||
body := gin.H{}
|
body := gin.H{}
|
||||||
if uex.WithClient {
|
if uex.WithClient {
|
||||||
@ -318,6 +331,8 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Clients
|
||||||
|
|
||||||
for _, cex := range clientExamples {
|
for _, cex := range clientExamples {
|
||||||
body := gin.H{}
|
body := gin.H{}
|
||||||
body["agent_model"] = cex.AgentModel
|
body["agent_model"] = cex.AgentModel
|
||||||
@ -327,15 +342,9 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
|
|||||||
RequestAuthPost[gin.H](t, users[cex.User].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/clients", users[cex.User].UID), body)
|
RequestAuthPost[gin.H](t, users[cex.User].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/clients", users[cex.User].UID), body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Messages
|
||||||
|
|
||||||
for _, mex := range messageExamples {
|
for _, mex := range messageExamples {
|
||||||
//User int
|
|
||||||
//Channel string
|
|
||||||
//SenderName string
|
|
||||||
//Priority int
|
|
||||||
//Key int
|
|
||||||
//Title string
|
|
||||||
//Content string
|
|
||||||
//TSOffset time.Duration
|
|
||||||
body := gin.H{}
|
body := gin.H{}
|
||||||
body["title"] = mex.Title
|
body["title"] = mex.Title
|
||||||
body["user_id"] = users[mex.User].UID
|
body["user_id"] = users[mex.User].UID
|
||||||
@ -364,6 +373,15 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
|
|||||||
RequestPost[gin.H](t, baseUrl, "/", body)
|
RequestPost[gin.H](t, baseUrl, "/", body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sub/Unsub for Users 12+13
|
||||||
|
|
||||||
|
{
|
||||||
|
//TODO User 12 unsubscribe from 12:chan_self_unsub
|
||||||
|
//TODO User 13 request-subscribe to 13:chan_other_request
|
||||||
|
//TODO User 13 request-subscribe to 13:chan_other_accepted
|
||||||
|
//TODO User 13 accept subscription from user 12 to 13:chan_other_accepted
|
||||||
|
}
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
return DefData{User: users}
|
return DefData{User: users}
|
||||||
|
Loading…
Reference in New Issue
Block a user