Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(server): export items as geojson and csv via integration api #1194

Merged
merged 51 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
827b715
wip: items with project as geojson and csv
nourbalaha Jul 9, 2024
790ac50
update the csv format and description in the integration schema
nourbalaha Jul 9, 2024
d21d4e3
generate the schema
nourbalaha Jul 9, 2024
82928eb
wip: geojson schema
nourbalaha Jul 9, 2024
e0eb89c
update integration.yml
nourbalaha Jul 10, 2024
4cab64b
wip
nourbalaha Jul 11, 2024
4fcd005
Merge branch 'main' of ssh://github.com/reearth/reearth-cms into feat…
nourbalaha Jul 12, 2024
e7a799e
add two other end points
nourbalaha Jul 12, 2024
086d605
Merge branch 'main' into feat-server/integration-geojson-csv
nourbalaha Jul 17, 2024
93215bd
Merge branch 'main' into feat-server/integration-geojson-csv
nourbalaha Jul 17, 2024
08f061a
add ref param to endpoints
nourbalaha Jul 17, 2024
16dce26
add pagination and geo field params
nourbalaha Jul 18, 2024
89fcbd4
wip: implement items with project as geojson
nourbalaha Jul 18, 2024
a86540f
refactor
nourbalaha Jul 18, 2024
3aa8e50
i18n
nourbalaha Jul 18, 2024
2e756a3
add TestNewFeatureCollection and refactor
nourbalaha Jul 19, 2024
436febb
wip: geojson and csv
nourbalaha Jul 22, 2024
164e8a6
wip: ItemsAsGeoJSON and ItemsAsCSV
nourbalaha Jul 22, 2024
4e29b3c
add pagination to items by model as geojson and csv
nourbalaha Jul 23, 2024
58fdd87
update item queries
nourbalaha Jul 23, 2024
a3a5735
change coordinates type to float64
nourbalaha Jul 23, 2024
7da3405
wip: csv and geojson
nourbalaha Jul 23, 2024
94fca73
refactor
nourbalaha Jul 23, 2024
02c0e78
fix: features property in FeatureCollection
nourbalaha Jul 24, 2024
6b802b6
fix props and add unit test
nourbalaha Jul 24, 2024
89e3b95
refactor
nourbalaha Jul 24, 2024
7915f96
rename helpers test file
nourbalaha Jul 24, 2024
e3970bb
refactor csv
nourbalaha Jul 25, 2024
068b8b1
add e2e tests
nourbalaha Jul 25, 2024
86c0e3a
refactor e2e tests
nourbalaha Jul 25, 2024
bc2eee2
update e2e tests
nourbalaha Jul 25, 2024
71eaf78
add new helper functions
nourbalaha Jul 25, 2024
d998949
add more unit tests for csv
nourbalaha Jul 25, 2024
af947d2
add more unit tests to geojson
nourbalaha Jul 26, 2024
83c8ed4
return the geojson as feature collection
nourbalaha Jul 26, 2024
c484c0e
add japanese translations
nourbalaha Jul 26, 2024
94ebead
fix: csv and geojson bugs
nourbalaha Jul 26, 2024
941acf5
add unit tests for group
nourbalaha Jul 26, 2024
fff2bda
update geojson content type
nourbalaha Jul 29, 2024
605f248
Revert "update geojson content type"
nourbalaha Jul 29, 2024
bb775c5
update Japanese translations
nourbalaha Jul 29, 2024
6d61d5a
remove group and reference fields from export
nourbalaha Jul 30, 2024
787b505
refactor
nourbalaha Aug 2, 2024
1f1eff2
add check if result is empty
nourbalaha Aug 2, 2024
e1748ea
refactor2
nourbalaha Aug 2, 2024
fb7f821
Merge branch 'main' into feat-server/integration-geojson-csv
nourbalaha Aug 2, 2024
bffc2ca
refactor
nourbalaha Aug 2, 2024
199638b
refactor
nourbalaha Aug 2, 2024
91bb987
Revert "refactor"
nourbalaha Aug 2, 2024
148446f
Merge branch 'main' of ssh://github.com/reearth/reearth-cms into feat…
nourbalaha Aug 2, 2024
b0b6535
Merge branch 'main' into feat-server/integration-geojson-csv
nourbalaha Aug 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions server/e2e/integration_item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,62 @@ func IntegrationSearchItem(e *httpexpect.Expect, mId string, page, perPage int,
return res
}

