Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): move export csv items and GeoJSON to usecase layer #1329

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
95 changes: 8 additions & 87 deletions server/internal/adapter/integration/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package integration
import (
"context"
"errors"
"io"

"github.com/reearth/reearth-cms/server/internal/usecase"
"github.com/reearth/reearth-cms/server/pkg/model"
Expand Down Expand Up @@ -74,55 +73,32 @@ func (s *Server) ItemsAsGeoJSON(ctx context.Context, request ItemsAsGeoJSONReque
op := adapter.Operator(ctx)
uc := adapter.Usecases(ctx)

sp, err := uc.Schema.FindByModel(ctx, request.ModelId, op)
if err != nil {
return ItemsAsGeoJSON400Response{}, err
}

p := fromPagination(request.Params.Page, request.Params.PerPage)
items, _, err := uc.Item.FindBySchema(ctx, sp.Schema().ID(), nil, p, op)
featureCollections, err := uc.Item.ItemsAsGeoJSON(ctx, request.ModelId, request.Params.Page, request.Params.PerPage, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsAsGeoJSON404Response{}, err
}
return ItemsAsGeoJSON400Response{}, err
}

fc, err := featureCollectionFromItems(items, sp.Schema())
if err != nil {
return ItemsAsGeoJSON400Response{}, err
}

return ItemsAsGeoJSON200JSONResponse{
Features: fc.Features,
Type: fc.Type,
Features: featureCollections.Features,
Type: featureCollections.Type,
}, nil
}

func (s *Server) ItemsAsCSV(ctx context.Context, request ItemsAsCSVRequestObject) (ItemsAsCSVResponseObject, error) {
op := adapter.Operator(ctx)
uc := adapter.Usecases(ctx)

sp, err := uc.Schema.FindByModel(ctx, request.ModelId, op)
if err != nil {
return ItemsAsCSV400Response{}, err
}

p := fromPagination(request.Params.Page, request.Params.PerPage)
items, _, err := uc.Item.FindBySchema(ctx, sp.Schema().ID(), nil, p, op)
pr, err := uc.Item.ItemsAsCSV(ctx, request.ModelId, request.Params.Page, request.Params.PerPage, op)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me!
Just a quick note: we have two functions that handle exporting items as CSV. You've already completed ItemsAsCSV, so please apply the same approach to the ItemsWithProjectAsCSV function. Once that's done, kindly remove the csvFromItems function from the adapter level.

if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsAsCSV404Response{}, err
}
return ItemsAsCSV400Response{}, err
}

pr, pw := io.Pipe()
err = csvFromItems(pw, items, sp.Schema())
if err != nil {
return ItemsAsCSV400Response{}, err
}

return ItemsAsCSV200TextcsvResponse{
Body: pr,
}, nil
Expand Down Expand Up @@ -201,87 +177,32 @@ func (s *Server) ItemsWithProjectAsGeoJSON(ctx context.Context, request ItemsWit
op := adapter.Operator(ctx)
uc := adapter.Usecases(ctx)

prj, err := uc.Project.FindByIDOrAlias(ctx, request.ProjectIdOrAlias, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsGeoJSON404Response{}, err
}
return ItemsWithProjectAsGeoJSON400Response{}, err
}

m, err := uc.Model.FindByIDOrKey(ctx, prj.ID(), request.ModelIdOrKey, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsGeoJSON404Response{}, err
}
return ItemsWithProjectAsGeoJSON400Response{}, err
}

sp, err := uc.Schema.FindByModel(ctx, m.ID(), op)
if err != nil {
return ItemsWithProjectAsGeoJSON400Response{}, err
}

p := fromPagination(request.Params.Page, request.Params.PerPage)
items, _, err := uc.Item.FindBySchema(ctx, sp.Schema().ID(), nil, p, op)
featureCollection, err := uc.Item.ItemsWithProjectAsGeoJSON(ctx, request.ProjectIdOrAlias, request.ModelIdOrKey, request.Params.Page, request.Params.PerPage, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsGeoJSON404Response{}, err
}
return ItemsWithProjectAsGeoJSON400Response{}, err
}

fc, err := featureCollectionFromItems(items, sp.Schema())
if err != nil {
return ItemsWithProjectAsGeoJSON400Response{}, err
}

return ItemsWithProjectAsGeoJSON200JSONResponse{
Features: fc.Features,
Type: fc.Type,
Features: featureCollection.Features,
Type: featureCollection.Type,
}, nil
}

