From 833b54140e6a52dd574fdf3c285cdf3d49bb319f Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Mon, 2 Dec 2024 18:21:26 +0700 Subject: [PATCH 1/8] feat(server): move export csv items to usecase layer --- server/internal/adapter/integration/item.go | 14 +-- server/internal/usecase/interactor/item.go | 79 ++++++++++++++ .../usecase/interactor/item_export.go | 64 +++++++++++ .../usecase/interactor/item_export_test.go | 100 ++++++++++++++++++ server/internal/usecase/interfaces/item.go | 2 + 5 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 server/internal/usecase/interactor/item_export.go create mode 100644 server/internal/usecase/interactor/item_export_test.go diff --git a/server/internal/adapter/integration/item.go b/server/internal/adapter/integration/item.go index f79f876905..5c4494831a 100644 --- a/server/internal/adapter/integration/item.go +++ b/server/internal/adapter/integration/item.go @@ -103,13 +103,7 @@ func (s *Server) ItemsAsCSV(ctx context.Context, request ItemsAsCSVRequestObject 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) if err != nil { if errors.Is(err, rerror.ErrNotFound) { return ItemsAsCSV404Response{}, err @@ -117,12 +111,6 @@ func (s *Server) ItemsAsCSV(ctx context.Context, request ItemsAsCSVRequestObject return ItemsAsCSV400Response{}, err } - pr, pw := io.Pipe() - err = csvFromItems(pw, items, sp.Schema()) - if err != nil { - return ItemsAsCSV400Response{}, err - } - return ItemsAsCSV200TextcsvResponse{ Body: pr, }, nil diff --git a/server/internal/usecase/interactor/item.go b/server/internal/usecase/interactor/item.go index e66a157a49..ee4c888290 100644 --- a/server/internal/usecase/interactor/item.go +++ b/server/internal/usecase/interactor/item.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "time" "github.com/reearth/reearth-cms/server/internal/usecase" @@ -11,6 +12,7 @@ import ( "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/item" "github.com/reearth/reearth-cms/server/pkg/request" @@ -25,6 +27,9 @@ import ( "golang.org/x/exp/slices" ) +const maxPerPage = 100 +const defaultPerPage int64 = 50 + type Item struct { repos *repo.Container gateways *gateway.Container @@ -1179,3 +1184,77 @@ func (i Item) getReferencedItems(ctx context.Context, fields []*item.Field) ([]i } return i.repos.Item.FindByIDs(ctx, ids, nil) } + +func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, perPage *int, operator *usecase.Operator) (*io.PipeReader, error) { + 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 + } + + 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, nil + } + + 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) + }) + + schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) + + // fromPagination + 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) + } + } + } + + paginationOffset := usecasex.OffsetPagination{ + Offset: (p - 1) * pp, + Limit: pp, + }.Wrap() + + 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 + }) +} diff --git a/server/internal/usecase/interactor/item_export.go b/server/internal/usecase/interactor/item_export.go new file mode 100644 index 0000000000..21decd5964 --- /dev/null +++ b/server/internal/usecase/interactor/item_export.go @@ -0,0 +1,64 @@ +package interactor + +import ( + "encoding/csv" + "io" + + "github.com/labstack/gommon/log" + "github.com/reearth/reearth-cms/server/pkg/integrationapi" + "github.com/reearth/reearth-cms/server/pkg/item" + "github.com/reearth/reearth-cms/server/pkg/schema" + "github.com/reearth/reearthx/i18n" + "github.com/reearth/reearthx/rerror" + "github.com/samber/lo" +) + +var ( + pointFieldIsNotSupportedError = rerror.NewE(i18n.T("point type is not supported in any geometry field in this model")) +) + +// CSV +func csvFromItems(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) error { + if !s.IsPointFieldSupported() { + return pointFieldIsNotSupportedError + } + + go handleCSVGeneration(pw, l, s) + + return nil +} + +func handleCSVGeneration(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) { + err := generateCSV(pw, l, s) + if err != nil { + log.Errorf("failed to generate CSV: %v", err) + _ = pw.CloseWithError(err) + } else { + _ = pw.Close() + } +} + +func generateCSV(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) error { + w := csv.NewWriter(pw) + defer w.Flush() + + headers := integrationapi.BuildCSVHeaders(s) + if err := w.Write(headers); err != nil { + return err + } + + nonGeoFields := lo.Filter(s.Fields(), func(f *schema.Field, _ int) bool { + return !f.IsGeometryField() + }) + + for _, ver := range l { + row, ok := integrationapi.RowFromItem(ver.Value(), nonGeoFields) + if ok { + if err := w.Write(row); err != nil { + return err + } + } + } + + return w.Error() +} diff --git a/server/internal/usecase/interactor/item_export_test.go b/server/internal/usecase/interactor/item_export_test.go new file mode 100644 index 0000000000..a7a891d275 --- /dev/null +++ b/server/internal/usecase/interactor/item_export_test.go @@ -0,0 +1,100 @@ +package interactor + +import ( + "io" + "testing" + + "github.com/reearth/reearth-cms/server/pkg/id" + "github.com/reearth/reearth-cms/server/pkg/item" + "github.com/reearth/reearth-cms/server/pkg/schema" + "github.com/reearth/reearth-cms/server/pkg/value" + "github.com/reearth/reearth-cms/server/pkg/version" + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/util" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +func TestCSVFromItems(t *testing.T) { + iid := id.NewItemID() + sid := id.NewSchemaID() + mid := id.NewModelID() + tid := id.NewThreadID() + pid := id.NewProjectID() + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).MustBuild() + sf3 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf4 := schema.NewField(tp4).NewID().Name("age").Key(id.RandomKey()).MustBuild() + sf5 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("isMarried").Key(id.RandomKey()).MustBuild() + s1 := schema.New().ID(sid).Fields([]*schema.Field{sf1, sf3, sf4, sf5}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fi2 := item.NewField(sf3.ID(), value.TypeGeometryEditor.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fi3 := item.NewField(sf4.ID(), value.TypeInteger.Value(30).AsMultiple(), nil) + fi4 := item.NewField(sf5.ID(), value.TypeBool.Value(true).AsMultiple(), nil) + i1 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi1, fi2, fi3, fi4}). + Model(mid). + Thread(tid). + MustBuild() + v1 := version.New() + vi1 := version.MustBeValue(v1, nil, version.NewRefs(version.Latest), util.Now(), i1) + + // with geometry fields + ver1 := item.VersionedList{vi1} + _, pw := io.Pipe() + err := csvFromItems(pw, ver1, s1) + assert.Nil(t, err) + + // no geometry fields + iid2 := id.NewItemID() + sid2 := id.NewSchemaID() + mid2 := id.NewModelID() + tid2 := id.NewThreadID() + sf2 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Key(id.RandomKey()).MustBuild() + s2 := schema.New().ID(sid).Fields([]*schema.Field{sf2}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() + i2 := item.New(). + ID(iid2). + Schema(sid2). + Project(pid). + Fields([]*item.Field{item.NewField(sf2.ID(), value.TypeText.Value("test").AsMultiple(), nil)}). + Model(mid2). + Thread(tid2). + MustBuild() + v2 := version.New() + vi2 := version.MustBeValue(v2, nil, version.NewRefs(version.Latest), util.Now(), i2) + ver2 := item.VersionedList{vi2} + expectErr2 := pointFieldIsNotSupportedError + _, pw1 := io.Pipe() + err = csvFromItems(pw1, ver2, s2) + assert.Equal(t, expectErr2, err) + + // point field is not supported + iid3 := id.NewItemID() + sid3 := id.NewSchemaID() + mid3 := id.NewModelID() + tid3 := id.NewThreadID() + gst2 := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypeLineString, schema.GeometryObjectSupportedTypePolygon} + sf6 := schema.NewField(schema.NewGeometryObject(gst2).TypeProperty()).NewID().Name("geo3").Key(id.RandomKey()).MustBuild() + s3 := schema.New().ID(sid).Fields([]*schema.Field{sf6}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() + i3 := item.New(). + ID(iid3). + Schema(sid3). + Project(pid). + Fields([]*item.Field{item.NewField(sf6.ID(), value.TypeText.Value("{\n \"coordinates\": [\n [\n 139.65439725962517,\n 36.34793305387103\n ],\n [\n 139.61688622815393,\n 35.910803456352724\n ]\n ],\n \"type\": \"LineString\"\n}").AsMultiple(), nil)}). + Model(mid3). + Thread(tid3). + MustBuild() + v3 := version.New() + vi3 := version.MustBeValue(v3, nil, version.NewRefs(version.Latest), util.Now(), i3) + ver3 := item.VersionedList{vi3} + expectErr3 := pointFieldIsNotSupportedError + _, pw2 := io.Pipe() + err = csvFromItems(pw2, ver3, s3) + assert.Equal(t, expectErr3, err) +} diff --git a/server/internal/usecase/interfaces/item.go b/server/internal/usecase/interfaces/item.go index 425706c1d9..76d70541c3 100644 --- a/server/internal/usecase/interfaces/item.go +++ b/server/internal/usecase/interfaces/item.go @@ -2,6 +2,7 @@ package interfaces import ( "context" + "io" "time" "github.com/reearth/reearth-cms/server/internal/usecase" @@ -91,6 +92,7 @@ type Item interface { FindAllVersionsByID(context.Context, id.ItemID, *usecase.Operator) (item.VersionedList, error) Search(context.Context, schema.Package, *item.Query, *usecasex.Pagination, *usecase.Operator) (item.VersionedList, *usecasex.PageInfo, error) ItemStatus(context.Context, id.ItemIDList, *usecase.Operator) (map[id.ItemID]item.Status, error) + ItemsAsCSV(context.Context, id.ModelID, *int, *int, *usecase.Operator) (*io.PipeReader, error) LastModifiedByModel(context.Context, id.ModelID, *usecase.Operator) (time.Time, error) IsItemReferenced(context.Context, id.ItemID, id.FieldID, *usecase.Operator) (bool, error) Create(context.Context, CreateItemParam, *usecase.Operator) (item.Versioned, error) From 0119c170a92ce68e2d0b1f44a51bb93d0d2fa994 Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Tue, 3 Dec 2024 11:38:34 +0700 Subject: [PATCH 2/8] tidy code organization --- server/internal/usecase/interactor/item.go | 48 ++++++++++++---------- server/internal/usecase/interfaces/item.go | 3 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/server/internal/usecase/interactor/item.go b/server/internal/usecase/interactor/item.go index ee4c888290..a6d4b6cc4e 100644 --- a/server/internal/usecase/interactor/item.go +++ b/server/internal/usecase/interactor/item.go @@ -1185,6 +1185,7 @@ 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) { return Run1(ctx, operator, i.repos, Usecase().Transaction(), func(ctx context.Context) (*io.PipeReader, error) { model, err := i.repos.Model.FindByID(ctx, modelID) @@ -1202,7 +1203,7 @@ func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, per } s := schemaList.Schema(lo.ToPtr(model.Schema())) if s == nil { - return nil, nil + return nil, rerror.ErrNotFound } groups, err := i.repos.Group.FindByIDs(ctx, s.Groups()) @@ -1224,26 +1225,7 @@ func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, per schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) // fromPagination - 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) - } - } - } - - paginationOffset := usecasex.OffsetPagination{ - Offset: (p - 1) * pp, - Limit: pp, - }.Wrap() + paginationOffset := fromPagination(page, perPage) items, _, err := i.repos.Item.FindBySchema(ctx, schemaPackage.Schema().ID(), nil, nil, paginationOffset) if err != nil { @@ -1255,6 +1237,30 @@ func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, per if err != nil { return nil, err } + return pr, nil }) } + +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() +} diff --git a/server/internal/usecase/interfaces/item.go b/server/internal/usecase/interfaces/item.go index 76d70541c3..a40911c07a 100644 --- a/server/internal/usecase/interfaces/item.go +++ b/server/internal/usecase/interfaces/item.go @@ -92,7 +92,6 @@ type Item interface { FindAllVersionsByID(context.Context, id.ItemID, *usecase.Operator) (item.VersionedList, error) Search(context.Context, schema.Package, *item.Query, *usecasex.Pagination, *usecase.Operator) (item.VersionedList, *usecasex.PageInfo, error) ItemStatus(context.Context, id.ItemIDList, *usecase.Operator) (map[id.ItemID]item.Status, error) - ItemsAsCSV(context.Context, id.ModelID, *int, *int, *usecase.Operator) (*io.PipeReader, error) LastModifiedByModel(context.Context, id.ModelID, *usecase.Operator) (time.Time, error) IsItemReferenced(context.Context, id.ItemID, id.FieldID, *usecase.Operator) (bool, error) Create(context.Context, CreateItemParam, *usecase.Operator) (item.Versioned, error) @@ -101,4 +100,6 @@ type Item interface { Publish(context.Context, id.ItemIDList, *usecase.Operator) (item.VersionedList, error) Unpublish(context.Context, id.ItemIDList, *usecase.Operator) (item.VersionedList, error) Import(context.Context, ImportItemsParam, *usecase.Operator) (ImportItemsResponse, error) + // ItemsAsCSV exports items data in content to csv file by modelID. + ItemsAsCSV(context.Context, id.ModelID, *int, *int, *usecase.Operator) (*io.PipeReader, error) } From 583ef20aee48dc2422572e39de25b5bd075a817b Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Tue, 3 Dec 2024 12:29:53 +0700 Subject: [PATCH 3/8] add operator handling in ItemsAsCSV --- server/internal/usecase/interactor/item.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/internal/usecase/interactor/item.go b/server/internal/usecase/interactor/item.go index a6d4b6cc4e..7048e35e7d 100644 --- a/server/internal/usecase/interactor/item.go +++ b/server/internal/usecase/interactor/item.go @@ -1187,6 +1187,9 @@ func (i Item) getReferencedItems(ctx context.Context, fields []*item.Field) ([]i // 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 { From 35c1dc04d2b3fe20b8a6e287adefa2152fed71ab Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Thu, 5 Dec 2024 02:42:41 +0700 Subject: [PATCH 4/8] move ItemsAsGeoJSON, ItemsWithProjectAsCSV, and ItemsWithProjectAsGeoJSON to usecase layer --- server/internal/adapter/integration/item.go | 81 +------ .../adapter/integration/item_export.go | 70 ------ .../adapter/integration/item_export_test.go | 100 --------- server/internal/usecase/interactor/item.go | 199 ++++++++++++++++++ .../usecase/interactor/item_export.go | 5 + server/internal/usecase/interfaces/item.go | 8 + 6 files changed, 219 insertions(+), 244 deletions(-) delete mode 100644 server/internal/adapter/integration/item_export.go delete mode 100644 server/internal/adapter/integration/item_export_test.go diff --git a/server/internal/adapter/integration/item.go b/server/internal/adapter/integration/item.go index 5c4494831a..25662e7275 100644 --- a/server/internal/adapter/integration/item.go +++ b/server/internal/adapter/integration/item.go @@ -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" @@ -74,13 +73,7 @@ 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 @@ -88,14 +81,9 @@ func (s *Server) ItemsAsGeoJSON(ctx context.Context, request ItemsAsGeoJSONReque 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 } @@ -189,29 +177,7 @@ 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 @@ -219,14 +185,9 @@ func (s *Server) ItemsWithProjectAsGeoJSON(ctx context.Context, request ItemsWit 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 } @@ -234,15 +195,7 @@ func (s *Server) ItemsWithProjectAsCSV(ctx context.Context, request ItemsWithPro 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) + 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 @@ -250,26 +203,6 @@ func (s *Server) ItemsWithProjectAsCSV(ctx context.Context, request ItemsWithPro 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) - 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 diff --git a/server/internal/adapter/integration/item_export.go b/server/internal/adapter/integration/item_export.go deleted file mode 100644 index fcc4ee77ee..0000000000 --- a/server/internal/adapter/integration/item_export.go +++ /dev/null @@ -1,70 +0,0 @@ -package integration - -import ( - "encoding/csv" - "io" - - "github.com/reearth/reearth-cms/server/pkg/integrationapi" - "github.com/reearth/reearth-cms/server/pkg/item" - "github.com/reearth/reearth-cms/server/pkg/schema" - "github.com/reearth/reearthx/i18n" - "github.com/reearth/reearthx/log" - "github.com/reearth/reearthx/rerror" - "github.com/samber/lo" -) - -var ( - pointFieldIsNotSupportedError = rerror.NewE(i18n.T("point type is not supported in any geometry field in this model")) -) - -// GeoJSON -func featureCollectionFromItems(ver item.VersionedList, s *schema.Schema) (*integrationapi.FeatureCollection, error) { - return integrationapi.FeatureCollectionFromItems(ver, s) -} - -// CSV -func csvFromItems(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) error { - if !s.IsPointFieldSupported() { - return pointFieldIsNotSupportedError - } - - go handleCSVGeneration(pw, l, s) - - return nil -} - -func handleCSVGeneration(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) { - err := generateCSV(pw, l, s) - if err != nil { - log.Errorf("failed to generate CSV: %v", err) - _ = pw.CloseWithError(err) - } else { - _ = pw.Close() - } -} - -func generateCSV(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) error { - w := csv.NewWriter(pw) - defer w.Flush() - - headers := integrationapi.BuildCSVHeaders(s) - if err := w.Write(headers); err != nil { - return err - } - - nonGeoFields := lo.Filter(s.Fields(), func(f *schema.Field, _ int) bool { - return !f.IsGeometryField() - }) - - for _, ver := range l { - row, ok := integrationapi.RowFromItem(ver.Value(), nonGeoFields) - if ok { - if err := w.Write(row); err != nil { - return err - } - } - } - - return w.Error() -} - diff --git a/server/internal/adapter/integration/item_export_test.go b/server/internal/adapter/integration/item_export_test.go deleted file mode 100644 index 774f31abf7..0000000000 --- a/server/internal/adapter/integration/item_export_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package integration - -import ( - "io" - "testing" - - "github.com/reearth/reearth-cms/server/pkg/id" - "github.com/reearth/reearth-cms/server/pkg/item" - "github.com/reearth/reearth-cms/server/pkg/schema" - "github.com/reearth/reearth-cms/server/pkg/value" - "github.com/reearth/reearth-cms/server/pkg/version" - "github.com/reearth/reearthx/account/accountdomain" - "github.com/reearth/reearthx/util" - "github.com/samber/lo" - "github.com/stretchr/testify/assert" -) - -func TestCSVFromItems(t *testing.T) { - iid := id.NewItemID() - sid := id.NewSchemaID() - mid := id.NewModelID() - tid := id.NewThreadID() - pid := id.NewProjectID() - gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} - gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} - sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).MustBuild() - sf3 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).MustBuild() - in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) - tp4 := in4.TypeProperty() - sf4 := schema.NewField(tp4).NewID().Name("age").Key(id.RandomKey()).MustBuild() - sf5 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("isMarried").Key(id.RandomKey()).MustBuild() - s1 := schema.New().ID(sid).Fields([]*schema.Field{sf1, sf3, sf4, sf5}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() - fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) - fi2 := item.NewField(sf3.ID(), value.TypeGeometryEditor.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) - fi3 := item.NewField(sf4.ID(), value.TypeInteger.Value(30).AsMultiple(), nil) - fi4 := item.NewField(sf5.ID(), value.TypeBool.Value(true).AsMultiple(), nil) - i1 := item.New(). - ID(iid). - Schema(sid). - Project(pid). - Fields([]*item.Field{fi1, fi2, fi3, fi4}). - Model(mid). - Thread(tid). - MustBuild() - v1 := version.New() - vi1 := version.MustBeValue(v1, nil, version.NewRefs(version.Latest), util.Now(), i1) - - // with geometry fields - ver1 := item.VersionedList{vi1} - _, pw := io.Pipe() - err := csvFromItems(pw, ver1, s1) - assert.Nil(t, err) - - // no geometry fields - iid2 := id.NewItemID() - sid2 := id.NewSchemaID() - mid2 := id.NewModelID() - tid2 := id.NewThreadID() - sf2 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Key(id.RandomKey()).MustBuild() - s2 := schema.New().ID(sid).Fields([]*schema.Field{sf2}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() - i2 := item.New(). - ID(iid2). - Schema(sid2). - Project(pid). - Fields([]*item.Field{item.NewField(sf2.ID(), value.TypeText.Value("test").AsMultiple(), nil)}). - Model(mid2). - Thread(tid2). - MustBuild() - v2 := version.New() - vi2 := version.MustBeValue(v2, nil, version.NewRefs(version.Latest), util.Now(), i2) - ver2 := item.VersionedList{vi2} - expectErr2 := pointFieldIsNotSupportedError - _, pw1 := io.Pipe() - err = csvFromItems(pw1, ver2, s2) - assert.Equal(t, expectErr2, err) - - // point field is not supported - iid3 := id.NewItemID() - sid3 := id.NewSchemaID() - mid3 := id.NewModelID() - tid3 := id.NewThreadID() - gst2 := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypeLineString, schema.GeometryObjectSupportedTypePolygon} - sf6 := schema.NewField(schema.NewGeometryObject(gst2).TypeProperty()).NewID().Name("geo3").Key(id.RandomKey()).MustBuild() - s3 := schema.New().ID(sid).Fields([]*schema.Field{sf6}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() - i3 := item.New(). - ID(iid3). - Schema(sid3). - Project(pid). - Fields([]*item.Field{item.NewField(sf6.ID(), value.TypeText.Value("{\n \"coordinates\": [\n [\n 139.65439725962517,\n 36.34793305387103\n ],\n [\n 139.61688622815393,\n 35.910803456352724\n ]\n ],\n \"type\": \"LineString\"\n}").AsMultiple(), nil)}). - Model(mid3). - Thread(tid3). - MustBuild() - v3 := version.New() - vi3 := version.MustBeValue(v3, nil, version.NewRefs(version.Latest), util.Now(), i3) - ver3 := item.VersionedList{vi3} - expectErr3 := pointFieldIsNotSupportedError - _, pw2 := io.Pipe() - err = csvFromItems(pw2, ver3, s3) - assert.Equal(t, expectErr3, err) -} \ No newline at end of file diff --git a/server/internal/usecase/interactor/item.go b/server/internal/usecase/interactor/item.go index 7048e35e7d..0e6437c565 100644 --- a/server/internal/usecase/interactor/item.go +++ b/server/internal/usecase/interactor/item.go @@ -14,7 +14,10 @@ import ( "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" @@ -1245,6 +1248,202 @@ func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, per }) } +// 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 + } + + 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) + }) + + schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) + + // 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 + } + + 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) + }) + + schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) + + // 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 + }) +} + +// 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 + } + + 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) + }) + + schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) + + // 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 fromPagination(page, perPage *int) *usecasex.Pagination { p := int64(1) if page != nil && *page > 0 { diff --git a/server/internal/usecase/interactor/item_export.go b/server/internal/usecase/interactor/item_export.go index 21decd5964..2f504b83ef 100644 --- a/server/internal/usecase/interactor/item_export.go +++ b/server/internal/usecase/interactor/item_export.go @@ -17,6 +17,11 @@ var ( pointFieldIsNotSupportedError = rerror.NewE(i18n.T("point type is not supported in any geometry field in this model")) ) +// GeoJSON +func featureCollectionFromItems(ver item.VersionedList, s *schema.Schema) (*integrationapi.FeatureCollection, error) { + return integrationapi.FeatureCollectionFromItems(ver, s) +} + // CSV func csvFromItems(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) error { if !s.IsPointFieldSupported() { diff --git a/server/internal/usecase/interfaces/item.go b/server/internal/usecase/interfaces/item.go index a40911c07a..7cf839505c 100644 --- a/server/internal/usecase/interfaces/item.go +++ b/server/internal/usecase/interfaces/item.go @@ -7,8 +7,10 @@ import ( "github.com/reearth/reearth-cms/server/internal/usecase" "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/schema" "github.com/reearth/reearth-cms/server/pkg/version" "github.com/reearth/reearthx/i18n" @@ -102,4 +104,10 @@ type Item interface { Import(context.Context, ImportItemsParam, *usecase.Operator) (ImportItemsResponse, error) // ItemsAsCSV exports items data in content to csv file by modelID. ItemsAsCSV(context.Context, id.ModelID, *int, *int, *usecase.Operator) (*io.PipeReader, error) + // ItemsAsGeoJSON converts items to Geo JSON type given a model ID + ItemsAsGeoJSON(context.Context, id.ModelID, *int, *int, *usecase.Operator) (*integrationapi.FeatureCollection, error) + // ItemsWithProjectAsCSV converts items content to CSV given by project ID or project Alias and model ID or model Key + ItemsWithProjectAsCSV(context.Context, project.IDOrAlias, model.IDOrKey, *int, *int, *usecase.Operator) (*io.PipeReader, error) + // ItemsWithProjectAsGeoJSON converts items content to Geo JSON given by project ID or project Alias and model ID or model Key + ItemsWithProjectAsGeoJSON(context.Context, project.IDOrAlias, model.IDOrKey, *int, *int, *usecase.Operator) (*integrationapi.FeatureCollection, error) } From 06101c7ac86efa131d97b552aec209f71d906e4e Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Fri, 6 Dec 2024 18:58:20 +0700 Subject: [PATCH 5/8] add buildeSchemaPackage func in Item --- server/internal/usecase/interactor/item.go | 149 +++++---------------- 1 file changed, 37 insertions(+), 112 deletions(-) diff --git a/server/internal/usecase/interactor/item.go b/server/internal/usecase/interactor/item.go index 0e6437c565..5872ebf928 100644 --- a/server/internal/usecase/interactor/item.go +++ b/server/internal/usecase/interactor/item.go @@ -1199,37 +1199,11 @@ func (i Item) ItemsAsCSV(ctx context.Context, modelID id.ModelID, page *int, per return nil, err } - 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()) + schemaPackage, err := i.buildSchemaPackage(ctx, model) 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) - }) - - schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) - // fromPagination paginationOffset := fromPagination(page, perPage) @@ -1261,37 +1235,11 @@ func (i Item) ItemsAsGeoJSON(ctx context.Context, modelID id.ModelID, page *int, return nil, err } - 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()) + schemaPackage, err := i.buildSchemaPackage(ctx, model) 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) - }) - - schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) - // fromPagination paginationOffset := fromPagination(page, perPage) @@ -1326,38 +1274,10 @@ func (i Item) ItemsWithProjectAsCSV(ctx context.Context, projectIDorAlias projec return nil, err } - 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()...)) + schemaPackage, err := i.buildSchemaPackage(ctx, model) 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) - }) - - schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) // fromPagination paginationOffset := fromPagination(page, perPage) @@ -1394,39 +1314,11 @@ func (i Item) ItemsWithProjectAsGeoJSON(ctx context.Context, projectIDorAlias pr return nil, err } - schemaIDs := id.SchemaIDList{model.Schema()} - if model.Metadata() != nil { - schemaIDs = append(schemaIDs, *model.Metadata()) - } - - schemaList, err := i.repos.Schema.FindByIDs(ctx, schemaIDs) + schemaPackage, err := i.buildSchemaPackage(ctx, model) 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) - }) - - schemaPackage := schema.NewPackage(s, schemaList.Schema(model.Metadata()), groupSchemaMap, referencedSchemaMap) - // fromPagination paginationOffset := fromPagination(page, perPage) @@ -1444,6 +1336,39 @@ func (i Item) ItemsWithProjectAsGeoJSON(ctx context.Context, projectIDorAlias pr }) } +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 +} + func fromPagination(page, perPage *int) *usecasex.Pagination { p := int64(1) if page != nil && *page > 0 { From e66d2129cbd28202125522ff70e00bac11fed1be Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Tue, 10 Dec 2024 12:15:30 +0700 Subject: [PATCH 6/8] added UT for public methods --- .../internal/usecase/interactor/item_test.go | 659 ++++++++++++++++++ 1 file changed, 659 insertions(+) diff --git a/server/internal/usecase/interactor/item_test.go b/server/internal/usecase/interactor/item_test.go index a8ce0df478..cf2c52573c 100644 --- a/server/internal/usecase/interactor/item_test.go +++ b/server/internal/usecase/interactor/item_test.go @@ -3,6 +3,7 @@ package interactor import ( "context" "errors" + "io" "testing" "time" @@ -11,6 +12,7 @@ import ( "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/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" @@ -22,6 +24,7 @@ import ( "github.com/reearth/reearthx/account/accountdomain/user" "github.com/reearth/reearthx/account/accountdomain/workspace" "github.com/reearth/reearthx/account/accountusecase" + "github.com/reearth/reearthx/i18n" "github.com/reearth/reearthx/rerror" "github.com/reearth/reearthx/usecasex" "github.com/reearth/reearthx/util" @@ -1082,3 +1085,659 @@ func TestWorkFlow(t *testing.T) { assert.NoError(t, err) assert.Equal(t, map[id.ItemID]item.Status{i.ID(): item.StatusPublic}, status) } + +func TestItem_ItemsAsCSV(t *testing.T) { + r := []workspace.Role{workspace.RoleReader, workspace.RoleWriter} + w := accountdomain.NewWorkspaceID() + prj := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} + + sid1 := id.NewSchemaID() + fid1 := id.NewFieldID() + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).ID(fid1).MustBuild() + s1 := schema.New().ID(sid1).Workspace(w).Project(prj.ID()).Fields(schema.FieldList{sf1}).MustBuild() + m1 := model.New().NewID().Schema(s1.ID()).Key(id.RandomKey()).Project(s1.Project()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fs1 := []*item.Field{fi1} + i1 := item.New().ID(id.NewItemID()).Schema(s1.ID()).Model(m1.ID()).Project(s1.Project()).Thread(id.NewThreadID()).Fields(fs1).MustBuild() + i1IDStr := i1.ID().String() + + sid2 := id.NewSchemaID() + fid2 := id.NewFieldID() + sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).ID(fid2).MustBuild() + s2 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf2}).MustBuild() + m2 := model.New().NewID().Schema(s2.ID()).Key(id.RandomKey()).Project(s2.Project()).MustBuild() + fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) + fs2 := []*item.Field{fi2} + i2 := item.New().NewID().Schema(s2.ID()).Model(m2.ID()).Project(s2.Project()).Thread(id.NewThreadID()).Fields(fs2).MustBuild() + + fid3 := id.NewFieldID() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf3 := schema.NewField(tp4).NewID().Name("age").Key(id.RandomKey()).ID(fid3).MustBuild() + s3 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf3}).MustBuild() + m3 := model.New().NewID().Schema(s3.ID()).Key(id.RandomKey()).Project(s3.Project()).MustBuild() + fs3 := []*item.Field{item.NewField(sf3.ID(), value.TypeReference.Value(nil).AsMultiple(), nil)} + i3 := item.New().NewID().Schema(s3.ID()).Model(m3.ID()).Project(s3.Project()).Thread(id.NewThreadID()).Fields(fs3).MustBuild() + + m4 := id.NewModelID() + + ctx := context.Background() + db := memory.New() + lo.Must0(db.Project.Save(ctx, prj)) + lo.Must0(db.Schema.Save(ctx, s1)) + lo.Must0(db.Model.Save(ctx, m1)) + lo.Must0(db.Item.Save(ctx, i1)) + lo.Must0(db.Schema.Save(ctx, s2)) + lo.Must0(db.Model.Save(ctx, m2)) + lo.Must0(db.Item.Save(ctx, i2)) + lo.Must0(db.Schema.Save(ctx, s3)) + lo.Must0(db.Model.Save(ctx, m3)) + lo.Must0(db.Item.Save(ctx, i3)) + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true + + page1 := 1 + perPage1 := 10 + + wid := accountdomain.NewWorkspaceID() + u := user.New().NewID().Email("aaa@bbb.com").Workspace(wid).Name("foo").MustBuild() + op := &usecase.Operator{ + AcOperator: &accountusecase.Operator{ + User: lo.ToPtr(u.ID()), + }, + } + + opUserNil := &usecase.Operator{ + AcOperator: &accountusecase.Operator{}, + } + + type args struct { + ctx context.Context + modelID id.ModelID + page *int + perPage *int + op *usecase.Operator + } + tests := []struct { + name string + args args + mockItemErr bool + want []byte + wantError error + }{ + { + name: "success", + args: args{ + ctx: context.Background(), + modelID: m1.ID(), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte("id,location_lat,location_lng\n" + i1IDStr + ",139.28179282584915,36.58570985749664\n"), + wantError: nil, + }, + { + name: "error point type is not supported in any geometry field multiple coordinates", + args: args{ + ctx: context.Background(), + modelID: m2.ID(), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte{}, + wantError: pointFieldIsNotSupportedError, + }, + { + name: "error point type is not supported in any geometry field non geometry field", + args: args{ + ctx: context.Background(), + modelID: m3.ID(), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte{}, + wantError: pointFieldIsNotSupportedError, + }, + { + name: "error model ID not found", + args: args{ + ctx: context.Background(), + modelID: m4, + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte{}, + wantError: rerror.ErrNotFound, + }, + { + name: "error operator user is nil", + args: args{ + ctx: context.Background(), + modelID: m4, + page: &page1, + perPage: &perPage1, + op: opUserNil, + }, + want: []byte{}, + wantError: interfaces.ErrInvalidOperator, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + pr, err := itemUC.ItemsAsCSV(ctx, tt.args.modelID, tt.args.page, tt.args.perPage, tt.args.op) + result := []byte{} + if pr != nil { + result, _ = io.ReadAll(pr) + } + + assert.Equal(t, tt.want, result) + assert.Equal(t, tt.wantError, err) + }) + } +} + +func TestItem_ItemsAsGeoJSON(t *testing.T) { + r := []workspace.Role{workspace.RoleReader, workspace.RoleWriter} + w := accountdomain.NewWorkspaceID() + prj := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} + + sid1 := id.NewSchemaID() + fid1 := id.NewFieldID() + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).ID(fid1).MustBuild() + s1 := schema.New().ID(sid1).Workspace(w).Project(prj.ID()).Fields(schema.FieldList{sf1}).MustBuild() + m1 := model.New().NewID().Schema(s1.ID()).Key(id.RandomKey()).Project(s1.Project()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fs1 := []*item.Field{fi1} + i1 := item.New().ID(id.NewItemID()).Schema(s1.ID()).Model(m1.ID()).Project(s1.Project()).Thread(id.NewThreadID()).Fields(fs1).MustBuild() + + v1 := version.New() + vi1 := version.MustBeValue(v1, nil, version.NewRefs(version.Latest), util.Now(), i1) + // with geometry fields + ver1 := item.VersionedList{vi1} + + fc1, _ := featureCollectionFromItems(ver1, s1) + + sid2 := id.NewSchemaID() + fid2 := id.NewFieldID() + sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).ID(fid2).MustBuild() + s2 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf2}).MustBuild() + m2 := model.New().NewID().Schema(s2.ID()).Key(id.RandomKey()).Project(s2.Project()).MustBuild() + fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) + fs2 := []*item.Field{fi2} + i2 := item.New().NewID().Schema(s2.ID()).Model(m2.ID()).Project(s2.Project()).Thread(id.NewThreadID()).Fields(fs2).MustBuild() + + fid3 := id.NewFieldID() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf3 := schema.NewField(tp4).NewID().Name("age").Key(id.RandomKey()).ID(fid3).MustBuild() + s3 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf3}).MustBuild() + m3 := model.New().NewID().Schema(s3.ID()).Key(id.RandomKey()).Project(s3.Project()).MustBuild() + fs3 := []*item.Field{item.NewField(sf3.ID(), value.TypeReference.Value(nil).AsMultiple(), nil)} + i3 := item.New().NewID().Schema(s3.ID()).Model(m3.ID()).Project(s3.Project()).Thread(id.NewThreadID()).Fields(fs3).MustBuild() + + m4 := id.NewModelID() + + ctx := context.Background() + db := memory.New() + lo.Must0(db.Project.Save(ctx, prj)) + lo.Must0(db.Schema.Save(ctx, s1)) + lo.Must0(db.Model.Save(ctx, m1)) + lo.Must0(db.Item.Save(ctx, i1)) + lo.Must0(db.Schema.Save(ctx, s2)) + lo.Must0(db.Model.Save(ctx, m2)) + lo.Must0(db.Item.Save(ctx, i2)) + lo.Must0(db.Schema.Save(ctx, s3)) + lo.Must0(db.Model.Save(ctx, m3)) + lo.Must0(db.Item.Save(ctx, i3)) + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true + + page1 := 1 + perPage1 := 10 + + wid := accountdomain.NewWorkspaceID() + u := user.New().NewID().Email("aaa@bbb.com").Workspace(wid).Name("foo").MustBuild() + op := &usecase.Operator{ + AcOperator: &accountusecase.Operator{ + User: lo.ToPtr(u.ID()), + }, + } + + opUserNil := &usecase.Operator{ + AcOperator: &accountusecase.Operator{}, + } + + type args struct { + ctx context.Context + modelID id.ModelID + page *int + perPage *int + op *usecase.Operator + } + tests := []struct { + name string + args args + mockItemErr bool + want *integrationapi.FeatureCollection + wantError error + }{ + { + name: "success", + args: args{ + ctx: context.Background(), + modelID: m1.ID(), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: fc1, + wantError: nil, + }, + { + name: "error no geometry field in this model", + args: args{ + ctx: context.Background(), + modelID: m2.ID(), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: nil, + wantError: rerror.NewE(i18n.T("no geometry field in this model")), + }, + { + name: "error no geometry field in this model / integer", + args: args{ + ctx: context.Background(), + modelID: m3.ID(), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: nil, + wantError: rerror.NewE(i18n.T("no geometry field in this model")), + }, + { + name: "error model ID not found", + args: args{ + ctx: context.Background(), + modelID: m4, + page: &page1, + perPage: &perPage1, + op: op, + }, + want: nil, + wantError: rerror.ErrNotFound, + }, + { + name: "error operator user is nil", + args: args{ + ctx: context.Background(), + modelID: m1.ID(), + page: &page1, + perPage: &perPage1, + op: opUserNil, + }, + want: nil, + wantError: interfaces.ErrInvalidOperator, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + result, err := itemUC.ItemsAsGeoJSON(ctx, tt.args.modelID, tt.args.page, tt.args.perPage, tt.args.op) + + assert.Equal(t, tt.want, result) + assert.Equal(t, tt.wantError, err) + }) + } +} + +func TestItem_ItemsWithProjectAsCSV(t *testing.T) { + r := []workspace.Role{workspace.RoleReader, workspace.RoleWriter} + w := accountdomain.NewWorkspaceID() + prj := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} + + sid1 := id.NewSchemaID() + fid1 := id.NewFieldID() + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).ID(fid1).MustBuild() + s1 := schema.New().ID(sid1).Workspace(w).Project(prj.ID()).Fields(schema.FieldList{sf1}).MustBuild() + m1 := model.New().NewID().Schema(s1.ID()).Key(id.RandomKey()).Project(s1.Project()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fs1 := []*item.Field{fi1} + i1 := item.New().ID(id.NewItemID()).Schema(s1.ID()).Model(m1.ID()).Project(s1.Project()).Thread(id.NewThreadID()).Fields(fs1).MustBuild() + i1IDStr := i1.ID().String() + + sid2 := id.NewSchemaID() + fid2 := id.NewFieldID() + sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).ID(fid2).MustBuild() + s2 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf2}).MustBuild() + m2 := model.New().NewID().Schema(s2.ID()).Key(id.RandomKey()).Project(s2.Project()).MustBuild() + fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) + fs2 := []*item.Field{fi2} + i2 := item.New().NewID().Schema(s2.ID()).Model(m2.ID()).Project(s2.Project()).Thread(id.NewThreadID()).Fields(fs2).MustBuild() + + fid3 := id.NewFieldID() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf3 := schema.NewField(tp4).NewID().Name("age").Key(id.RandomKey()).ID(fid3).MustBuild() + s3 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf3}).MustBuild() + m3 := model.New().NewID().Schema(s3.ID()).Key(id.RandomKey()).Project(s3.Project()).MustBuild() + fs3 := []*item.Field{item.NewField(sf3.ID(), value.TypeReference.Value(nil).AsMultiple(), nil)} + i3 := item.New().NewID().Schema(s3.ID()).Model(m3.ID()).Project(s3.Project()).Thread(id.NewThreadID()).Fields(fs3).MustBuild() + + m4 := id.NewModelID() + + ctx := context.Background() + db := memory.New() + lo.Must0(db.Project.Save(ctx, prj)) + lo.Must0(db.Schema.Save(ctx, s1)) + lo.Must0(db.Model.Save(ctx, m1)) + lo.Must0(db.Item.Save(ctx, i1)) + lo.Must0(db.Schema.Save(ctx, s2)) + lo.Must0(db.Model.Save(ctx, m2)) + lo.Must0(db.Item.Save(ctx, i2)) + lo.Must0(db.Schema.Save(ctx, s3)) + lo.Must0(db.Model.Save(ctx, m3)) + lo.Must0(db.Item.Save(ctx, i3)) + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true + + page1 := 1 + perPage1 := 10 + + wid := accountdomain.NewWorkspaceID() + u := user.New().NewID().Email("aaa@bbb.com").Workspace(wid).Name("foo").MustBuild() + op := &usecase.Operator{ + AcOperator: &accountusecase.Operator{ + User: lo.ToPtr(u.ID()), + }, + } + + opUserNil := &usecase.Operator{ + AcOperator: &accountusecase.Operator{}, + } + + type args struct { + ctx context.Context + projectIDorAlias project.IDOrAlias + modelIDOrKey model.IDOrKey + page *int + perPage *int + op *usecase.Operator + } + tests := []struct { + name string + args args + mockItemErr bool + want []byte + wantError error + }{ + { + name: "success", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m1.ID().String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte("id,location_lat,location_lng\n" + i1IDStr + ",139.28179282584915,36.58570985749664\n"), + wantError: nil, + }, + { + name: "error pointFieldIsNotSupportedError", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m2.ID().String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte{}, + wantError: pointFieldIsNotSupportedError, + }, + { + name: "error pointFieldIsNotSupportedError non-geometry type", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m3.ID().String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte{}, + wantError: pointFieldIsNotSupportedError, + }, + { + name: "error model ID not found", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m4.String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: []byte{}, + wantError: rerror.ErrNotFound, + }, + { + name: "error operator user is nil", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m1.ID().String()), + page: &page1, + perPage: &perPage1, + op: opUserNil, + }, + want: []byte{}, + wantError: interfaces.ErrInvalidOperator, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + pr, err := itemUC.ItemsWithProjectAsCSV(ctx, tt.args.projectIDorAlias, tt.args.modelIDOrKey, tt.args.page, tt.args.perPage, tt.args.op) + result := []byte{} + if pr != nil { + result, _ = io.ReadAll(pr) + } + + assert.Equal(t, tt.want, result) + assert.Equal(t, tt.wantError, err) + }) + } +} + +func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { + r := []workspace.Role{workspace.RoleReader, workspace.RoleWriter} + w := accountdomain.NewWorkspaceID() + prj := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} + + sid1 := id.NewSchemaID() + fid1 := id.NewFieldID() + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).ID(fid1).MustBuild() + s1 := schema.New().ID(sid1).Workspace(w).Project(prj.ID()).Fields(schema.FieldList{sf1}).MustBuild() + m1 := model.New().NewID().Schema(s1.ID()).Key(id.RandomKey()).Project(s1.Project()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fs1 := []*item.Field{fi1} + i1 := item.New().ID(id.NewItemID()).Schema(s1.ID()).Model(m1.ID()).Project(s1.Project()).Thread(id.NewThreadID()).Fields(fs1).MustBuild() + + v1 := version.New() + vi1 := version.MustBeValue(v1, nil, version.NewRefs(version.Latest), util.Now(), i1) + // with geometry fields + ver1 := item.VersionedList{vi1} + + fc1, _ := featureCollectionFromItems(ver1, s1) + + sid2 := id.NewSchemaID() + fid2 := id.NewFieldID() + sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).ID(fid2).MustBuild() + s2 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf2}).MustBuild() + m2 := model.New().NewID().Schema(s2.ID()).Key(id.RandomKey()).Project(s2.Project()).MustBuild() + fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) + fs2 := []*item.Field{fi2} + i2 := item.New().NewID().Schema(s2.ID()).Model(m2.ID()).Project(s2.Project()).Thread(id.NewThreadID()).Fields(fs2).MustBuild() + + fid3 := id.NewFieldID() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf3 := schema.NewField(tp4).NewID().Name("age").Key(id.RandomKey()).ID(fid3).MustBuild() + s3 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf3}).MustBuild() + m3 := model.New().NewID().Schema(s3.ID()).Key(id.RandomKey()).Project(s3.Project()).MustBuild() + fs3 := []*item.Field{item.NewField(sf3.ID(), value.TypeReference.Value(nil).AsMultiple(), nil)} + i3 := item.New().NewID().Schema(s3.ID()).Model(m3.ID()).Project(s3.Project()).Thread(id.NewThreadID()).Fields(fs3).MustBuild() + + m4 := id.NewModelID() + + ctx := context.Background() + db := memory.New() + lo.Must0(db.Project.Save(ctx, prj)) + lo.Must0(db.Schema.Save(ctx, s1)) + lo.Must0(db.Model.Save(ctx, m1)) + lo.Must0(db.Item.Save(ctx, i1)) + lo.Must0(db.Schema.Save(ctx, s2)) + lo.Must0(db.Model.Save(ctx, m2)) + lo.Must0(db.Item.Save(ctx, i2)) + lo.Must0(db.Schema.Save(ctx, s3)) + lo.Must0(db.Model.Save(ctx, m3)) + lo.Must0(db.Item.Save(ctx, i3)) + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true + + page1 := 1 + perPage1 := 10 + + wid := accountdomain.NewWorkspaceID() + u := user.New().NewID().Email("aaa@bbb.com").Workspace(wid).Name("foo").MustBuild() + op := &usecase.Operator{ + AcOperator: &accountusecase.Operator{ + User: lo.ToPtr(u.ID()), + }, + } + + opUserNil := &usecase.Operator{ + AcOperator: &accountusecase.Operator{}, + } + + type args struct { + ctx context.Context + projectIDorAlias project.IDOrAlias + modelIDOrKey model.IDOrKey + page *int + perPage *int + op *usecase.Operator + } + tests := []struct { + name string + args args + mockItemErr bool + want *integrationapi.FeatureCollection + wantError error + }{ + { + name: "success", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m1.ID().String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: fc1, + wantError: nil, + }, + { + name: "error pointFieldIsNotSupportedError", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m2.ID().String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: nil, + wantError: rerror.NewE(i18n.T("no geometry field in this model")), + }, + { + name: "error pointFieldIsNotSupportedError non-geometry type", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m3.ID().String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: nil, + wantError: rerror.NewE(i18n.T("no geometry field in this model")), + }, + { + name: "error model ID not found", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m4.String()), + page: &page1, + perPage: &perPage1, + op: op, + }, + want: nil, + wantError: rerror.ErrNotFound, + }, + { + name: "error operator user is nil", + args: args{ + ctx: context.Background(), + projectIDorAlias: project.IDOrAlias(prj.ID().String()), + modelIDOrKey: model.IDOrKey(m1.ID().String()), + page: &page1, + perPage: &perPage1, + op: opUserNil, + }, + want: nil, + wantError: interfaces.ErrInvalidOperator, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + result, err := itemUC.ItemsWithProjectAsGeoJSON(ctx, tt.args.projectIDorAlias, tt.args.modelIDOrKey, tt.args.page, tt.args.perPage, tt.args.op) + + assert.Equal(t, tt.want, result) + assert.Equal(t, tt.wantError, err) + }) + } +} From c0876b1ad25ff64c5cc845297dde57ae161369d3 Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Thu, 12 Dec 2024 15:41:00 +0700 Subject: [PATCH 7/8] Fix UT database save --- .../internal/usecase/interactor/item_test.go | 285 ++++++++++++------ 1 file changed, 185 insertions(+), 100 deletions(-) diff --git a/server/internal/usecase/interactor/item_test.go b/server/internal/usecase/interactor/item_test.go index cf2c52573c..9835b0df6d 100644 --- a/server/internal/usecase/interactor/item_test.go +++ b/server/internal/usecase/interactor/item_test.go @@ -1094,6 +1094,7 @@ func TestItem_ItemsAsCSV(t *testing.T) { gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} + // Geometry Object type sid1 := id.NewSchemaID() fid1 := id.NewFieldID() sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).ID(fid1).MustBuild() @@ -1104,15 +1105,17 @@ func TestItem_ItemsAsCSV(t *testing.T) { i1 := item.New().ID(id.NewItemID()).Schema(s1.ID()).Model(m1.ID()).Project(s1.Project()).Thread(id.NewThreadID()).Fields(fs1).MustBuild() i1IDStr := i1.ID().String() + // GeometryEditor type item sid2 := id.NewSchemaID() fid2 := id.NewFieldID() sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).ID(fid2).MustBuild() s2 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf2}).MustBuild() m2 := model.New().NewID().Schema(s2.ID()).Key(id.RandomKey()).Project(s2.Project()).MustBuild() - fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) + fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[ ],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) fs2 := []*item.Field{fi2} i2 := item.New().NewID().Schema(s2.ID()).Model(m2.ID()).Project(s2.Project()).Thread(id.NewThreadID()).Fields(fs2).MustBuild() + // integer type item fid3 := id.NewFieldID() in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) tp4 := in4.TypeProperty() @@ -1124,21 +1127,6 @@ func TestItem_ItemsAsCSV(t *testing.T) { m4 := id.NewModelID() - ctx := context.Background() - db := memory.New() - lo.Must0(db.Project.Save(ctx, prj)) - lo.Must0(db.Schema.Save(ctx, s1)) - lo.Must0(db.Model.Save(ctx, m1)) - lo.Must0(db.Item.Save(ctx, i1)) - lo.Must0(db.Schema.Save(ctx, s2)) - lo.Must0(db.Model.Save(ctx, m2)) - lo.Must0(db.Item.Save(ctx, i2)) - lo.Must0(db.Schema.Save(ctx, s3)) - lo.Must0(db.Model.Save(ctx, m3)) - lo.Must0(db.Item.Save(ctx, i3)) - itemUC := NewItem(db, nil) - itemUC.ignoreEvent = true - page1 := 1 perPage1 := 10 @@ -1164,7 +1152,9 @@ func TestItem_ItemsAsCSV(t *testing.T) { tests := []struct { name string args args - mockItemErr bool + seedsItems item.List + seedSchemas *schema.Schema + seedModels *model.Model want []byte wantError error }{ @@ -1177,11 +1167,14 @@ func TestItem_ItemsAsCSV(t *testing.T) { perPage: &perPage1, op: op, }, - want: []byte("id,location_lat,location_lng\n" + i1IDStr + ",139.28179282584915,36.58570985749664\n"), - wantError: nil, + seedsItems: item.List{i1}, + seedSchemas: s1, + seedModels: m1, + want: []byte("id,location_lat,location_lng\n" + i1IDStr + ",139.28179282584915,36.58570985749664\n"), + wantError: nil, }, { - name: "error point type is not supported in any geometry field multiple coordinates", + name: "success geometry editor type", args: args{ ctx: context.Background(), modelID: m2.ID(), @@ -1189,8 +1182,11 @@ func TestItem_ItemsAsCSV(t *testing.T) { perPage: &perPage1, op: op, }, - want: []byte{}, - wantError: pointFieldIsNotSupportedError, + seedsItems: item.List{i2}, + seedSchemas: s2, + seedModels: m2, + want: []byte("id,location_lat,location_lng\n"), + wantError: nil, }, { name: "error point type is not supported in any geometry field non geometry field", @@ -1201,8 +1197,11 @@ func TestItem_ItemsAsCSV(t *testing.T) { perPage: &perPage1, op: op, }, - want: []byte{}, - wantError: pointFieldIsNotSupportedError, + seedsItems: item.List{i3}, + seedSchemas: s3, + seedModels: m3, + want: []byte{}, + wantError: pointFieldIsNotSupportedError, }, { name: "error model ID not found", @@ -1233,6 +1232,22 @@ func TestItem_ItemsAsCSV(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.Background() + db := memory.New() + for _, seed := range tt.seedsItems { + err := db.Item.Save(ctx, seed) + assert.NoError(t, err) + } + + if tt.seedSchemas != nil { + err := db.Schema.Save(ctx, tt.seedSchemas) + assert.NoError(t, err) + } + if tt.seedModels != nil { + err := db.Model.Save(ctx, tt.seedModels) + assert.NoError(t, err) + } + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true pr, err := itemUC.ItemsAsCSV(ctx, tt.args.modelID, tt.args.page, tt.args.perPage, tt.args.op) result := []byte{} @@ -1278,6 +1293,11 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) fs2 := []*item.Field{fi2} i2 := item.New().NewID().Schema(s2.ID()).Model(m2.ID()).Project(s2.Project()).Thread(id.NewThreadID()).Fields(fs2).MustBuild() + v2 := version.New() + vi2 := version.MustBeValue(v2, nil, version.NewRefs(version.Latest), util.Now(), i2) + + ver2 := item.VersionedList{vi2} + fc2, _ := featureCollectionFromItems(ver2, s2) fid3 := id.NewFieldID() in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) @@ -1290,21 +1310,6 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { m4 := id.NewModelID() - ctx := context.Background() - db := memory.New() - lo.Must0(db.Project.Save(ctx, prj)) - lo.Must0(db.Schema.Save(ctx, s1)) - lo.Must0(db.Model.Save(ctx, m1)) - lo.Must0(db.Item.Save(ctx, i1)) - lo.Must0(db.Schema.Save(ctx, s2)) - lo.Must0(db.Model.Save(ctx, m2)) - lo.Must0(db.Item.Save(ctx, i2)) - lo.Must0(db.Schema.Save(ctx, s3)) - lo.Must0(db.Model.Save(ctx, m3)) - lo.Must0(db.Item.Save(ctx, i3)) - itemUC := NewItem(db, nil) - itemUC.ignoreEvent = true - page1 := 1 perPage1 := 10 @@ -1330,7 +1335,9 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { tests := []struct { name string args args - mockItemErr bool + seedsItems item.List + seedSchemas *schema.Schema + seedModels *model.Model want *integrationapi.FeatureCollection wantError error }{ @@ -1343,11 +1350,14 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { perPage: &perPage1, op: op, }, - want: fc1, - wantError: nil, + seedsItems: item.List{i1}, + seedSchemas: s1, + seedModels: m1, + want: fc1, + wantError: nil, }, { - name: "error no geometry field in this model", + name: "success geometry editor type", args: args{ ctx: context.Background(), modelID: m2.ID(), @@ -1355,8 +1365,11 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { perPage: &perPage1, op: op, }, - want: nil, - wantError: rerror.NewE(i18n.T("no geometry field in this model")), + seedsItems: item.List{i2}, + seedSchemas: s2, + seedModels: m2, + want: fc2, + wantError: nil, }, { name: "error no geometry field in this model / integer", @@ -1367,8 +1380,11 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { perPage: &perPage1, op: op, }, - want: nil, - wantError: rerror.NewE(i18n.T("no geometry field in this model")), + seedsItems: item.List{i3}, + seedSchemas: s3, + seedModels: m3, + want: nil, + wantError: rerror.NewE(i18n.T("no geometry field in this model")), }, { name: "error model ID not found", @@ -1400,6 +1416,22 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { t.Parallel() ctx := context.Background() + db := memory.New() + for _, seed := range tt.seedsItems { + err := db.Item.Save(ctx, seed) + assert.NoError(t, err) + } + + if tt.seedSchemas != nil { + err := db.Schema.Save(ctx, tt.seedSchemas) + assert.NoError(t, err) + } + if tt.seedModels != nil { + err := db.Model.Save(ctx, tt.seedModels) + assert.NoError(t, err) + } + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true result, err := itemUC.ItemsAsGeoJSON(ctx, tt.args.modelID, tt.args.page, tt.args.perPage, tt.args.op) assert.Equal(t, tt.want, result) @@ -1446,21 +1478,6 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { m4 := id.NewModelID() - ctx := context.Background() - db := memory.New() - lo.Must0(db.Project.Save(ctx, prj)) - lo.Must0(db.Schema.Save(ctx, s1)) - lo.Must0(db.Model.Save(ctx, m1)) - lo.Must0(db.Item.Save(ctx, i1)) - lo.Must0(db.Schema.Save(ctx, s2)) - lo.Must0(db.Model.Save(ctx, m2)) - lo.Must0(db.Item.Save(ctx, i2)) - lo.Must0(db.Schema.Save(ctx, s3)) - lo.Must0(db.Model.Save(ctx, m3)) - lo.Must0(db.Item.Save(ctx, i3)) - itemUC := NewItem(db, nil) - itemUC.ignoreEvent = true - page1 := 1 perPage1 := 10 @@ -1487,7 +1504,10 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { tests := []struct { name string args args - mockItemErr bool + seedProject *project.Project + seedsItems item.List + seedSchema *schema.Schema + seedModel *model.Model want []byte wantError error }{ @@ -1501,11 +1521,15 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { perPage: &perPage1, op: op, }, - want: []byte("id,location_lat,location_lng\n" + i1IDStr + ",139.28179282584915,36.58570985749664\n"), - wantError: nil, + seedProject: prj, + seedsItems: item.List{i1}, + seedSchema: s1, + seedModel: m1, + want: []byte("id,location_lat,location_lng\n" + i1IDStr + ",139.28179282584915,36.58570985749664\n"), + wantError: nil, }, { - name: "error pointFieldIsNotSupportedError", + name: "success geometry editor type", args: args{ ctx: context.Background(), projectIDorAlias: project.IDOrAlias(prj.ID().String()), @@ -1514,8 +1538,12 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { perPage: &perPage1, op: op, }, - want: []byte{}, - wantError: pointFieldIsNotSupportedError, + seedProject: prj, + seedsItems: item.List{i2}, + seedSchema: s2, + seedModel: m2, + want: []byte("id,location_lat,location_lng\n"), + wantError: nil, }, { name: "error pointFieldIsNotSupportedError non-geometry type", @@ -1527,8 +1555,12 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { perPage: &perPage1, op: op, }, - want: []byte{}, - wantError: pointFieldIsNotSupportedError, + seedProject: prj, + seedsItems: item.List{i3}, + seedSchema: s3, + seedModel: m3, + want: []byte{}, + wantError: pointFieldIsNotSupportedError, }, { name: "error model ID not found", @@ -1540,8 +1572,9 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { perPage: &perPage1, op: op, }, - want: []byte{}, - wantError: rerror.ErrNotFound, + seedProject: prj, + want: []byte{}, + wantError: rerror.ErrNotFound, }, { name: "error operator user is nil", @@ -1553,8 +1586,9 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { perPage: &perPage1, op: opUserNil, }, - want: []byte{}, - wantError: interfaces.ErrInvalidOperator, + seedProject: prj, + want: []byte{}, + wantError: interfaces.ErrInvalidOperator, }, } for _, tt := range tests { @@ -1562,6 +1596,28 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { t.Parallel() ctx := context.Background() + db := memory.New() + + if tt.seedProject != nil { + err := db.Project.Save(ctx, tt.seedProject) + assert.NoError(t, err) + } + for _, seed := range tt.seedsItems { + err := db.Item.Save(ctx, seed) + assert.NoError(t, err) + } + + if tt.seedSchema != nil { + err := db.Schema.Save(ctx, tt.seedSchema) + assert.NoError(t, err) + } + if tt.seedModel != nil { + err := db.Model.Save(ctx, tt.seedModel) + assert.NoError(t, err) + } + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true + pr, err := itemUC.ItemsWithProjectAsCSV(ctx, tt.args.projectIDorAlias, tt.args.modelIDOrKey, tt.args.page, tt.args.perPage, tt.args.op) result := []byte{} if pr != nil { @@ -1606,6 +1662,11 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) fs2 := []*item.Field{fi2} i2 := item.New().NewID().Schema(s2.ID()).Model(m2.ID()).Project(s2.Project()).Thread(id.NewThreadID()).Fields(fs2).MustBuild() + v2 := version.New() + vi2 := version.MustBeValue(v2, nil, version.NewRefs(version.Latest), util.Now(), i2) + + ver2 := item.VersionedList{vi2} + fc2, _ := featureCollectionFromItems(ver2, s2) fid3 := id.NewFieldID() in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) @@ -1618,21 +1679,6 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { m4 := id.NewModelID() - ctx := context.Background() - db := memory.New() - lo.Must0(db.Project.Save(ctx, prj)) - lo.Must0(db.Schema.Save(ctx, s1)) - lo.Must0(db.Model.Save(ctx, m1)) - lo.Must0(db.Item.Save(ctx, i1)) - lo.Must0(db.Schema.Save(ctx, s2)) - lo.Must0(db.Model.Save(ctx, m2)) - lo.Must0(db.Item.Save(ctx, i2)) - lo.Must0(db.Schema.Save(ctx, s3)) - lo.Must0(db.Model.Save(ctx, m3)) - lo.Must0(db.Item.Save(ctx, i3)) - itemUC := NewItem(db, nil) - itemUC.ignoreEvent = true - page1 := 1 perPage1 := 10 @@ -1659,7 +1705,10 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { tests := []struct { name string args args - mockItemErr bool + seedProject *project.Project + seedsItems item.List + seedSchema *schema.Schema + seedModel *model.Model want *integrationapi.FeatureCollection wantError error }{ @@ -1673,8 +1722,12 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { perPage: &perPage1, op: op, }, - want: fc1, - wantError: nil, + seedProject: prj, + seedsItems: item.List{i1}, + seedSchema: s1, + seedModel: m1, + want: fc1, + wantError: nil, }, { name: "error pointFieldIsNotSupportedError", @@ -1686,8 +1739,12 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { perPage: &perPage1, op: op, }, - want: nil, - wantError: rerror.NewE(i18n.T("no geometry field in this model")), + seedProject: prj, + seedsItems: item.List{i2}, + seedSchema: s2, + seedModel: m2, + want: fc2, + wantError: nil, }, { name: "error pointFieldIsNotSupportedError non-geometry type", @@ -1699,8 +1756,12 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { perPage: &perPage1, op: op, }, - want: nil, - wantError: rerror.NewE(i18n.T("no geometry field in this model")), + seedProject: prj, + seedsItems: item.List{i3}, + seedSchema: s3, + seedModel: m3, + want: nil, + wantError: rerror.NewE(i18n.T("no geometry field in this model")), }, { name: "error model ID not found", @@ -1712,8 +1773,9 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { perPage: &perPage1, op: op, }, - want: nil, - wantError: rerror.ErrNotFound, + seedProject: prj, + want: nil, + wantError: rerror.ErrNotFound, }, { name: "error operator user is nil", @@ -1725,8 +1787,9 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { perPage: &perPage1, op: opUserNil, }, - want: nil, - wantError: interfaces.ErrInvalidOperator, + seedProject: prj, + want: nil, + wantError: interfaces.ErrInvalidOperator, }, } for _, tt := range tests { @@ -1734,6 +1797,28 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { t.Parallel() ctx := context.Background() + db := memory.New() + + if tt.seedProject != nil { + err := db.Project.Save(ctx, tt.seedProject) + assert.NoError(t, err) + } + for _, seed := range tt.seedsItems { + err := db.Item.Save(ctx, seed) + assert.NoError(t, err) + } + + if tt.seedSchema != nil { + err := db.Schema.Save(ctx, tt.seedSchema) + assert.NoError(t, err) + } + if tt.seedModel != nil { + err := db.Model.Save(ctx, tt.seedModel) + assert.NoError(t, err) + } + itemUC := NewItem(db, nil) + itemUC.ignoreEvent = true + result, err := itemUC.ItemsWithProjectAsGeoJSON(ctx, tt.args.projectIDorAlias, tt.args.modelIDOrKey, tt.args.page, tt.args.perPage, tt.args.op) assert.Equal(t, tt.want, result) From ffc405269d42da8e7888ceccde91bc932cb0eb99 Mon Sep 17 00:00:00 2001 From: jasonkarel Date: Thu, 12 Dec 2024 16:07:41 +0700 Subject: [PATCH 8/8] fix race condition in project ut --- .../internal/usecase/interactor/item_test.go | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/server/internal/usecase/interactor/item_test.go b/server/internal/usecase/interactor/item_test.go index 9835b0df6d..62683e88ba 100644 --- a/server/internal/usecase/interactor/item_test.go +++ b/server/internal/usecase/interactor/item_test.go @@ -1417,6 +1417,7 @@ func TestItem_ItemsAsGeoJSON(t *testing.T) { ctx := context.Background() db := memory.New() + for _, seed := range tt.seedsItems { err := db.Item.Save(ctx, seed) assert.NoError(t, err) @@ -1633,7 +1634,11 @@ func TestItem_ItemsWithProjectAsCSV(t *testing.T) { func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { r := []workspace.Role{workspace.RoleReader, workspace.RoleWriter} w := accountdomain.NewWorkspaceID() - prj := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + prj1 := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + prj2 := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + prj3 := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + prj4 := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() + prj5 := project.New().NewID().Workspace(w).RequestRoles(r).MustBuild() gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} @@ -1641,7 +1646,7 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { sid1 := id.NewSchemaID() fid1 := id.NewFieldID() sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("geo1").Key(id.RandomKey()).ID(fid1).MustBuild() - s1 := schema.New().ID(sid1).Workspace(w).Project(prj.ID()).Fields(schema.FieldList{sf1}).MustBuild() + s1 := schema.New().ID(sid1).Workspace(w).Project(prj1.ID()).Fields(schema.FieldList{sf1}).MustBuild() m1 := model.New().NewID().Schema(s1.ID()).Key(id.RandomKey()).Project(s1.Project()).MustBuild() fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) fs1 := []*item.Field{fi1} @@ -1657,7 +1662,7 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { sid2 := id.NewSchemaID() fid2 := id.NewFieldID() sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(id.RandomKey()).ID(fid2).MustBuild() - s2 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf2}).MustBuild() + s2 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj2.ID()).Fields(schema.FieldList{sf2}).MustBuild() m2 := model.New().NewID().Schema(s2.ID()).Key(id.RandomKey()).Project(s2.Project()).MustBuild() fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}").AsMultiple(), nil) fs2 := []*item.Field{fi2} @@ -1672,7 +1677,7 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) tp4 := in4.TypeProperty() sf3 := schema.NewField(tp4).NewID().Name("age").Key(id.RandomKey()).ID(fid3).MustBuild() - s3 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).Fields(schema.FieldList{sf3}).MustBuild() + s3 := schema.New().ID(sid2).Workspace(accountdomain.NewWorkspaceID()).Project(prj3.ID()).Fields(schema.FieldList{sf3}).MustBuild() m3 := model.New().NewID().Schema(s3.ID()).Key(id.RandomKey()).Project(s3.Project()).MustBuild() fs3 := []*item.Field{item.NewField(sf3.ID(), value.TypeReference.Value(nil).AsMultiple(), nil)} i3 := item.New().NewID().Schema(s3.ID()).Model(m3.ID()).Project(s3.Project()).Thread(id.NewThreadID()).Fields(fs3).MustBuild() @@ -1716,13 +1721,13 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { name: "success", args: args{ ctx: context.Background(), - projectIDorAlias: project.IDOrAlias(prj.ID().String()), + projectIDorAlias: project.IDOrAlias(prj1.ID().String()), modelIDOrKey: model.IDOrKey(m1.ID().String()), page: &page1, perPage: &perPage1, op: op, }, - seedProject: prj, + seedProject: prj1, seedsItems: item.List{i1}, seedSchema: s1, seedModel: m1, @@ -1730,16 +1735,16 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { wantError: nil, }, { - name: "error pointFieldIsNotSupportedError", + name: "success geometry editor", args: args{ ctx: context.Background(), - projectIDorAlias: project.IDOrAlias(prj.ID().String()), + projectIDorAlias: project.IDOrAlias(prj2.ID().String()), modelIDOrKey: model.IDOrKey(m2.ID().String()), page: &page1, perPage: &perPage1, op: op, }, - seedProject: prj, + seedProject: prj2, seedsItems: item.List{i2}, seedSchema: s2, seedModel: m2, @@ -1750,13 +1755,13 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { name: "error pointFieldIsNotSupportedError non-geometry type", args: args{ ctx: context.Background(), - projectIDorAlias: project.IDOrAlias(prj.ID().String()), + projectIDorAlias: project.IDOrAlias(prj3.ID().String()), modelIDOrKey: model.IDOrKey(m3.ID().String()), page: &page1, perPage: &perPage1, op: op, }, - seedProject: prj, + seedProject: prj3, seedsItems: item.List{i3}, seedSchema: s3, seedModel: m3, @@ -1767,13 +1772,13 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { name: "error model ID not found", args: args{ ctx: context.Background(), - projectIDorAlias: project.IDOrAlias(prj.ID().String()), + projectIDorAlias: project.IDOrAlias(prj4.ID().String()), modelIDOrKey: model.IDOrKey(m4.String()), page: &page1, perPage: &perPage1, op: op, }, - seedProject: prj, + seedProject: prj4, want: nil, wantError: rerror.ErrNotFound, }, @@ -1781,13 +1786,13 @@ func TestItem_ItemsWithProjectAsGeoJSON(t *testing.T) { name: "error operator user is nil", args: args{ ctx: context.Background(), - projectIDorAlias: project.IDOrAlias(prj.ID().String()), + projectIDorAlias: project.IDOrAlias(prj5.ID().String()), modelIDOrKey: model.IDOrKey(m1.ID().String()), page: &page1, perPage: &perPage1, op: opUserNil, }, - seedProject: prj, + seedProject: prj5, want: nil, wantError: interfaces.ErrInvalidOperator, },