Fix SQL unmarshalling of optional nested structs (LEFT JOIN)

This commit is contained in:
Mike Schwörer 2022-12-22 12:43:40 +01:00
parent 0cb2a977a0
commit 0112d681ac
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
20 changed files with 403 additions and 135 deletions

View File

@ -17,6 +17,8 @@
- diff my currently used scnsend script vs the one in the docs here
- Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions
-------------------------------------------------------------------------------------------------------------------------------
- in my script: use (backupname || hostname) for sendername

View File

@ -18,6 +18,7 @@ const (
BINDFAIL_QUERY_PARAM APIError = 1151
BINDFAIL_BODY_PARAM APIError = 1152
BINDFAIL_URI_PARAM APIError = 1153
INVALID_BODY_PARAM APIError = 1161
INVALID_ENUM_VALUE APIError = 1171
NO_TITLE APIError = 1201
@ -29,12 +30,13 @@ const (
CHANNEL_TOO_LONG APIError = 1207
CHANNEL_NAME_WOULD_CHANGE APIError = 1207
USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302
CHANNEL_NOT_FOUND APIError = 1303
SUBSCRIPTION_NOT_FOUND APIError = 1304
MESSAGE_NOT_FOUND APIError = 1305
USER_AUTH_FAILED APIError = 1311
USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302
CHANNEL_NOT_FOUND APIError = 1303
SUBSCRIPTION_NOT_FOUND APIError = 1304
MESSAGE_NOT_FOUND APIError = 1305
SUBSCRIPTION_USER_MISMATCH APIError = 1306
USER_AUTH_FAILED APIError = 1311
NO_DEVICE_LINKED APIError = 1401

View File

@ -2,7 +2,6 @@ package handler
import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/db/cursortoken"
@ -501,8 +500,8 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
// @ID api-channels-list
// @Tags API-v2
//
// @Param uid path int true "UserID"
// @Param selector query string true "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
// @Param uid path int true "UserID"
// @Param selector query string false "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
//
// @Success 200 {object} handler.ListChannels.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
@ -667,6 +666,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
if b.Name == "" {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil)
}
channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
@ -677,17 +680,17 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
user, err := h.database.GetUser(ctx, u.UserID)
if err == sql.ErrNoRows {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", nil)
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
}
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if channelExisting != nil {
@ -915,27 +918,27 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
// ListUserSubscriptions swaggerdoc
//
// @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
// @Tags API-v2
// @Summary List all subscriptions of a user (incoming/owned)
// @Description The possible values for 'selector' are:
// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
// @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)
//
// @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)
// @ID api-user-subscriptions-list
// @Tags API-v2
//
// @Success 200 {object} handler.ListUserSubscriptions.response
// @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 500 {object} ginresp.apiError "internal server error"
// @Param uid path int true "UserID"
// @Param selector query string true "Filter subscriptions (default: owner_all)" Enums(outgoing_all, outgoing_confirmed, outgoing_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
//
// @Router /api/users/{uid}/subscriptions [GET]
// @Success 200 {object} handler.ListUserSubscriptions.response
// @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 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/users/{uid}/subscriptions [GET]
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid"`
@ -964,48 +967,48 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
var res []models.Subscription
var err error
if sel == "owner_all" {
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "owner_confirmed" {
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" {
if sel == "outgoing_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" {
} else if sel == "outgoing_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" {
} else if sel == "outgoing_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 if sel == "incoming_all" {
res, err = h.database.ListSubscriptionsByChannelOwner(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.ListSubscriptionsByChannelOwner(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.ListSubscriptionsByChannelOwner(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)
@ -1111,9 +1114,8 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON()))
@ -1159,9 +1161,8 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
err = h.database.DeleteSubscription(ctx, u.SubscriptionID)
@ -1174,27 +1175,29 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
// CreateSubscription swaggerdoc
//
// @Summary Creare/Request a subscription
// @ID api-subscriptions-create
// @Tags API-v2
// @Summary Create/Request a subscription
// @Description Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body
// @ID api-subscriptions-create
// @Tags API-v2
//
// @Param uid path int true "UserID"
// @Param query_data query handler.CreateSubscription.query false " "
// @Param post_data body handler.CreateSubscription.body false " "
// @Param uid path int true "UserID"
// @Param query_data query handler.CreateSubscription.query false " "
// @Param post_data body handler.CreateSubscription.body false " "
//
// @Success 200 {object} models.SubscriptionJSON
// @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 500 {object} ginresp.apiError "internal server error"
// @Success 200 {object} models.SubscriptionJSON
// @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 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/users/{uid}/subscriptions [POST]
// @Router /api/users/{uid}/subscriptions [POST]
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid"`
}
type body struct {
ChannelOwnerUserID models.UserID `form:"channel_owner_user_id" binding:"required"`
Channel string `form:"channel_name" binding:"required"`
ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id"`
ChannelInternalName *string `json:"channel_internal_name"`
ChannelID *models.ChannelID `json:"channel_id"`
}
type query struct {
ChanSubscribeKey *string `json:"chan_subscribe_key" form:"chan_subscribe_key"`
@ -1213,21 +1216,45 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
channelInternalName := h.app.NormalizeChannelInternalName(b.Channel)
var channel models.Channel
if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil {
channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName)
outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil)
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if channel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) {
ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
sub, err := h.database.CreateSubscription(ctx, u.UserID, *channel, channel.OwnerUserID == u.UserID)
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
}
@ -1279,9 +1306,8 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
if subscription.SubscriberUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
if b.Confirmed != nil {

View File

@ -124,7 +124,7 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
_, libVersionNumber, _ := sqlite3.Version()
if libVersionNumber < 3039000 {
ginresp.InternalError(errors.New("sqlite version too low"))
return ginresp.InternalError(errors.New("sqlite version too low"))
}
err := h.app.Database.Ping(ctx)

View File

@ -57,6 +57,30 @@ func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, s
return &channel, nil
}
func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE channel_id = :cid LIMIT 1", sq.PP{
"cid": chanid,
})
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
}
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {

View File

@ -206,12 +206,7 @@ func (pp *DBPreprocessor) getTableColumns(ctx context.Context, tablename string)
}
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"`
Name string `db:"name"`
}
rows, err := pp.db.Query(ctx, "PRAGMA table_info('"+tablename+"');", sq.PP{})
@ -219,7 +214,7 @@ func (pp *DBPreprocessor) getTableColumns(ctx context.Context, tablename string)
return nil, err
}
resrows, err := sq.ScanAll[res](rows, true)
resrows, err := sq.ScanAll[res](rows, sq.SModeFast, sq.Unsafe, true)
if err != nil {
return nil, err
}