func (s *Server) ItemsWithProjectAsCSV(ctx context.Context, request ItemsWithProjectAsCSVRequestObject) (ItemsWithProjectAsCSVResponseObject, error) {
op := adapter.Operator(ctx)
uc := adapter.Usecases(ctx)

prj, err := uc.Project.FindByIDOrAlias(ctx, request.ProjectIdOrAlias, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsCSV404Response{}, err
}
return ItemsWithProjectAsCSV400Response{}, err
}

m, err := uc.Model.FindByIDOrKey(ctx, prj.ID(), request.ModelIdOrKey, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsCSV404Response{}, err
}
return ItemsWithProjectAsCSV400Response{}, err
}

sp, err := uc.Schema.FindByModel(ctx, m.ID(), op)
if err != nil {
return ItemsWithProjectAsCSV400Response{}, err
}

p := fromPagination(request.Params.Page, request.Params.PerPage)
items, _, err := uc.Item.FindBySchema(ctx, sp.Schema().ID(), nil, p, op)
pr, err := uc.Item.ItemsWithProjectAsCSV(ctx, request.ProjectIdOrAlias, request.ModelIdOrKey, request.Params.Page, request.Params.PerPage, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsCSV404Response{}, err
}
return ItemsWithProjectAsCSV400Response{}, err
}

pr, pw := io.Pipe()
err = csvFromItems(pw, items, sp.Schema())
if err != nil {
return ItemsWithProjectAsCSV400Response{}, err
}

return ItemsWithProjectAsCSV200TextcsvResponse{
Body: pr,
}, nil
Expand Down
212 changes: 212 additions & 0 deletions server/internal/usecase/interactor/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import (
"context"
"errors"
"fmt"
"io"
"time"

"github.com/reearth/reearth-cms/server/internal/usecase"
"github.com/reearth/reearth-cms/server/internal/usecase/gateway"
"github.com/reearth/reearth-cms/server/internal/usecase/interfaces"
"github.com/reearth/reearth-cms/server/internal/usecase/repo"
"github.com/reearth/reearth-cms/server/pkg/event"
"github.com/reearth/reearth-cms/server/pkg/group"
"github.com/reearth/reearth-cms/server/pkg/id"
"github.com/reearth/reearth-cms/server/pkg/integrationapi"
"github.com/reearth/reearth-cms/server/pkg/item"
"github.com/reearth/reearth-cms/server/pkg/model"
"github.com/reearth/reearth-cms/server/pkg/project"
"github.com/reearth/reearth-cms/server/pkg/request"
"github.com/reearth/reearth-cms/server/pkg/schema"
"github.com/reearth/reearth-cms/server/pkg/thread"
Expand All @@ -25,6 +30,9 @@ import (
"golang.org/x/exp/slices"
)

const maxPerPage = 100
const defaultPerPage int64 = 50

type Item struct {
repos *repo.Container
gateways *gateway.Container
Expand Down Expand Up @@ -1179,3 +1187,207 @@ func (i Item) getReferencedItems(ctx context.Context, fields []*item.Field) ([]i
}
return i.repos.Item.FindByIDs(ctx, ids, nil)
}

// ItemsAsCSV exports items data in content to csv file by modelID.
func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, perPage *int, operator *usecase.Operator) (*io.PipeReader, error) {
if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}
return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*io.PipeReader, error) {
model, err := i.repos.Model.FindByID(ctx, modelID)
if err != nil {
return nil, err
}

schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}

// fromPagination
paginationOffset := fromPagination(page, perPage)

items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}

pr, pw := io.Pipe()
err = csvFromItems(pw, items, schemaPackage.Schema())
if err != nil {
return nil, err
}

return pr, nil
})
}
Comment on lines +1191 to +1223
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add permission check for model access.

The method should verify if the operator has read access to the model before proceeding with the export.

Add the permission check after retrieving the model:

 		if err != nil {
 			return nil, err
 		}
+		if !operator.CanRead(model) {
+			return nil, interfaces.ErrOperationDenied
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ItemsAsCSV exports items data in content to csv file by modelID.
func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, perPage *int, operator *usecase.Operator) (*io.PipeReader, error) {
if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}
return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*io.PipeReader, error) {
model, err := i.repos.Model.FindByID(ctx, modelID)
if err != nil {
return nil, err
}
schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}
// fromPagination
paginationOffset := fromPagination(page, perPage)
items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
err = csvFromItems(pw, items, schemaPackage.Schema())
if err != nil {
return nil, err
}
return pr, nil
})
}
// ItemsAsCSV exports items data in content to csv file by modelID.
func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, perPage *int, operator *usecase.Operator) (*io.PipeReader, error) {
if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}
return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*io.PipeReader, error) {
model, err := i.repos.Model.FindByID(ctx, modelID)
if err != nil {
return nil, err
}
if !operator.CanRead(model) {
return nil, interfaces.ErrOperationDenied
}
schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}
// fromPagination
paginationOffset := fromPagination(page, perPage)
items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
err = csvFromItems(pw, items, schemaPackage.Schema())
if err != nil {
return nil, err
}
return pr, nil
})
}


