opcotech/elemo

View on GitHub
internal/repository/neo4j/role.go

Summary

Maintainability
B
4 hrs
Test Coverage
package neo4j

import (
    "context"
    "errors"
    "time"

    "github.com/neo4j/neo4j-go-driver/v5/neo4j"

    "github.com/opcotech/elemo/internal/model"
    "github.com/opcotech/elemo/internal/repository"
)

// RoleRepository is a repository for managing roles.
type RoleRepository struct {
    *baseRepository
}

func (r *RoleRepository) scan(rp, mp, pp string) func(rec *neo4j.Record) (*model.Role, error) {
    return func(rec *neo4j.Record) (*model.Role, error) {
        role := new(model.Role)

        val, _, err := neo4j.GetRecordValue[neo4j.Node](rec, rp)
        if err != nil {
            return nil, err
        }

        if err := ScanIntoStruct(&val, &role, []string{"id"}); err != nil {
            return nil, err
        }

        role.ID, _ = model.NewIDFromString(val.GetProperties()["id"].(string), model.ResourceTypeRole.String())

        if role.Members, err = ParseIDsFromRecord(rec, mp, model.ResourceTypeUser.String()); err != nil {
            return nil, err
        }

        if role.Permissions, err = ParseIDsFromRecord(rec, pp, model.ResourceTypePermission.String()); err != nil {
            return nil, err
        }

        if err := role.Validate(); err != nil {
            return nil, err
        }

        return role, nil
    }
}

func (r *RoleRepository) Create(ctx context.Context, createdBy, belongsTo model.ID, role *model.Role) error {
    ctx, span := r.tracer.Start(ctx, "repository.neo4j.RoleRepository/Create")
    defer span.End()

    if err := belongsTo.Validate(); err != nil {
        return errors.Join(repository.ErrRoleCreate, err)
    }

    if err := role.Validate(); err != nil {
        return errors.Join(repository.ErrRoleCreate, err)
    }

    createdAt := time.Now().UTC()

    role.ID = model.MustNewID(model.ResourceTypeRole)
    role.CreatedAt = &createdAt
    role.UpdatedAt = nil

    cypher := `
    MATCH (u:` + createdBy.Label() + ` {id: $owner_id}), (b:` + belongsTo.Label() + ` {id: $belongs_to_id})
    MERGE (r:` + role.ID.Label() + ` {id: $role_id})
    ON CREATE SET r += { name: $name, description: $description, created_at: datetime($created_at) }
    CREATE (r)<-[:` + EdgeKindHasTeam.String() + ` { id: $has_team_id, created_at: datetime($created_at) }]-(b)
    CREATE (u)-[:` + EdgeKindMemberOf.String() + ` { id: $membership_id, created_at: datetime($created_at) }]->(r)
    MERGE (u)-[p:` + EdgeKindHasPermission.String() + ` {id: $perm_id, kind: $perm_kind}]->(r) ON CREATE SET p.created_at = datetime($created_at)
    `

    params := map[string]any{
        "owner_id":      createdBy.String(),
        "belongs_to_id": belongsTo.String(),
        "role_id":       role.ID.String(),
        "membership_id": model.NewRawID(),
        "has_team_id":   model.NewRawID(),
        "perm_id":       model.NewRawID(),
        "perm_kind":     model.PermissionKindAll.String(),
        "name":          role.Name,
        "description":   role.Description,
        "created_at":    createdAt.Format(time.RFC3339Nano),
    }

    if err := ExecuteWriteAndConsume(ctx, r.db, cypher, params); err != nil {
        return errors.Join(err, repository.ErrRoleCreate)
    }

    return nil
}

func (r *RoleRepository) Get(ctx context.Context, id, belongsTo model.ID) (*model.Role, error) {
    ctx, span := r.tracer.Start(ctx, "repository.neo4j.RoleRepository/Get")
    defer span.End()

    cypher := `
    MATCH (r:` + id.Label() + ` {id: $id}), (b:` + belongsTo.Label() + ` {id: $belongs_to_id})
    OPTIONAL MATCH (r)<-[:` + EdgeKindMemberOf.String() + `]-(u:` + model.ResourceTypeUser.String() + `)
    OPTIONAL MATCH (r)-[p:` + EdgeKindHasPermission.String() + `]->()
    RETURN r, collect(DISTINCT u.id) AS m, collect(DISTINCT p.id) AS p
    `

    params := map[string]any{
        "id":            id.String(),
        "belongs_to_id": belongsTo.String(),
    }

    role, err := ExecuteReadAndReadSingle(ctx, r.db, cypher, params, r.scan("r", "m", "p"))
    if err != nil {
        return nil, errors.Join(err, repository.ErrRoleRead)
    }

    return role, nil
}