func IntegrationItemsAsGeoJSON(e *httpexpect.Expect, mId string, page, perPage int, query string, sort, sortDir string, filter map[string]any) *httpexpect.Value {
res := e.GET("/api/models/{modelId}/items.geojson", mId).
WithHeader("Origin", "https://example.com").
WithHeader("X-Reearth-Debug-User", uId1.String()).
WithHeader("Content-Type", "application/json").
WithQuery("page", page).
WithQuery("perPage", perPage).
Expect().
Status(http.StatusOK).
JSON()

return res
}

func IntegrationItemsWithProjectAsGeoJSON(e *httpexpect.Expect, pId string, mId string, page, perPage int, query string, sort, sortDir string, filter map[string]any) *httpexpect.Value {
res := e.GET("/api/projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.geojson", pId, mId).
WithHeader("Origin", "https://example.com").
WithHeader("X-Reearth-Debug-User", uId1.String()).
WithHeader("Content-Type", "application/json").
WithQuery("page", page).
WithQuery("perPage", perPage).
Expect().
Status(http.StatusOK).
JSON()

return res
}

func IntegrationItemsAsCSV(e *httpexpect.Expect, mId string, page, perPage int, query string, sort, sortDir string, filter map[string]any) *httpexpect.String {
res := e.GET("/api/models/{modelId}/items.csv", mId).
WithHeader("Origin", "https://example.com").
WithHeader("X-Reearth-Debug-User", uId1.String()).
WithHeader("Content-Type", "text/csv").
WithQuery("page", page).
WithQuery("perPage", perPage).
Expect().
Status(http.StatusOK).
Body()

return res
}

func IntegrationItemsWithProjectAsCSV(e *httpexpect.Expect, pId string, mId string, page, perPage int, query string, sort, sortDir string, filter map[string]any) *httpexpect.String {
res := e.GET("/api/projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.csv", pId, mId).
WithHeader("Origin", "https://example.com").
WithHeader("X-Reearth-Debug-User", uId1.String()).
WithHeader("Content-Type", "text/csv").
WithQuery("page", page).
WithQuery("perPage", perPage).
Expect().
Status(http.StatusOK).
Body()

return res
}