// ItemsAsGeoJSON converts items to Geo JSON type given a model ID
func (i Item) ItemsAsGeoJSON(ctx context.Context, modelID id.ModelID, page *int, perPage *int, operator *usecase.Operator) (*integrationapi.FeatureCollection, error) {

if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}

return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*integrationapi.FeatureCollection, error) {
model, err := i.repos.Model.FindByID(ctx, modelID)
if err != nil {
return nil, err
}

schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}

// fromPagination
paginationOffset := fromPagination(page, perPage)

items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}

featureCollections, err := featureCollectionFromItems(items, schemaPackage.Schema())
if err != nil {
return nil, err
}

return featureCollections, nil
})
}

// ItemsWithProjectAsCSV converts items content to CSV given by project ID or project Alias and model ID or model Key
func (i Item) ItemsWithProjectAsCSV(ctx context.Context, projectIDorAlias project.IDOrAlias, modelIDOrKey model.IDOrKey, page *int, perPage *int, operator *usecase.Operator) (*io.PipeReader, error) {
if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}

return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*io.PipeReader, error) {
project, err := i.repos.Project.FindByIDOrAlias(ctx, projectIDorAlias)
if err != nil {
return nil, err
}

model, err := i.repos.Model.FindByIDOrKey(ctx, project.ID(), modelIDOrKey)
if err != nil {
return nil, err
}

schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}

// fromPagination
paginationOffset := fromPagination(page, perPage)

items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}

pr, pw := io.Pipe()
err = csvFromItems(pw, items, schemaPackage.Schema())
if err != nil {
return nil, err
}
jasonkarel marked this conversation as resolved.
Show resolved Hide resolved

return pr, nil
})
}
Comment on lines +1260 to +1298
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add permission check for project and model access.

The method should verify if the operator has read access to both the project and model.

Add the permission checks:

 		if err != nil {
 			return nil, err
 		}
+		if !operator.CanRead(project) {
+			return nil, interfaces.ErrOperationDenied
+		}

 		model, err := i.repos.Model.FindByIDOrKey(ctx, project.ID(), modelIDOrKey)
 		if err != nil {
 			return nil, err
 		}
+		if !operator.CanRead(model) {
+			return nil, interfaces.ErrOperationDenied
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ItemsWithProjectAsCSV converts items content to CSV given by project ID or project Alias and model ID or model Key
func (i Item) ItemsWithProjectAsCSV(ctx context.Context, projectIDorAlias project.IDOrAlias, modelIDOrKey model.IDOrKey, page *int, perPage *int, operator *usecase.Operator) (*io.PipeReader, error) {
if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}
return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*io.PipeReader, error) {
project, err := i.repos.Project.FindByIDOrAlias(ctx, projectIDorAlias)
if err != nil {
return nil, err
}
model, err := i.repos.Model.FindByIDOrKey(ctx, project.ID(), modelIDOrKey)
if err != nil {
return nil, err
}
schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}
// fromPagination
paginationOffset := fromPagination(page, perPage)
items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
err = csvFromItems(pw, items, schemaPackage.Schema())
if err != nil {
return nil, err
}
return pr, nil
})
}
// ItemsWithProjectAsCSV converts items content to CSV given by project ID or project Alias and model ID or model Key
func (i Item) ItemsWithProjectAsCSV(ctx context.Context, projectIDorAlias project.IDOrAlias, modelIDOrKey model.IDOrKey, page *int, perPage *int, operator *usecase.Operator) (*io.PipeReader, error) {
if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}
return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*io.PipeReader, error) {
project, err := i.repos.Project.FindByIDOrAlias(ctx, projectIDorAlias)
if err != nil {
return nil, err
}
if !operator.CanRead(project) {
return nil, interfaces.ErrOperationDenied
}
model, err := i.repos.Model.FindByIDOrKey(ctx, project.ID(), modelIDOrKey)
if err != nil {
return nil, err
}
if !operator.CanRead(model) {
return nil, interfaces.ErrOperationDenied
}
schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}
// fromPagination
paginationOffset := fromPagination(page, perPage)
items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
err = csvFromItems(pw, items, schemaPackage.Schema())
if err != nil {
return nil, err
}
return pr, nil
})
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @yk-eukarya , I wonder if this is needed ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


