mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
SMQ-2761 - Add route field to channels (#2772)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
@@ -454,7 +454,7 @@ paths:
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/{domainID}/channels/{chanID}/connect:
|
||||
post:
|
||||
operationId: connectClientsToChannel
|
||||
@@ -534,14 +534,14 @@ components:
|
||||
type: string
|
||||
example: channelName
|
||||
description: Free-form channel name. Channel name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
example: long channel description
|
||||
description: Channel description, free form text.
|
||||
parent_id:
|
||||
type: string
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Id of parent channel, it must be existing channel.
|
||||
route:
|
||||
type: string
|
||||
example: channelRoute
|
||||
description: Channel route.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "location": "example" }
|
||||
@@ -557,7 +557,7 @@ components:
|
||||
ParentGroupReqObj:
|
||||
type: object
|
||||
properties:
|
||||
parent_group_id:
|
||||
parent_group_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
@@ -587,10 +587,10 @@ components:
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Channel parent identifier.
|
||||
description:
|
||||
route:
|
||||
type: string
|
||||
example: long channel description
|
||||
description: Channel description, free form text.
|
||||
example: channelRoute
|
||||
description: Channel route.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "role": "general" }
|
||||
@@ -655,10 +655,6 @@ components:
|
||||
type: string
|
||||
example: channelName
|
||||
description: Free-form channel name. Channel name is unique on the given hierarchy level.
|
||||
description:
|
||||
type: string
|
||||
example: long description but not too long
|
||||
description: Channel description, free form text.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "role": "general" }
|
||||
@@ -666,8 +662,7 @@ components:
|
||||
required:
|
||||
- name
|
||||
- metadata
|
||||
- description
|
||||
|
||||
|
||||
ChannelUpdateTags:
|
||||
type: object
|
||||
properties:
|
||||
@@ -817,7 +812,7 @@ components:
|
||||
format: uuid
|
||||
required: false
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
|
||||
Metadata:
|
||||
name: metadata
|
||||
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
|
||||
@@ -867,7 +862,7 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChannelReqObj"
|
||||
|
||||
|
||||
ChannelsCreateReq:
|
||||
description: JSON-formatted document describing the new channels to be registered
|
||||
required: true
|
||||
@@ -885,15 +880,15 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChannelUpdate"
|
||||
|
||||
|
||||
ChannelUpdateTagsReq:
|
||||
description: JSON-formated document describing the tags of channel to be updated.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChannelUpdate"
|
||||
|
||||
$ref: "#/components/schemas/ChannelUpdateTags"
|
||||
|
||||
ChannelParentGroupReq:
|
||||
description: JSON-formated document describing the parent group to be set to or removed from a channel.
|
||||
required: true
|
||||
@@ -909,7 +904,7 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConnectionReqSchema"
|
||||
|
||||
|
||||
ChannelConnReq:
|
||||
description: JSON-formatted document describing the new connection.
|
||||
required: true
|
||||
@@ -976,7 +971,7 @@ components:
|
||||
operationId: unassignGroupsFromChannel
|
||||
parameters:
|
||||
chanID: $response.body#/id
|
||||
|
||||
|
||||
ChannelsCreateRes:
|
||||
description: Registered new channels.
|
||||
headers:
|
||||
|
||||
@@ -73,6 +73,8 @@ func TestCreateChannelEndpoint(t *testing.T) {
|
||||
"name": "test",
|
||||
},
|
||||
}
|
||||
reqWithRoute := reqChannel
|
||||
reqWithRoute.Route = valid
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -97,6 +99,16 @@ func TestCreateChannelEndpoint(t *testing.T) {
|
||||
status: http.StatusCreated,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create channel with route",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
req: reqWithRoute,
|
||||
contentType: contentType,
|
||||
svcResp: []channels.Channel{validChannelResp},
|
||||
status: http.StatusCreated,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create channel with invalid token",
|
||||
token: invalidToken,
|
||||
@@ -140,6 +152,29 @@ func TestCreateChannelEndpoint(t *testing.T) {
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
{
|
||||
desc: "create channel with invalid route format",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
req: channels.Channel{
|
||||
Name: valid,
|
||||
Route: "__invalid",
|
||||
},
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "create channel with UUID route",
|
||||
token: validToken, domainID: validID,
|
||||
req: channels.Channel{
|
||||
Name: valid,
|
||||
Route: testsutil.GenerateUUID(t),
|
||||
},
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "create channel with invalid content type",
|
||||
token: validToken,
|
||||
@@ -205,6 +240,7 @@ func TestCreateChannelsEndpoint(t *testing.T) {
|
||||
Metadata: map[string]interface{}{
|
||||
"name": "test",
|
||||
},
|
||||
Route: valid,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -276,6 +312,33 @@ func TestCreateChannelsEndpoint(t *testing.T) {
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
{
|
||||
desc: "create channels with invalid route format",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
req: []channels.Channel{
|
||||
{
|
||||
Name: valid,
|
||||
Route: "__invalid",
|
||||
},
|
||||
},
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "create channel with UUID route",
|
||||
token: validToken, domainID: validID,
|
||||
req: []channels.Channel{
|
||||
{
|
||||
Name: valid,
|
||||
Route: testsutil.GenerateUUID(t),
|
||||
},
|
||||
},
|
||||
contentType: contentType,
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "create channels with invalid content type",
|
||||
token: validToken,
|
||||
|
||||
@@ -25,6 +25,14 @@ func (req createChannelReq) validate() error {
|
||||
return apiutil.ErrMissingChannelID
|
||||
}
|
||||
}
|
||||
if req.Channel.Route != "" {
|
||||
if err := api.ValidateRoute(req.Channel.Route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.ValidateUUID(req.Channel.Route); err == nil {
|
||||
return apiutil.ErrInvalidRouteFormat
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -46,6 +54,14 @@ func (req createChannelsReq) validate() error {
|
||||
if len(channel.Name) > api.MaxNameSize {
|
||||
return apiutil.ErrNameSize
|
||||
}
|
||||
if channel.Route != "" {
|
||||
if err := api.ValidateRoute(channel.Route); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.ValidateUUID(channel.Route); err == nil {
|
||||
return apiutil.ErrInvalidRouteFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -26,7 +26,8 @@ func TestCreateChannelReqValidation(t *testing.T) {
|
||||
desc: "valid request",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Name: valid,
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
@@ -35,11 +36,32 @@ func TestCreateChannelReqValidation(t *testing.T) {
|
||||
desc: "long name",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrNameSize,
|
||||
},
|
||||
{
|
||||
desc: "invalid route",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: "__invalid",
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "uuid as route",
|
||||
req: createChannelReq{
|
||||
Channel: channels.Channel{
|
||||
Name: valid,
|
||||
Route: testsutil.GenerateUUID(t),
|
||||
},
|
||||
},
|
||||
err: apiutil.ErrInvalidRouteFormat,
|
||||
},
|
||||
{
|
||||
desc: "missing channel ID",
|
||||
req: createChannelReq{
|
||||
@@ -68,7 +90,8 @@ func TestCreateChannelsReqValidation(t *testing.T) {
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
Name: valid,
|
||||
Name: valid,
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -79,7 +102,8 @@ func TestCreateChannelsReqValidation(t *testing.T) {
|
||||
req: createChannelsReq{
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Name: strings.Repeat("a", api.MaxNameSize+1),
|
||||
Route: valid,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ type Channel struct {
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
ParentGroup string `json:"parent_group_id,omitempty"`
|
||||
Domain string `json:"domain_id,omitempty"`
|
||||
Route string `json:"route,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
@@ -151,6 +152,9 @@ type Repository interface {
|
||||
// RetrieveByID retrieves the channel having the provided identifier
|
||||
RetrieveByID(ctx context.Context, id string) (Channel, error)
|
||||
|
||||
// RetrieveByRoute retrieves the channel having the provided route
|
||||
RetrieveByRoute(ctx context.Context, route, domainID string) (Channel, error)
|
||||
|
||||
// RetrieveByIDWithRoles retrieves channel by its unique ID along with member roles.
|
||||
RetrieveByIDWithRoles(ctx context.Context, id, memberID string) (Channel, error)
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ func (cce createChannelEvent) Encode() (map[string]interface{}, error) {
|
||||
"operation": channelCreate,
|
||||
"id": cce.ID,
|
||||
"roles_provisioned": cce.rolesProvisioned,
|
||||
"route": cce.Route,
|
||||
"status": cce.Status.String(),
|
||||
"created_at": cce.CreatedAt,
|
||||
"domain": cce.DomainID,
|
||||
@@ -97,6 +98,9 @@ func (uce updateChannelEvent) Encode() (map[string]interface{}, error) {
|
||||
if uce.ID != "" {
|
||||
val["id"] = uce.ID
|
||||
}
|
||||
if uce.Route != "" {
|
||||
val["route"] = uce.Route
|
||||
}
|
||||
if uce.Name != "" {
|
||||
val["name"] = uce.Name
|
||||
}
|
||||
@@ -161,6 +165,9 @@ func (vce viewChannelEvent) Encode() (map[string]interface{}, error) {
|
||||
if vce.Name != "" {
|
||||
val["name"] = vce.Name
|
||||
}
|
||||
if vce.Route != "" {
|
||||
val["route"] = vce.Route
|
||||
}
|
||||
if len(vce.Tags) > 0 {
|
||||
val["tags"] = vce.Tags
|
||||
}
|
||||
|
||||
@@ -1063,6 +1063,62 @@ func (_c *Repository_RetrieveByIDWithRoles_Call) RunAndReturn(run func(ctx conte
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveByRoute provides a mock function for the type Repository
|
||||
func (_mock *Repository) RetrieveByRoute(ctx context.Context, route string, domainID string) (channels.Channel, error) {
|
||||
ret := _mock.Called(ctx, route, domainID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveByRoute")
|
||||
}
|
||||
|
||||
var r0 channels.Channel
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (channels.Channel, error)); ok {
|
||||
return returnFunc(ctx, route, domainID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) channels.Channel); ok {
|
||||
r0 = returnFunc(ctx, route, domainID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(channels.Channel)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, route, domainID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_RetrieveByRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByRoute'
|
||||
type Repository_RetrieveByRoute_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveByRoute is a helper method to define mock.On call
|
||||
// - ctx
|
||||
// - route
|
||||
// - domainID
|
||||
func (_e *Repository_Expecter) RetrieveByRoute(ctx interface{}, route interface{}, domainID interface{}) *Repository_RetrieveByRoute_Call {
|
||||
return &Repository_RetrieveByRoute_Call{Call: _e.mock.On("RetrieveByRoute", ctx, route, domainID)}
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveByRoute_Call) Run(run func(ctx context.Context, route string, domainID string)) *Repository_RetrieveByRoute_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveByRoute_Call) Return(channel channels.Channel, err error) *Repository_RetrieveByRoute_Call {
|
||||
_c.Call.Return(channel, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveByRoute_Call) RunAndReturn(run func(ctx context.Context, route string, domainID string) (channels.Channel, error)) *Repository_RetrieveByRoute_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveEntitiesRolesActionsMembers provides a mock function for the type Repository
|
||||
func (_mock *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) {
|
||||
ret := _mock.Called(ctx, entityIDs)
|
||||
|
||||
@@ -58,9 +58,9 @@ func (cr *channelRepository) Save(ctx context.Context, chs ...channels.Channel)
|
||||
dbchs = append(dbchs, dbch)
|
||||
}
|
||||
|
||||
q := `INSERT INTO channels (id, name, tags, domain_id, parent_group_id, metadata, created_at, updated_at, updated_by, status)
|
||||
VALUES (:id, :name, :tags, :domain_id, :parent_group_id, :metadata, :created_at, :updated_at, :updated_by, :status)
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`
|
||||
q := `INSERT INTO channels (id, name, tags, domain_id, parent_group_id, route, metadata, created_at, updated_at, updated_by, status)
|
||||
VALUES (:id, :name, :tags, :domain_id, :parent_group_id, :route, :metadata, :created_at, :updated_at, :updated_by, :status)
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbchs)
|
||||
if err != nil {
|
||||
@@ -100,7 +100,7 @@ func (cr *channelRepository) Update(ctx context.Context, channel channels.Channe
|
||||
}
|
||||
q := fmt.Sprintf(`UPDATE channels SET %s updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE id = :id AND status = :status
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`,
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`,
|
||||
upq)
|
||||
channel.Status = channels.EnabledStatus
|
||||
return cr.update(ctx, channel, q)
|
||||
@@ -109,7 +109,7 @@ func (cr *channelRepository) Update(ctx context.Context, channel channels.Channe
|
||||
func (cr *channelRepository) UpdateTags(ctx context.Context, channel channels.Channel) (channels.Channel, error) {
|
||||
q := `UPDATE channels SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE id = :id AND status = :status
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`
|
||||
channel.Status = channels.EnabledStatus
|
||||
return cr.update(ctx, channel, q)
|
||||
}
|
||||
@@ -117,13 +117,13 @@ func (cr *channelRepository) UpdateTags(ctx context.Context, channel channels.Ch
|
||||
func (cr *channelRepository) ChangeStatus(ctx context.Context, channel channels.Channel) (channels.Channel, error) {
|
||||
q := `UPDATE channels SET status = :status, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE id = :id
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`
|
||||
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`
|
||||
|
||||
return cr.update(ctx, channel, q)
|
||||
}
|
||||
|
||||
func (cr *channelRepository) RetrieveByID(ctx context.Context, id string) (channels.Channel, error) {
|
||||
q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, metadata, created_at, updated_at, updated_by, status FROM channels WHERE id = :id`
|
||||
q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, metadata, created_at, updated_at, updated_by, status FROM channels WHERE id = :id`
|
||||
|
||||
dbch := dbChannel{
|
||||
ID: id,
|
||||
@@ -146,6 +146,32 @@ func (cr *channelRepository) RetrieveByID(ctx context.Context, id string) (chann
|
||||
return channels.Channel{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
func (cr *channelRepository) RetrieveByRoute(ctx context.Context, route, domainID string) (channels.Channel, error) {
|
||||
q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, metadata, created_at, updated_at, updated_by, status
|
||||
FROM channels WHERE route = :route AND domain_id = :domain_id`
|
||||
|
||||
dbch := dbChannel{
|
||||
Route: toNullString(route),
|
||||
Domain: domainID,
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbch)
|
||||
if err != nil {
|
||||
return channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
dbch = dbChannel{}
|
||||
if row.Next() {
|
||||
if err := row.StructScan(&dbch); err != nil {
|
||||
return channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
return toChannel(dbch)
|
||||
}
|
||||
|
||||
return channels.Channel{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
func (cr *channelRepository) RetrieveByIDWithRoles(ctx context.Context, id, memberID string) (channels.Channel, error) {
|
||||
query := `
|
||||
WITH selected_channel AS (
|
||||
@@ -311,6 +337,7 @@ func (cr *channelRepository) RetrieveByIDWithRoles(ctx context.Context, id, memb
|
||||
c2.tags,
|
||||
COALESCE(c2.domain_id, '') AS domain_id,
|
||||
COALESCE(c2.parent_group_id, '') AS parent_group_id,
|
||||
c2.route,
|
||||
c2.metadata,
|
||||
c2.created_at,
|
||||
c2.created_by,
|
||||
@@ -381,6 +408,7 @@ func (cr *channelRepository) RetrieveAll(ctx context.Context, pm channels.Page)
|
||||
c.metadata,
|
||||
COALESCE(c.domain_id, '') AS domain_id,
|
||||
COALESCE(parent_group_id, '') AS parent_group_id,
|
||||
c.route,
|
||||
COALESCE((SELECT path FROM groups WHERE id = c.parent_group_id), ''::::ltree) AS parent_group_path,
|
||||
c.status,
|
||||
c.created_by,
|
||||
@@ -488,6 +516,7 @@ func (repo *channelRepository) retrieveChannels(ctx context.Context, domainID, u
|
||||
c.name,
|
||||
c.domain_id,
|
||||
c.parent_group_id,
|
||||
c.route,
|
||||
c.tags,
|
||||
c.metadata,
|
||||
c.created_by,
|
||||
@@ -546,6 +575,7 @@ func (repo *channelRepository) retrieveChannels(ctx context.Context, domainID, u
|
||||
c.name,
|
||||
c.domain_id,
|
||||
c.parent_group_id,
|
||||
c.route,
|
||||
c.tags,
|
||||
c.metadata,
|
||||
c.created_by,
|
||||
@@ -592,6 +622,7 @@ WITH direct_channels AS (
|
||||
c.name,
|
||||
c.domain_id,
|
||||
c.parent_group_id,
|
||||
c.route,
|
||||
c.tags,
|
||||
c.metadata,
|
||||
c.created_by,
|
||||
@@ -749,6 +780,7 @@ groups_channels AS (
|
||||
c.name,
|
||||
c.domain_id,
|
||||
c.parent_group_id,
|
||||
c.route,
|
||||
c.tags,
|
||||
c.metadata,
|
||||
c.created_by,
|
||||
@@ -780,6 +812,7 @@ final_channels AS (
|
||||
gc."name",
|
||||
gc.domain_id,
|
||||
gc.parent_group_id,
|
||||
gc.route,
|
||||
gc.tags,
|
||||
gc.metadata,
|
||||
gc.created_by,
|
||||
@@ -804,6 +837,7 @@ final_channels AS (
|
||||
dc."name",
|
||||
dc.domain_id,
|
||||
dc.parent_group_id,
|
||||
dc.route,
|
||||
dc.tags,
|
||||
dc.metadata,
|
||||
dc.created_by,
|
||||
@@ -1075,6 +1109,7 @@ type dbChannel struct {
|
||||
ParentGroup sql.NullString `db:"parent_group_id,omitempty"`
|
||||
Tags pgtype.TextArray `db:"tags,omitempty"`
|
||||
Domain string `db:"domain_id"`
|
||||
Route sql.NullString `db:"route,omitempty"`
|
||||
Metadata []byte `db:"metadata,omitempty"`
|
||||
CreatedBy *string `db:"created_by,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at,omitempty"`
|
||||
@@ -1125,6 +1160,7 @@ func toDBChannel(ch channels.Channel) (dbChannel, error) {
|
||||
Name: ch.Name,
|
||||
ParentGroup: toNullString(ch.ParentGroup),
|
||||
Domain: ch.Domain,
|
||||
Route: toNullString(ch.Route),
|
||||
Tags: tags,
|
||||
Metadata: data,
|
||||
CreatedBy: createdBy,
|
||||
@@ -1198,6 +1234,7 @@ func toChannel(ch dbChannel) (channels.Channel, error) {
|
||||
Name: ch.Name,
|
||||
Tags: tags,
|
||||
Domain: ch.Domain,
|
||||
Route: toString(ch.Route),
|
||||
ParentGroup: toString(ch.ParentGroup),
|
||||
Metadata: metadata,
|
||||
CreatedBy: createdBy,
|
||||
|
||||
@@ -29,6 +29,7 @@ var (
|
||||
Domain: testsutil.GenerateUUID(&testing.T{}),
|
||||
ParentGroup: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: namegen.Generate(),
|
||||
Route: testsutil.GenerateUUID(&testing.T{}),
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Metadata: map[string]interface{}{"key": "value"},
|
||||
CreatedAt: time.Now().UTC().Truncate(time.Microsecond),
|
||||
@@ -54,6 +55,19 @@ func TestSave(t *testing.T) {
|
||||
|
||||
duplicateChannelID := testsutil.GenerateUUID(t)
|
||||
|
||||
duplicateRoute := testsutil.GenerateUUID(t)
|
||||
duplicateDomain := testsutil.GenerateUUID(t)
|
||||
|
||||
duplicateChannel := channels.Channel{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Domain: duplicateDomain,
|
||||
Name: namegen.Generate(),
|
||||
Route: duplicateRoute,
|
||||
}
|
||||
|
||||
_, err := repo.Save(context.Background(), duplicateChannel)
|
||||
require.Nil(t, err, fmt.Sprintf("save channel unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
channel channels.Channel
|
||||
@@ -149,6 +163,17 @@ func TestSave(t *testing.T) {
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add channel with duplicate route",
|
||||
channel: channels.Channel{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Domain: duplicateDomain,
|
||||
Name: namegen.Generate(),
|
||||
Route: duplicateRoute,
|
||||
},
|
||||
resp: []channels.Channel{},
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -183,6 +208,7 @@ func TestUpdate(t *testing.T) {
|
||||
channel: channels.Channel{
|
||||
ID: validChannel.ID,
|
||||
Name: namegen.Generate(),
|
||||
Route: testsutil.GenerateUUID(t),
|
||||
Metadata: map[string]interface{}{"key": "value"},
|
||||
UpdatedAt: validTimestamp,
|
||||
UpdatedBy: testsutil.GenerateUUID(t),
|
||||
@@ -330,6 +356,7 @@ func TestChangeStatus(t *testing.T) {
|
||||
disabledChannel := validChannel
|
||||
disabledChannel.ID = testsutil.GenerateUUID(t)
|
||||
disabledChannel.Name = namegen.Generate()
|
||||
disabledChannel.Route = testsutil.GenerateUUID(t)
|
||||
disabledChannel.Status = channels.DisabledStatus
|
||||
|
||||
_, err := repo.Save(context.Background(), validChannel, disabledChannel)
|
||||
@@ -442,6 +469,69 @@ func TestRetrieveByID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByRoute(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM channels")
|
||||
require.Nil(t, err, fmt.Sprintf("clean channels unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewRepository(database)
|
||||
|
||||
_, err := repo.Save(context.Background(), validChannel)
|
||||
require.Nil(t, err, fmt.Sprintf("save channel unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
route string
|
||||
domainID string
|
||||
resp channels.Channel
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve channel by route successfully",
|
||||
route: validChannel.Route,
|
||||
domainID: validChannel.Domain,
|
||||
resp: validChannel,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve channel by id with invalid route",
|
||||
route: "invalid-route",
|
||||
domainID: validChannel.Domain,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve channel by id with empty route",
|
||||
route: "",
|
||||
domainID: validChannel.Domain,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve channel by id with invalid domain",
|
||||
route: validChannel.Route,
|
||||
domainID: "invalid-domain",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve channel by id with empty domain",
|
||||
route: validChannel.Route,
|
||||
domainID: "",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
channel, err := repo.RetrieveByRoute(context.Background(), tc.route, tc.domainID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.resp, channel, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, channel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM channels")
|
||||
@@ -460,6 +550,7 @@ func TestRetrieveAll(t *testing.T) {
|
||||
Domain: testsutil.GenerateUUID(t),
|
||||
ParentGroup: parentID,
|
||||
Name: name,
|
||||
Route: testsutil.GenerateUUID(t),
|
||||
Metadata: map[string]interface{}{"name": name},
|
||||
CreatedAt: time.Now().UTC().Truncate(time.Microsecond),
|
||||
Status: channels.EnabledStatus,
|
||||
|
||||
@@ -62,6 +62,17 @@ func Migration() (*migrate.MemoryMigrationSource, error) {
|
||||
`ALTER TABLE channels ADD CONSTRAINT channels_domain_id_name_key UNIQUE (domain_id, name)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "channels_03",
|
||||
Up: []string{
|
||||
`ALTER TABLE channels ADD COLUMN route VARCHAR(36);`,
|
||||
`CREATE UNIQUE INDEX unique_domain_route_not_null ON channels (domain_id, route) WHERE route IS NOT NULL;`,
|
||||
},
|
||||
Down: []string{
|
||||
`DROP INDEX IF EXISTS unique_domain_route_not_null;`,
|
||||
`ALTER TABLE channels DROP COLUMN route;`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
channelsMigration.Migrations = append(channelsMigration.Migrations, rolesMigration.Migrations...)
|
||||
|
||||
@@ -36,8 +36,9 @@ var (
|
||||
idProvider = uuid.New()
|
||||
namegen = namegenerator.NewGenerator()
|
||||
validChannel = channels.Channel{
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: namegen.Generate(),
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: namegen.Generate(),
|
||||
Route: namegen.Generate(),
|
||||
Metadata: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -46,8 +47,9 @@ var (
|
||||
Status: channels.EnabledStatus,
|
||||
}
|
||||
validChannelWithRoles = channels.Channel{
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: namegen.Generate(),
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: namegen.Generate(),
|
||||
Route: namegen.Generate(),
|
||||
Metadata: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -91,6 +93,9 @@ func newService(t *testing.T) channels.Service {
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService(t)
|
||||
|
||||
etChan := validChannel
|
||||
etChan.Route = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
channel channels.Channel
|
||||
@@ -292,8 +297,9 @@ func TestUpdateChannel(t *testing.T) {
|
||||
{
|
||||
desc: "update channel successfully",
|
||||
channel: channels.Channel{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namegen.Generate(),
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Name: namegen.Generate(),
|
||||
Route: namegen.Generate(),
|
||||
},
|
||||
repoResp: validChannel,
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ type Channel struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Route string `json:"route,omitempty"`
|
||||
ParentGroup string `json:"parent_group_id,omitempty"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
|
||||
@@ -54,12 +54,14 @@ func TestCreateChannel(t *testing.T) {
|
||||
|
||||
createChannelReq := channels.Channel{
|
||||
Name: channel.Name,
|
||||
Route: channel.Route,
|
||||
Metadata: channels.Metadata{"role": "client"},
|
||||
Status: channels.EnabledStatus,
|
||||
}
|
||||
|
||||
channelReq := sdk.Channel{
|
||||
Name: channel.Name,
|
||||
Route: channel.Route,
|
||||
Metadata: validMetadata,
|
||||
Status: channels.EnabledStatus.String(),
|
||||
}
|
||||
@@ -134,6 +136,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
desc: "create channel with parent group",
|
||||
channelReq: sdk.Channel{
|
||||
Name: channel.Name,
|
||||
Route: channel.Route,
|
||||
ParentGroup: parentID,
|
||||
Status: channels.EnabledStatus.String(),
|
||||
},
|
||||
@@ -142,6 +145,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
createChannelReq: channels.Channel{
|
||||
Name: channel.Name,
|
||||
ParentGroup: parentID,
|
||||
Route: channel.Route,
|
||||
Status: channels.EnabledStatus,
|
||||
},
|
||||
svcRes: []channels.Channel{convertChannel(pChannel)},
|
||||
@@ -153,6 +157,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
desc: "create channel with invalid parent",
|
||||
channelReq: sdk.Channel{
|
||||
Name: channel.Name,
|
||||
Route: channel.Route,
|
||||
ParentGroup: wrongID,
|
||||
Status: channels.EnabledStatus.String(),
|
||||
},
|
||||
@@ -161,6 +166,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
createChannelReq: channels.Channel{
|
||||
Name: channel.Name,
|
||||
ParentGroup: wrongID,
|
||||
Route: channel.Route,
|
||||
Status: channels.EnabledStatus,
|
||||
},
|
||||
svcRes: []channels.Channel{},
|
||||
@@ -173,6 +179,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
channelReq: sdk.Channel{
|
||||
ID: channel.ID,
|
||||
ParentGroup: parentID,
|
||||
Route: channel.Route,
|
||||
Name: channel.Name,
|
||||
Metadata: validMetadata,
|
||||
CreatedAt: channel.CreatedAt,
|
||||
@@ -184,6 +191,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
createChannelReq: channels.Channel{
|
||||
ID: channel.ID,
|
||||
ParentGroup: parentID,
|
||||
Route: channel.Route,
|
||||
Name: channel.Name,
|
||||
Metadata: channels.Metadata{"role": "client"},
|
||||
CreatedAt: channel.CreatedAt,
|
||||
@@ -294,8 +302,9 @@ func TestCreateChannels(t *testing.T) {
|
||||
token: validToken,
|
||||
channelsReq: []sdk.Channel{
|
||||
{
|
||||
ID: generateUUID(t),
|
||||
Name: "channel_1",
|
||||
ID: generateUUID(t),
|
||||
Name: "channel_1",
|
||||
Route: valid,
|
||||
Metadata: map[string]interface{}{
|
||||
"test": make(chan int),
|
||||
},
|
||||
@@ -2117,6 +2126,7 @@ func generateTestChannel(t *testing.T) sdk.Channel {
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
DomainID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: channelName,
|
||||
Route: valid,
|
||||
Metadata: sdk.Metadata{"role": "client"},
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
|
||||
@@ -213,6 +213,7 @@ func convertChannel(g sdk.Channel) mgchannels.Channel {
|
||||
Name: g.Name,
|
||||
Tags: g.Tags,
|
||||
ParentGroup: g.ParentGroup,
|
||||
Route: g.Route,
|
||||
Domain: g.DomainID,
|
||||
Metadata: channels.Metadata(g.Metadata),
|
||||
CreatedAt: g.CreatedAt,
|
||||
|
||||
Reference in New Issue
Block a user