// GET /models/{modelId}/items
func TestIntegrationItemListAPI(t *testing.T) {
e := StartServer(t, &app.Config{}, true, baseSeeder)
Expand Down Expand Up @@ -1163,6 +1219,99 @@ func TestIntegrationSearchItem(t *testing.T) {
// endregion
}

// GET /models/{modelId}/items.geojson
func TestIntegrationItemsAsGeoJSON(t *testing.T) {
e, _ := StartGQLServer(t, &app.Config{}, true, baseSeederUser)

pId, _ := createProject(e, wId.String(), "test", "test", "test-1")
mId, _ := createModel(e, pId, "test", "test", "test-1")
fids := createFieldOfEachType(t, e, mId)
sId, _, _ := getModel(e, mId)
i1Id, _ := createItem(e, mId, sId, nil, []map[string]any{
{"schemaFieldId": fids.textFId, "value": "test1", "type": "Text"},
{"schemaFieldId": fids.geometryObjectFid, "value": "{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}", "type": "GeometryObject"},
})

res := IntegrationItemsAsGeoJSON(e, mId, 1, 10, i1Id, "", "", nil)
res.Object().Value("type").String().IsEqual("FeatureCollection")
features := res.Object().Value("features").Array()
features.Length().IsEqual(1)
f := features.Value(0).Object()
f.Value("id").String().IsEqual(i1Id)
f.Value("type").String().IsEqual("Feature")
f.Value("properties").Object().Value("text").String().IsEqual("test1")
g := f.Value("geometry").Object()
g.Value("type").String().IsEqual("Point")
g.Value("coordinates").Array().IsEqual([]float64{139.28179282584915, 36.58570985749664})
}

// GET /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.geojson
func TestIntegrationItemsWithProjectAsGeoJSON(t *testing.T) {
e, _ := StartGQLServer(t, &app.Config{}, true, baseSeederUser)

pId, _ := createProject(e, wId.String(), "test", "test", "test-1")
mId, _ := createModel(e, pId, "test", "test", "test-1")
fids := createFieldOfEachType(t, e, mId)
sId, _, _ := getModel(e, mId)
i1Id, _ := createItem(e, mId, sId, nil, []map[string]any{
{"schemaFieldId": fids.textFId, "value": "test1", "type": "Text"},
{"schemaFieldId": fids.integerFId, "value": 30, "type": "Integer"},
{"schemaFieldId": fids.geometryObjectFid, "value": "{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}", "type": "GeometryObject"},
{"schemaFieldId": fids.geometryEditorFid, "value": "{\"coordinates\": [[[138.90306434425662,36.11737907906834],[138.90306434425662,36.33622175736386],[138.67187898370287,36.33622175736386],[138.67187898370287,36.11737907906834],[138.90306434425662,36.11737907906834]]],\"type\": \"Polygon\"}", "type": "GeometryEditor"},
})

res := IntegrationItemsWithProjectAsGeoJSON(e, pId, mId, 1, 10, i1Id, "", "", nil)
res.Object().Value("type").String().IsEqual("FeatureCollection")
features := res.Object().Value("features").Array()
features.Length().IsEqual(1)
f := features.Value(0).Object()
f.Value("id").String().IsEqual(i1Id)
f.Value("type").String().IsEqual("Feature")
f.Value("properties").Object().Value("text").String().IsEqual("test1")
f.Value("properties").Object().Value("integer").Number().IsEqual(30)
g := f.Value("geometry").Object()
g.Value("type").String().IsEqual("LineString")
g.Value("coordinates").Array().IsEqual([][]float64{{139.65439725962517, 36.34793305387103}, {139.61688622815393, 35.910803456352724}})
}

// GET /models/{modelId}/items.csv
func TestIntegrationItemsAsCSV(t *testing.T) {
e, _ := StartGQLServer(t, &app.Config{}, true, baseSeederUser)

pId, _ := createProject(e, wId.String(), "test", "test", "test-1")
mId, _ := createModel(e, pId, "test", "test", "test-1")
fids := createFieldOfEachType(t, e, mId)
sId, _, _ := getModel(e, mId)
i1Id, _ := createItem(e, mId, sId, nil, []map[string]any{
{"schemaFieldId": fids.textFId, "value": "test1", "type": "Text"},
{"schemaFieldId": fids.geometryObjectFid, "value": "{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}", "type": "GeometryObject"},
})

res := IntegrationItemsAsCSV(e, mId, 1, 10, i1Id, "", "", nil)
expected := fmt.Sprintf("id,location_lat,location_lng,text,textArea,markdown,asset,bool,select,integer,url,date,tag,checkbox\n%s,139.28179282584915,36.58570985749664,test1,,,,,,,,,,\n", i1Id)
res.IsEqual(expected)
}

// GET /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.csv
func TestIntegrationItemsWithProjectAsCSV(t *testing.T) {
e, _ := StartGQLServer(t, &app.Config{}, true, baseSeederUser)

pId, _ := createProject(e, wId.String(), "test", "test", "test-1")
mId, _ := createModel(e, pId, "test", "test", "test-1")
fids := createFieldOfEachType(t, e, mId)
sId, _, _ := getModel(e, mId)
i1Id, _ := createItem(e, mId, sId, nil, []map[string]any{
{"schemaFieldId": fids.textFId, "value": "test1", "type": "Text"},
{"schemaFieldId": fids.integerFId, "value": 30, "type": "Integer"},
{"schemaFieldId": fids.geometryObjectFid, "value": "{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}", "type": "GeometryObject"},
{"schemaFieldId": fids.geometryEditorFid, "value": "{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}", "type": "GeometryEditor"},
})

res := IntegrationItemsWithProjectAsCSV(e, pId, mId, 1, 10, i1Id, "", "", nil)
expected := fmt.Sprintf("id,location_lat,location_lng,text,textArea,markdown,asset,bool,select,integer,url,date,tag,checkbox\n%s,139.28179282584915,36.58570985749664,test1,,,,,,30,,,,\n", i1Id)
res.IsEqual(expected)
}

// POST /models/{modelId}/items
func TestIntegrationCreateItemAPI(t *testing.T) {
e := StartServer(t, &app.Config{}, true, baseSeeder)
Expand Down
3 changes: 3 additions & 0 deletions server/i18n/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ metadata schema not found: ""
model key is already used by another model: ""
model should have at least one view: ""
multiple reference is not supported: ""
no geometry field in this model: ""
no point field in this model: ""
not found: ""
not implemented: ""
not implemented yet: ""
Expand All @@ -70,6 +72,7 @@ one or more items not found: ""
only requests with status waiting can be approved: ""
only reviewers can approve: ""
operation denied: ""
point type is not supported in any geometry field in this model: ""
project alias is already used by another project: ""
project alias is not set: ""
projectID is required: ""
Expand Down
3 changes: 3 additions & 0 deletions server/i18n/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ metadata schema not found: メタデータのスキーマが見つかりませ
model key is already used by another model: このキーはすでに別のモデルで使用されています。
model should have at least one view: モデルには少なくとも 1 つのビューが必要です
multiple reference is not supported: 複数参照はサポートされていません
no geometry field in this model: このモデルにはジオメトリフィールドがありません。
no point field in this model: このモデルにはポイントフィールドがありません。
not found: 見つかりませんでした。
not implemented: 未実装です。
not implemented yet: 未実装です。
Expand All @@ -70,6 +72,7 @@ one or more items not found: 対象のアイテムが見つかりませんでし
only requests with status waiting can be approved: レビュー待ちのリクエストのみ承認可能です。
only reviewers can approve: レビュワーのみ承認可能です。
operation denied: 操作が拒否されました。
point type is not supported in any geometry field in this model: このモデルのどのジオメトリフィールドでも、ポイントタイプはサポートされていません。
project alias is already used by another project: プロジェクトエイリアスはすでに別のプロジェクトで使用されています。
project alias is not set: プロジェクトエイリアスが設定されていません。
projectID is required: プロジェクトIDは必須です。
Expand Down
151 changes: 151 additions & 0 deletions server/internal/adapter/integration/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package integration
import (
"context"
"errors"
"strings"

"github.com/reearth/reearth-cms/server/internal/usecase"
"github.com/reearth/reearth-cms/server/pkg/model"
Expand Down Expand Up @@ -69,6 +70,65 @@ func (s *Server) ItemFilter(ctx context.Context, request ItemFilterRequestObject
}, nil
}

func (s *Server) ItemsAsGeoJSON(ctx context.Context, request ItemsAsGeoJSONRequestObject) (ItemsAsGeoJSONResponseObject, error) {
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)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsAsGeoJSON404Response{}, err
}
return ItemsAsGeoJSON400Response{}, err
}

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

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

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

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

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