func (r *RoleRepository) GetAllBelongsTo(ctx context.Context, belongsTo model.ID, offset, limit int) ([]*model.Role, error) {
    ctx, span := r.tracer.Start(ctx, "repository.neo4j.RoleRepository/GetAllBelongsTo")
    defer span.End()

    if err := belongsTo.Validate(); err != nil {
        return nil, errors.Join(repository.ErrRoleRead, err)
    }

    cypher := `
    MATCH (r:` + model.ResourceTypeRole.String() + `)<-[:` + EdgeKindHasTeam.String() + `]-(:` + belongsTo.Label() + ` {id: $id})
    OPTIONAL MATCH (r)<-[:` + EdgeKindMemberOf.String() + `]-(u:` + model.ResourceTypeUser.String() + `)
    OPTIONAL MATCH (r)-[p:` + EdgeKindHasPermission.String() + `]->()
    RETURN r, collect(DISTINCT u.id) AS m, collect(DISTINCT p.id) AS p
    ORDER BY r.created_at DESC
    SKIP $offset LIMIT $limit`

    params := map[string]any{
        "id":     belongsTo.String(),
        "offset": offset,
        "limit":  limit,
    }

    roles, err := ExecuteReadAndReadAll(ctx, r.db, cypher, params, r.scan("r", "m", "p"))
    if err != nil {
        return nil, errors.Join(repository.ErrRoleRead, err)
    }

    return roles, nil
}

func (r *RoleRepository) Update(ctx context.Context, id, belongsTo model.ID, patch map[string]any) (*model.Role, error) {
    ctx, span := r.tracer.Start(ctx, "repository.neo4j.RoleRepository/Update")
    defer span.End()

    cypher := `
    MATCH (r:` + id.Label() + ` {id: $id}), (b:` + belongsTo.Label() + ` {id: $belongs_to_id})
    SET r += $patch, r.updated_at = datetime()
    WITH r
    OPTIONAL MATCH (r)<-[:` + EdgeKindMemberOf.String() + `]-(u:` + model.ResourceTypeUser.String() + `)
    OPTIONAL MATCH (r)-[p:` + EdgeKindHasPermission.String() + `]->()
    RETURN r, collect(DISTINCT u.id) AS m, collect(DISTINCT p.id) AS p`

    params := map[string]any{
        "id":            id.String(),
        "belongs_to_id": belongsTo.String(),
        "patch":         patch,
    }

    role, err := ExecuteWriteAndReadSingle(ctx, r.db, cypher, params, r.scan("r", "m", "p"))
    if err != nil {
        return nil, errors.Join(err, repository.ErrRoleUpdate)
    }

    return role, nil
}

func (r *RoleRepository) AddMember(ctx context.Context, roleID, memberID, belongsToID model.ID) error {
    ctx, span := r.tracer.Start(ctx, "repository.neo4j.RoleRepository/AddMember")
    defer span.End()

    cypher := `
    MATCH (r:` + roleID.Label() + ` {id: $role_id}), (u:` + memberID.Label() + ` {id: $member_id}), (b:` + belongsToID.Label() + ` {id: $belongs_to_id})
    MERGE (u)-[m:` + EdgeKindMemberOf.String() + `]->(r)
    ON CREATE SET m.created_at = datetime($now), m.id = $membership_id
    ON MATCH SET m.updated_at = datetime($now)`

    params := map[string]any{
        "role_id":       roleID.String(),
        "member_id":     memberID.String(),
        "belongs_to_id": belongsToID.String(),
        "membership_id": model.NewRawID(),
        "now":           time.Now().UTC().Format(time.RFC3339Nano),
    }

    if err := ExecuteWriteAndConsume(ctx, r.db, cypher, params); err != nil {
        return errors.Join(repository.ErrRoleAddMember, err)
    }

    return nil
}

func (r *RoleRepository) RemoveMember(ctx context.Context, roleID, memberID, belongsToID model.ID) error {
    ctx, span := r.tracer.Start(ctx, "repository.neo4j.RoleRepository/RemoveMember")
    defer span.End()

    cypher := `
    MATCH (:` + roleID.Label() + ` {id: $role_id})<-[r:` + EdgeKindMemberOf.String() + `]-(:` + memberID.Label() + ` {id: $member_id}), (b:` + belongsToID.Label() + ` {id: $belongs_to_id})
    DELETE r`

    params := map[string]any{
        "role_id":       roleID.String(),
        "member_id":     memberID.String(),
        "belongs_to_id": belongsToID.String(),
    }

    if err := ExecuteWriteAndConsume(ctx, r.db, cypher, params); err != nil {
        return errors.Join(repository.ErrRoleRemoveMember, err)
    }

    return nil
}

func (r *RoleRepository) Delete(ctx context.Context, id, belongsTo model.ID) error {
    ctx, span := r.tracer.Start(ctx, "repository.neo4j.RoleRepository/Delete")
    defer span.End()

    cypher := `MATCH (r:` + id.Label() + ` {id: $id}), (b:` + belongsTo.Label() + ` {id: $belongs_to_id}) DETACH DELETE r`
    params := map[string]any{
        "id":            id.String(),
        "belongs_to_id": belongsTo.String(),
    }

    if err := ExecuteWriteAndConsume(ctx, r.db, cypher, params); err != nil {
        return errors.Join(repository.ErrRoleDelete, err)
    }

    return nil
}

// NewRoleRepository creates a new role baseRepository.
func NewRoleRepository(opts ...RepositoryOption) (*RoleRepository, error) {
    baseRepo, err := newRepository(opts...)
    if err != nil {
        return nil, err
    }

    return &RoleRepository{
        baseRepository: baseRepo,
    }, nil
}