// ItemsWithProjectAsGeoJSON converts items content to Geo JSON given by project ID or project Alias and model ID or model Key
func (i Item) ItemsWithProjectAsGeoJSON(ctx context.Context, projectIDorAlias project.IDOrAlias, modelIDOrKey model.IDOrKey, page *int, perPage *int, operator *usecase.Operator) (*integrationapi.FeatureCollection, error) {
if operator.AcOperator.User == nil && operator.Integration == nil {
return nil, interfaces.ErrInvalidOperator
}

return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*integrationapi.FeatureCollection, error) {
project, err := i.repos.Project.FindByIDOrAlias(ctx, projectIDorAlias)
if err != nil {
return nil, err
}

model, err := i.repos.Model.FindByIDOrKey(ctx, project.ID(), modelIDOrKey)
if err != nil {
return nil, err
}

schemaPackage, err := i.buildSchemaPackage(ctx, model)
if err != nil {
return nil, err
}

// fromPagination
paginationOffset := fromPagination(page, perPage)

items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset)
if err != nil {
return nil, err
}

featureCollections, err := featureCollectionFromItems(items, schemaPackage.Schema())
if err != nil {
return nil, err
}

return featureCollections, nil
})
}

func (i Item) buildSchemaPackage(ctx context.Context, model *model.Model) (*schema.Package, error) {
schemaIDs := id.SchemaIDList{model.Schema()}
if model.Metadata() != nil {
schemaIDs = append(schemaIDs, *model.Metadata())
}
schemaList, err := i.repos.Schema.FindByIDs(ctx, schemaIDs)
if err != nil {
return nil, err
}
s := schemaList.Schema(lo.ToPtr(model.Schema()))
if s == nil {
return nil, rerror.ErrNotFound
}

groups, err := i.repos.Group.FindByIDs(ctx, s.Groups())
if err != nil {
return nil, err
}

groupSchemaList, err := i.repos.Schema.FindByIDs(ctx, groups.SchemaIDs().Add(s.ReferencedSchemas()...))
if err != nil {
return nil, err
}
groupSchemaMap := lo.SliceToMap(groups, func(g *group.Group) (id.GroupID, *schema.Schema) {
return g.ID(), schemaList.Schema(lo.ToPtr(g.Schema()))
})
referencedSchemaMap := lo.Map(s.ReferencedSchemas(), func(s schema.ID, _ int) *schema.Schema {
return groupSchemaList.Schema(&s)
})

return schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap), nil
}
Comment on lines +1339 to +1370
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (i Item) buildSchemaPackage(ctx context.Context, model *model.Model) (*schema.Package, error) {
schemaIDs := id.SchemaIDList{model.Schema()}
if model.Metadata() != nil {
schemaIDs = append(schemaIDs, *model.Metadata())
}
schemaList, err := i.repos.Schema.FindByIDs(ctx, schemaIDs)
if err != nil {
return nil, err
}
s := schemaList.Schema(lo.ToPtr(model.Schema()))
if s == nil {
return nil, rerror.ErrNotFound
}
groups, err := i.repos.Group.FindByIDs(ctx, s.Groups())
if err != nil {
return nil, err
}
groupSchemaList, err := i.repos.Schema.FindByIDs(ctx, groups.SchemaIDs().Add(s.ReferencedSchemas()...))
if err != nil {
return nil, err
}
groupSchemaMap := lo.SliceToMap(groups, func(g *group.Group) (id.GroupID, *schema.Schema) {
return g.ID(), schemaList.Schema(lo.ToPtr(g.Schema()))
})
referencedSchemaMap := lo.Map(s.ReferencedSchemas(), func(s schema.ID, _ int) *schema.Schema {
return groupSchemaList.Schema(&s)
})
return schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap), nil
}

get the schema package from uc.Schema.GetByModel in the adapter and pass it to the UC.
it's not good to duplicate the implementation, we can refactor it to a better way later.


func fromPagination(page, perPage *int) *usecasex.Pagination {
p := int64(1)
if page != nil && *page > 0 {
p = int64(*page)
}

pp := defaultPerPage
if perPage != nil {
if ppr := *perPage; 1 <= ppr {
if ppr > maxPerPage {
pp = int64(maxPerPage)
} else {
pp = int64(ppr)
}
}
}

return usecasex.OffsetPagination{
Offset: (p - 1) * pp,
Limit: pp,
}.Wrap()
}
Loading
Loading