csvString, err := integrationapi.CSVFromItems(items, sp.Schema())
if err != nil {
return nil, err
}
reader := strings.NewReader(csvString)
contentLength := reader.Len()
return ItemsAsCSV200TextcsvResponse{
Body: reader,
ContentLength: int64(contentLength),
}, nil
}

func (s *Server) ItemFilterWithProject(ctx context.Context, request ItemFilterWithProjectRequestObject) (ItemFilterWithProjectResponseObject, error) {
op := adapter.Operator(ctx)
uc := adapter.Usecases(ctx)
Expand Down Expand Up @@ -138,6 +198,97 @@ func (s *Server) ItemFilterWithProject(ctx context.Context, request ItemFilterWi
}, nil
}

func (s *Server) ItemsWithProjectAsGeoJSON(ctx context.Context, request ItemsWithProjectAsGeoJSONRequestObject) (ItemsWithProjectAsGeoJSONResponseObject, error) {
op := adapter.Operator(ctx)
uc := adapter.Usecases(ctx)

prj, err := uc.Project.FindByIDOrAlias(ctx, request.ProjectIdOrAlias, op)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsGeoJSON400Response{}, err
}
return nil, err
nourbalaha marked this conversation as resolved.
Show resolved Hide resolved
}

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
nourbalaha marked this conversation as resolved.
Show resolved Hide resolved
}

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)
if err != nil {
if errors.Is(err, rerror.ErrNotFound) {
return ItemsWithProjectAsGeoJSON404Response{}, err
}
return ItemsWithProjectAsGeoJSON400Response{}, err
}

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

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

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

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

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

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

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

csvString, err := integrationapi.CSVFromItems(items, sp.Schema())
if err != nil {
return nil, err
}
reader := strings.NewReader(csvString)
contentLength := reader.Len()
return ItemsWithProjectAsCSV200TextcsvResponse{
Body: reader,
ContentLength: int64(contentLength),
}, nil
}

func (s *Server) ItemCreate(ctx context.Context, request ItemCreateRequestObject) (ItemCreateResponseObject, error) {
op := adapter.Operator(ctx)
uc := adapter.Usecases(ctx)
Expand Down
Loading
Loading