View File

@ -62,7 +62,7 @@ func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.C
return data, nil
}
func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
func (db *Database) ListSubscriptionsByChannelOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err

View File

@ -8,7 +8,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.28.0
github.com/swaggo/swag v1.8.7
gogs.mikescher.com/BlackForestBytes/goext v0.0.42
gogs.mikescher.com/BlackForestBytes/goext v0.0.46
)
require (

View File

@ -122,6 +122,12 @@ gogs.mikescher.com/BlackForestBytes/goext v0.0.41 h1:3p/MtkHZ2gulSdizXql3VnFf2v7
gogs.mikescher.com/BlackForestBytes/goext v0.0.41/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.42 h1:u6+pDRrL9wSvJG7gVsGUO4dA54qzac5LsqoXqi6oo9E=
gogs.mikescher.com/BlackForestBytes/goext v0.0.42/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.44 h1:YC8SrQk1BEDR5wCdLZ2trnNvkUg/sssW94XYKrsKyc4=
gogs.mikescher.com/BlackForestBytes/goext v0.0.44/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.45 h1:1naABIgSa5hhWPT7kYAAEeIUBNLo7nVvE6/kz9LoY9Q=
gogs.mikescher.com/BlackForestBytes/goext v0.0.45/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.46 h1:7nV9RKnnz/qgkVWvlj4MOAITbe+Gas1niVQgvbHnNk8=
gogs.mikescher.com/BlackForestBytes/goext v0.0.46/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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=

View File

@ -117,7 +117,7 @@ func (c ChannelWithSubscriptionDB) Model() ChannelWithSubscription {
}
func DecodeChannel(r *sqlx.Rows) (Channel, error) {
data, err := sq.ScanSingle[ChannelDB](r, true)
data, err := sq.ScanSingle[ChannelDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Channel{}, err
}
@ -125,7 +125,7 @@ func DecodeChannel(r *sqlx.Rows) (Channel, error) {
}
func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
data, err := sq.ScanAll[ChannelDB](r, true)
data, err := sq.ScanAll[ChannelDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
@ -133,7 +133,7 @@ func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
}
func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error) {
data, err := sq.ScanSingle[ChannelWithSubscriptionDB](r, true)
data, err := sq.ScanSingle[ChannelWithSubscriptionDB](r, sq.SModeExtended, sq.Safe, true)
if err != nil {
return ChannelWithSubscription{}, err
}
@ -141,7 +141,7 @@ func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error
}
func DecodeChannelsWithSubscription(r *sqlx.Rows) ([]ChannelWithSubscription, error) {
data, err := sq.ScanAll[ChannelWithSubscriptionDB](r, true)
data, err := sq.ScanAll[ChannelWithSubscriptionDB](r, sq.SModeExtended, sq.Safe, true)
if err != nil {
return nil, err
}

View File

@ -69,7 +69,7 @@ func (c ClientDB) Model() Client {
}
func DecodeClient(r *sqlx.Rows) (Client, error) {
data, err := sq.ScanSingle[ClientDB](r, true)
data, err := sq.ScanSingle[ClientDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Client{}, err
}
@ -77,7 +77,7 @@ func DecodeClient(r *sqlx.Rows) (Client, error) {
}
func DecodeClients(r *sqlx.Rows) ([]Client, error) {
data, err := sq.ScanAll[ClientDB](r, true)
data, err := sq.ScanAll[ClientDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}

View File

@ -89,7 +89,7 @@ func (d DeliveryDB) Model() Delivery {
}
func DecodeDelivery(r *sqlx.Rows) (Delivery, error) {
data, err := sq.ScanSingle[DeliveryDB](r, true)
data, err := sq.ScanSingle[DeliveryDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Delivery{}, err
}
@ -97,7 +97,7 @@ func DecodeDelivery(r *sqlx.Rows) (Delivery, error) {
}
func DecodeDeliveries(r *sqlx.Rows) ([]Delivery, error) {
data, err := sq.ScanAll[DeliveryDB](r, true)
data, err := sq.ScanAll[DeliveryDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}

View File

@ -146,7 +146,7 @@ func (m MessageDB) Model() Message {
}
func DecodeMessage(r *sqlx.Rows) (Message, error) {
data, err := sq.ScanSingle[MessageDB](r, true)
data, err := sq.ScanSingle[MessageDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Message{}, err
}
@ -154,7 +154,7 @@ func DecodeMessage(r *sqlx.Rows) (Message, error) {
}
func DecodeMessages(r *sqlx.Rows) ([]Message, error) {
data, err := sq.ScanAll[MessageDB](r, true)
data, err := sq.ScanAll[MessageDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}

View File

@ -62,7 +62,7 @@ func (s SubscriptionDB) Model() Subscription {
}
func DecodeSubscription(r *sqlx.Rows) (Subscription, error) {
data, err := sq.ScanSingle[SubscriptionDB](r, true)
data, err := sq.ScanSingle[SubscriptionDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Subscription{}, err
}
@ -70,7 +70,7 @@ func DecodeSubscription(r *sqlx.Rows) (Subscription, error) {
}
func DecodeSubscriptions(r *sqlx.Rows) ([]Subscription, error) {
data, err := sq.ScanAll[SubscriptionDB](r, true)
data, err := sq.ScanAll[SubscriptionDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}

View File

@ -163,7 +163,7 @@ func (u UserDB) Model() User {
}
func DecodeUser(r *sqlx.Rows) (User, error) {
data, err := sq.ScanSingle[UserDB](r, true)
data, err := sq.ScanSingle[UserDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return User{}, err
}
@ -171,7 +171,7 @@ func DecodeUser(r *sqlx.Rows) (User, error) {
}
func DecodeUsers(r *sqlx.Rows) ([]User, error) {
data, err := sq.ScanAll[UserDB](r, true)
data, err := sq.ScanAll[UserDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}

View File

@ -1176,8 +1176,7 @@
"type": "string",
"description": "Filter channels (default: owned)",
"name": "selector",
"in": "query",
"required": true
"in": "query"
}
],
"responses": {
@ -1743,6 +1742,7 @@
},
"/api/users/{uid}/subscriptions": {
"get": {
"description": "The possible values for 'selector' are:\n- \"owner_all\" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)\n- \"owner_confirmed\" Confirmed subscriptions with the user as owner\n- \"owner_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as owner\n- \"incoming_all\" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)\n- \"incoming_confirmed\" Confirmed subscriptions from other users to channels of this user\n- \"incoming_unconfirmed\" Unconfirmed subscriptions from other users to channels of this user (= requests)",
"tags": [
"API-v2"
],
@ -1766,7 +1766,7 @@
"incoming_unconfirmed"
],
"type": "string",
"description": "Filter subscribptions (default: owner_all)",
"description": "Filter subscriptions (default: owner_all)",
"name": "selector",
"in": "query",
"required": true
@ -1800,10 +1800,11 @@
}
},
"post": {
"description": "Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body",
"tags": [
"API-v2"
],
"summary": "Creare/Request a subscription",
"summary": "Create/Request a subscription",
"operationId": "api-subscriptions-create",
"parameters": [
{
@ -2410,11 +2411,15 @@
"handler.CreateSubscription.body": {
"type": "object",
"required": [
"channel",
"channel_id",
"channel_internal_name",
"channel_owner_user_id"
],
"properties": {
"channel": {
"channel_id": {
"type": "integer"
},
"channel_internal_name": {
"type": "string"
},
"channel_owner_user_id": {

View File

@ -60,12 +60,15 @@ definitions:
type: object
handler.CreateSubscription.body:
properties:
channel:
channel_id:
type: integer
channel_internal_name:
type: string
channel_owner_user_id:
type: integer
required:
- channel
- channel_id
- channel_internal_name
- channel_owner_user_id
type: object
handler.CreateUser.body:
@ -1315,7 +1318,6 @@ paths:
- all_any
in: query
name: selector
required: true
type: string
responses:
"200":
@ -1697,6 +1699,14 @@ paths:
- API-v2
/api/users/{uid}/subscriptions:
get:
description: |-
The possible values for 'selector' are:
- "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)
- "owner_confirmed" Confirmed subscriptions with the user as owner
- "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner
- "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
- "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
- "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
operationId: api-user-subscriptions-list
parameters:
- description: UserID
@ -1704,7 +1714,7 @@ paths:
name: uid
required: true
type: integer
- description: 'Filter subscribptions (default: owner_all)'
- description: 'Filter subscriptions (default: owner_all)'
enum:
- owner_all
- owner_confirmed
@ -1737,6 +1747,8 @@ paths:
tags:
- API-v2
post:
description: Either [channel_owner_user_id, channel_internal_name] or [channel_id]
must be supplied in the request body
operationId: api-subscriptions-create
parameters:
- description: UserID
@ -1769,7 +1781,7 @@ paths:
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Creare/Request a subscription
summary: Create/Request a subscription
tags:
- API-v2
/api/users/{uid}/subscriptions/{sid}:

View File

@ -161,10 +161,10 @@ func TestListChannelsOwned(t *testing.T) {
}
testdata := map[int][]string{
0: {"main", "chattingchamber", "unicdhll", "promotions", "reminders"},
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "innovations", "reminders"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
@ -175,8 +175,8 @@ func TestListChannelsOwned(t *testing.T) {
11: {"promotions"},
12: {},
13: {},
14: {"", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"", "chan_other_nosub", "chan_other_request", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
for k, v := range testdata {
@ -186,19 +186,143 @@ func TestListChannelsOwned(t *testing.T) {
}
func TestListChannelsSubscribedAny(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", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
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, "internal_name")
}
}
func TestListChannelsAllAny(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", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
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, "internal_name")
}
}
func TestListChannelsSubscribed(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", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
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, "internal_name")
}
}
func TestListChannelsAll(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", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
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, "internal_name")
}
}
//TODO test missing channel-xx methods

View File

@ -257,8 +257,8 @@ var messageExamples = []msgex{
{11, "Promotions", "localhost", P2, SKEY, "New Product Launch: Introducing Our Latest Innovation", "We are excited to announce the release of our newest product, designed to revolutionize the industry. Don't miss out on this game-changing technology.", timeext.FromHours(-12.21)},
{11, "Promotions", "#S0", P0, SKEY, "Limited Time Offer: Get 50% Off Your Next Purchase", "For a limited time, take advantage of our special offer and get half off your next purchase. Don't miss out on this amazing deal.", 0},
{11, "Promotions", "#S0", P2, SKEY, "Customer Appreciation Sale: Save Up to 75% on Your Favorite Products", "", 0},
{11, "Promotions", "", P0, SKEY, "Sign Up for Our Newsletter and Save 10% on Your Next Order", "", 0},
{11, "Promotions", "", PX, AKEY, "New Arrivals: Check Out Our Latest Collection", "We've just added new items to our collection and we think you'll love them. Take a look and see what's new in fashion, home decor, and more.", 0},
{11, " Promotions", "", P0, SKEY, "Sign Up for Our Newsletter and Save 10% on Your Next Order", "", 0},
{11, "Promotions ", "", PX, AKEY, "New Arrivals: Check Out Our Latest Collection", "We've just added new items to our collection and we think you'll love them. Take a look and see what's new in fashion, home decor, and more.", 0},
{11, "Promotions", "", PX, AKEY, "Join Our Rewards Program and Earn Points on Every Purchase", "Sign up for our rewards program and earn points on every purchase you make. Redeem your points for discounts, free products, and more.", 0},
{11, "Promotions", "#S0", P0, SKEY, "Seasonal Special: Save on Your Favorite Fall Products", "As the leaves change color and the air gets cooler, we have the perfect products to help you enjoy the season. Take advantage of our special offers and save on your favorite fall products.", 0},
{11, "Promotions", "192.168.0.1", P1, AKEY, "Refer a Friend and Save on Your Next Order", "Share the love and refer a friend to our store. When they make a purchase, you'll receive a discount on your next order. It's a win-win for both of you.", 0},
@ -384,10 +384,10 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
// 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
doUnsubscribe(t, baseUrl, users[14], users[14], "chan_self_unsub")
doSubscribe(t, baseUrl, users[14], users[15], "chan_other_request")
doSubscribe(t, baseUrl, users[14], users[15], "chan_other_accepted")
doAcceptSub(t, baseUrl, users[15], users[14], "chan_other_accepted")
}
success = true
@ -395,6 +395,78 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
return DefData{User: users}
}
func doSubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) {
if user == chanOwner {
RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", user.UID), gin.H{
"channel_owner_user_id": chanOwner.UID,
"channel_internal_name": chanInternalName,
})
} else {
type chanlist struct {
Channels []gin.H `json:"channels"`
}
clist := RequestAuthGet[chanlist](t, chanOwner.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=owned", chanOwner.UID))
var chandat gin.H
for _, v := range clist.Channels {
if v["internal_name"].(string) == chanInternalName {
chandat = v
break
}
}
RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?chan_subscribe_key=%s", user.UID, chandat["subscribe_key"].(string)), gin.H{
"channel_id": chandat["channel_id"].(float64),
})
}
}
func doUnsubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) {
type chanlist struct {
Subscriptions []gin.H `json:"subscriptions"`
}
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?selector=outgoing_confirmed", user.UID))
var subdat gin.H
for _, v := range slist.Subscriptions {
if v["channel_internal_name"].(string) == chanInternalName && int64(v["channel_owner_user_id"].(float64)) == chanOwner.UID {
subdat = v
break
}
}
RequestAuthDelete[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{})
}
func doAcceptSub(t *testing.T, baseUrl string, user Userdat, subscriber Userdat, chanInternalName string) {
type chanlist struct {
Subscriptions []gin.H `json:"subscriptions"`
}
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?selector=incoming_unconfirmed", user.UID))
var subdat gin.H
for _, v := range slist.Subscriptions {
if v["channel_internal_name"].(string) == chanInternalName && int64(v["subscriber_user_id"].(float64)) == subscriber.UID {
subdat = v
break
}
}
RequestAuthDelete[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{})
}
func Lipsum(seed int64, paracount int) string {
return loremipsum.NewWithSeed(seed).Paragraphs(paracount)
}

View File

@ -152,7 +152,7 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
TPrintln("")
if resp.StatusCode != 200 {
TestFail(t, "Statuscode != 200")
TestFailFmt(t, "Statuscode != 200 (actual = %d)", resp.StatusCode)
}
var data TResult