From d72b93ecd7a2dd375af573505738bda15f4d7f11 Mon Sep 17 00:00:00 2001 From: caichi <54824604+caichi-t@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:18:26 +0900 Subject: [PATCH 1/5] fix(web): changes are not detected for some group fields (#1199) fix: the logic of detect changes in group field --- web/src/components/molecules/Content/Form/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/components/molecules/Content/Form/index.tsx b/web/src/components/molecules/Content/Form/index.tsx index 5e5ea493b4..326533ea2e 100644 --- a/web/src/components/molecules/Content/Form/index.tsx +++ b/web/src/components/molecules/Content/Form/index.tsx @@ -226,9 +226,8 @@ const ContentForm: React.FC = ({ (changedValues: any) => { const [key, value] = Object.entries(changedValues)[0]; if (checkIfSingleGroupField(key, value)) { - const [groupFieldKey, groupFieldValue] = Object.entries(initialFormValues[key])[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const changedFieldValue = (value as any)[groupFieldKey]; + const [groupFieldKey, changedFieldValue] = Object.entries(value as object)[0]; + const groupFieldValue = initialFormValues[key][groupFieldKey]; if ( JSON.stringify(emptyConvert(changedFieldValue)) === JSON.stringify(emptyConvert(groupFieldValue)) From ccba0a41834b723cc47c1a04add8cb0d1d9a27aa Mon Sep 17 00:00:00 2001 From: yk-eukarya <81808708+yk-eukarya@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:06:17 +0200 Subject: [PATCH 2/5] feat(server): schemata in integration api (#1200) imp --- server/e2e/integration_schema_test.go | 56 +- server/internal/adapter/integration/model.go | 41 +- server/internal/adapter/integration/schema.go | 26 +- .../adapter/integration/server.gen.go | 677 +++++++++--------- .../internal/infrastructure/memory/model.go | 15 + server/internal/infrastructure/mongo/model.go | 10 + .../infrastructure/mongo/model_test.go | 109 +++ server/internal/usecase/interactor/model.go | 4 + server/internal/usecase/interfaces/model.go | 2 + server/internal/usecase/repo/model.go | 1 + server/pkg/integrationapi/schema.go | 9 +- server/pkg/integrationapi/types.gen.go | 49 +- server/pkg/schema/schema.go | 2 +- server/schemas/integration.yml | 20 +- 14 files changed, 612 insertions(+), 409 deletions(-) diff --git a/server/e2e/integration_schema_test.go b/server/e2e/integration_schema_test.go index 275edd89d0..aeef48a664 100644 --- a/server/e2e/integration_schema_test.go +++ b/server/e2e/integration_schema_test.go @@ -10,24 +10,24 @@ import ( // POST /api/models/{modelId}/fields func TestIntegrationFieldCreateAPI(t *testing.T) { - endpoint := "/api/models/{modelId}/fields" + endpoint := "/api/schemata/{schemaId}/fields" e := StartServer(t, &app.Config{}, true, baseSeeder) - e.POST(endpoint, id.NewModelID()). + e.POST(endpoint, id.NewSchemaID()). Expect(). Status(http.StatusUnauthorized) - e.POST(endpoint, id.NewModelID()). + e.POST(endpoint, id.NewSchemaID()). WithHeader("authorization", "secret_abc"). Expect(). Status(http.StatusUnauthorized) - e.POST(endpoint, id.NewModelID()). + e.POST(endpoint, id.NewSchemaID()). WithHeader("authorization", "Bearer secret_abc"). Expect(). Status(http.StatusUnauthorized) - e.POST(endpoint, id.NewModelID()). + e.POST(endpoint, id.NewSchemaID()). WithHeader("authorization", "Bearer "+secret). Expect(). Status(http.StatusNotFound) @@ -38,7 +38,7 @@ func TestIntegrationFieldCreateAPI(t *testing.T) { Expect(). Status(http.StatusBadRequest) - obj1 := e.POST(endpoint, mId1). + obj1 := e.POST(endpoint, sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", @@ -72,10 +72,10 @@ func TestIntegrationFieldCreateAPI(t *testing.T) { // PATCH /api/models/{modelId}/fields/{FieldIdOrKey} func TestIntegrationFieldUpdateAPI(t *testing.T) { - endpoint := "/api/models/{modelId}/fields/{fieldIdOrKey}" + endpoint := "/api/schemata/{schemaId}/fields/{fieldIdOrKey}" e := StartServer(t, &app.Config{}, true, baseSeeder) - obj := e.POST("/api/models/{modelId}/fields", mId1). + obj := e.POST("/api/schemata/{schemaId}/fields", sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", @@ -91,22 +91,22 @@ func TestIntegrationFieldUpdateAPI(t *testing.T) { Expect(). Status(http.StatusUnauthorized) - e.PATCH(endpoint, id.NewModelID(), id.NewFieldID()). + e.PATCH(endpoint, id.NewSchemaID(), id.NewFieldID()). WithHeader("authorization", "secret_abc"). Expect(). Status(http.StatusUnauthorized) - e.PATCH(endpoint, id.NewModelID(), id.NewFieldID()). + e.PATCH(endpoint, id.NewSchemaID(), id.NewFieldID()). WithHeader("authorization", "Bearer secret_abc"). Expect(). Status(http.StatusUnauthorized) - e.PATCH(endpoint, id.NewModelID(), id.NewFieldID()). + e.PATCH(endpoint, id.NewSchemaID(), id.NewFieldID()). WithHeader("authorization", "Bearer "+secret). Expect(). Status(http.StatusNotFound) - e.PATCH(endpoint, id.NewModelID(), fId). + e.PATCH(endpoint, id.NewSchemaID(), fId). WithHeader("authorization", "Bearer "+secret). Expect(). Status(http.StatusNotFound) @@ -117,7 +117,7 @@ func TestIntegrationFieldUpdateAPI(t *testing.T) { Expect(). Status(http.StatusBadRequest) - e.PATCH(endpoint, mId1, fId). + e.PATCH(endpoint, sid1, fId). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1Updated", @@ -135,7 +135,7 @@ func TestIntegrationFieldUpdateAPI(t *testing.T) { HasValue("multiple", true). HasValue("required", true) - e.PATCH(endpoint, mId1, "fKey1Updated"). + e.PATCH(endpoint, sid1, "fKey1Updated"). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1Updated1", @@ -174,10 +174,10 @@ func TestIntegrationFieldUpdateAPI(t *testing.T) { // DELETE /api/models/{modelId}/fields/{FieldIdOrKey} func TestIntegrationFieldDeleteAPI(t *testing.T) { - endpoint := "/api/models/{modelId}/fields/{fieldIdOrKey}" + endpoint := "/api/schemata/{schemaId}/fields/{fieldIdOrKey}" e := StartServer(t, &app.Config{}, true, baseSeeder) - obj := e.POST("/api/models/{modelId}/fields", mId1). + obj := e.POST("/api/schemata/{schemaId}/fields", sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", @@ -189,26 +189,26 @@ func TestIntegrationFieldDeleteAPI(t *testing.T) { Status(http.StatusOK) fId := obj.JSON().Object().Value("id").String().Raw() - e.DELETE(endpoint, id.NewModelID(), id.NewFieldID()). + e.DELETE(endpoint, id.NewSchemaID(), id.NewFieldID()). Expect(). Status(http.StatusUnauthorized) - e.DELETE(endpoint, id.NewModelID(), id.NewFieldID()). + e.DELETE(endpoint, id.NewSchemaID(), id.NewFieldID()). WithHeader("authorization", "secret_abc"). Expect(). Status(http.StatusUnauthorized) - e.DELETE(endpoint, id.NewModelID(), id.NewFieldID()). + e.DELETE(endpoint, id.NewSchemaID(), id.NewFieldID()). WithHeader("authorization", "Bearer secret_abc"). Expect(). Status(http.StatusUnauthorized) - e.DELETE(endpoint, id.NewModelID(), id.NewFieldID()). + e.DELETE(endpoint, id.NewSchemaID(), id.NewFieldID()). WithHeader("authorization", "Bearer "+secret). Expect(). Status(http.StatusNotFound) - e.DELETE(endpoint, id.NewModelID(), fId). + e.DELETE(endpoint, id.NewSchemaID(), fId). WithHeader("authorization", "Bearer "+secret). Expect(). Status(http.StatusNotFound) @@ -219,7 +219,7 @@ func TestIntegrationFieldDeleteAPI(t *testing.T) { Expect(). Status(http.StatusBadRequest) - e.DELETE(endpoint, mId1, fId). + e.DELETE(endpoint, sid1, fId). WithHeader("authorization", "Bearer "+secret). Expect(). Status(http.StatusOK). @@ -227,7 +227,7 @@ func TestIntegrationFieldDeleteAPI(t *testing.T) { Object(). HasValue("id", fId) - obj = e.POST("/api/models/{modelId}/fields", mId1). + obj = e.POST("/api/schemata/{schemaId}/fields", sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", @@ -239,7 +239,7 @@ func TestIntegrationFieldDeleteAPI(t *testing.T) { Status(http.StatusOK) fId = obj.JSON().Object().Value("id").String().Raw() - e.DELETE(endpoint, mId1, "fKey1"). + e.DELETE(endpoint, sid1, "fKey1"). WithHeader("authorization", "Bearer "+secret). Expect(). Status(http.StatusOK). @@ -340,7 +340,7 @@ func TestIntegrationFieldUpdateWithProjectAPI(t *testing.T) { endpoint := "/api/projects/{projectIdOrAlias}/models/{modelIdOrKey}/fields/{fieldIdOrKey}" e := StartServer(t, &app.Config{}, true, baseSeeder) - obj := e.POST("/api/models/{modelId}/fields", mId1). + obj := e.POST("/api/schemata/{schemaId}/fields", sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", @@ -436,7 +436,7 @@ func TestIntegrationFieldDeleteWithProjectAPI(t *testing.T) { endpoint := "/api/projects/{projectIdOrAlias}/models/{modelIdOrKey}/fields/{fieldIdOrKey}" e := StartServer(t, &app.Config{}, true, baseSeeder) - obj := e.POST("/api/models/{modelId}/fields", mId1). + obj := e.POST("/api/schemata/{schemaId}/fields", sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", @@ -480,7 +480,7 @@ func TestIntegrationFieldDeleteWithProjectAPI(t *testing.T) { Object(). HasValue("id", fId) - obj = e.POST("/api/models/{modelId}/fields", mId1). + obj = e.POST("/api/schemata/{schemaId}/fields", sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", @@ -500,7 +500,7 @@ func TestIntegrationFieldDeleteWithProjectAPI(t *testing.T) { Object(). HasValue("id", fId) - obj = e.POST("/api/models/{modelId}/fields", mId1). + obj = e.POST("/api/schemata/{schemaId}/fields", sid1). WithHeader("authorization", "Bearer "+secret). WithJSON(map[string]interface{}{ "key": "fKey1", diff --git a/server/internal/adapter/integration/model.go b/server/internal/adapter/integration/model.go index 7fd92a7f34..d40581102b 100644 --- a/server/internal/adapter/integration/model.go +++ b/server/internal/adapter/integration/model.go @@ -31,11 +31,15 @@ func (s *Server) ModelFilter(ctx context.Context, request ModelFilterRequestObje models := make([]integrationapi.Model, 0, len(ms)) for _, m := range ms { + sp, err := uc.Schema.FindByModel(ctx, m.ID(), op) + if err != nil { + return nil, err + } lastModified, err := uc.Item.LastModifiedByModel(ctx, m.ID(), op) if err != nil && !errors.Is(err, rerror.ErrNotFound) { return nil, err } - models = append(models, integrationapi.NewModel(m, lastModified)) + models = append(models, integrationapi.NewModel(m, sp, lastModified)) } return ModelFilter200JSONResponse{ @@ -73,12 +77,17 @@ func (s *Server) ModelCreate(ctx context.Context, request ModelCreateRequestObje return ModelCreate400Response{}, err } + sp, err := uc.Schema.FindByModel(ctx, m.ID(), op) + if err != nil { + return nil, err + } + lastModified, err := uc.Item.LastModifiedByModel(ctx, m.ID(), op) if err != nil && !errors.Is(err, rerror.ErrNotFound) { return nil, err } - return ModelCreate200JSONResponse(integrationapi.NewModel(m, lastModified)), nil + return ModelCreate200JSONResponse(integrationapi.NewModel(m, sp, lastModified)), nil } func (s *Server) ModelGet(ctx context.Context, request ModelGetRequestObject) (ModelGetResponseObject, error) { @@ -90,12 +99,17 @@ func (s *Server) ModelGet(ctx context.Context, request ModelGetRequestObject) (M return nil, err } + sp, err := uc.Schema.FindByModel(ctx, m.ID(), op) + if err != nil { + return nil, err + } + lastModified, err := uc.Item.LastModifiedByModel(ctx, request.ModelId, op) if err != nil && !errors.Is(err, rerror.ErrNotFound) { return nil, err } - return ModelGet200JSONResponse(integrationapi.NewModel(m, lastModified)), nil + return ModelGet200JSONResponse(integrationapi.NewModel(m, sp, lastModified)), nil } func (s *Server) ModelGetWithProject(ctx context.Context, request ModelGetWithProjectRequestObject) (ModelGetWithProjectResponseObject, error) { @@ -118,6 +132,11 @@ func (s *Server) ModelGetWithProject(ctx context.Context, request ModelGetWithPr return ModelGetWithProject500Response{}, nil } + sp, err := uc.Schema.FindByModel(ctx, m.ID(), op) + if err != nil { + return nil, err + } + lastModified, err := uc.Item.LastModifiedByModel(ctx, m.ID(), op) if err != nil { if errors.Is(err, rerror.ErrNotFound) { @@ -126,7 +145,7 @@ func (s *Server) ModelGetWithProject(ctx context.Context, request ModelGetWithPr return ModelGetWithProject500Response{}, nil } - return ModelGetWithProject200JSONResponse(integrationapi.NewModel(m, lastModified)), nil + return ModelGetWithProject200JSONResponse(integrationapi.NewModel(m, sp, lastModified)), nil } func (s *Server) ModelUpdate(ctx context.Context, request ModelUpdateRequestObject) (ModelUpdateResponseObject, error) { @@ -148,12 +167,17 @@ func (s *Server) ModelUpdate(ctx context.Context, request ModelUpdateRequestObje return ModelUpdate400Response{}, err } + sp, err := uc.Schema.FindByModel(ctx, m.ID(), op) + if err != nil { + return nil, err + } + lastModified, err := uc.Item.LastModifiedByModel(ctx, request.ModelId, op) if err != nil { return nil, err } - return ModelUpdate200JSONResponse(integrationapi.NewModel(m, lastModified)), nil + return ModelUpdate200JSONResponse(integrationapi.NewModel(m, sp, lastModified)), nil } func (s *Server) ModelUpdateWithProject(ctx context.Context, request ModelUpdateWithProjectRequestObject) (ModelUpdateWithProjectResponseObject, error) { @@ -191,12 +215,17 @@ func (s *Server) ModelUpdateWithProject(ctx context.Context, request ModelUpdate return ModelUpdateWithProject400Response{}, err } + sp, err := uc.Schema.FindByModel(ctx, m.ID(), op) + if err != nil { + return nil, err + } + lastModified, err := uc.Item.LastModifiedByModel(ctx, m.ID(), op) if err != nil { return nil, err } - return ModelUpdateWithProject200JSONResponse(integrationapi.NewModel(m, lastModified)), nil + return ModelUpdateWithProject200JSONResponse(integrationapi.NewModel(m, sp, lastModified)), nil } func (s *Server) ModelDelete(ctx context.Context, request ModelDeleteRequestObject) (ModelDeleteResponseObject, error) { diff --git a/server/internal/adapter/integration/schema.go b/server/internal/adapter/integration/schema.go index 7c9513b95e..5ef21515ec 100644 --- a/server/internal/adapter/integration/schema.go +++ b/server/internal/adapter/integration/schema.go @@ -24,7 +24,7 @@ func (s *Server) FieldCreate(ctx context.Context, request FieldCreateRequestObje uc := adapter.Usecases(ctx) op := adapter.Operator(ctx) - sch, err := uc.Schema.FindByModel(ctx, request.ModelId, op) + sch, err := uc.Schema.FindByID(ctx, request.SchemaId, op) if err != nil { if errors.Is(err, rerror.ErrNotFound) { return FieldCreate400Response{}, err @@ -32,14 +32,19 @@ func (s *Server) FieldCreate(ctx context.Context, request FieldCreateRequestObje return FieldCreate400Response{}, err } + m, err := uc.Model.FindBySchema(ctx, sch.ID(), op) + if err != nil { + return FieldCreate400Response{}, err + } + tp, dv, err := FromSchemaTypeProperty(*request.Body.Type, *request.Body.Multiple) if err != nil { return nil, err } param := interfaces.CreateFieldParam{ - ModelID: &request.ModelId, - SchemaID: sch.Schema().ID(), + ModelID: m.ID().Ref(), + SchemaID: sch.ID(), Type: integrationapi.FromValueType(request.Body.Type), Name: *request.Body.Key, Description: nil, @@ -130,7 +135,7 @@ func (s *Server) FieldUpdate(ctx context.Context, request FieldUpdateRequestObje uc := adapter.Usecases(ctx) op := adapter.Operator(ctx) - sch, err := uc.Schema.FindByModel(ctx, request.ModelId, op) + sch, err := uc.Schema.FindByID(ctx, request.SchemaId, op) if err != nil { if errors.Is(err, rerror.ErrNotFound) { return FieldUpdate400Response{}, err @@ -138,6 +143,11 @@ func (s *Server) FieldUpdate(ctx context.Context, request FieldUpdateRequestObje return FieldUpdate400Response{}, err } + m, err := uc.Model.FindBySchema(ctx, sch.ID(), op) + if err != nil { + return FieldUpdate400Response{}, err + } + idOrKey := (*string)(&request.FieldIdOrKey) f := sch.FieldByIDOrKey(id.FieldIDFromRef(idOrKey), id.NewKeyFromPtr(idOrKey)) if f == nil { @@ -146,8 +156,8 @@ func (s *Server) FieldUpdate(ctx context.Context, request FieldUpdateRequestObje param := interfaces.UpdateFieldParam{ FieldID: f.ID(), - ModelID: &request.ModelId, - SchemaID: sch.Schema().ID(), + ModelID: m.ID().Ref(), + SchemaID: sch.ID(), Name: request.Body.Key, Description: nil, Key: request.Body.Key, @@ -243,7 +253,7 @@ func (s *Server) FieldDelete(ctx context.Context, request FieldDeleteRequestObje uc := adapter.Usecases(ctx) op := adapter.Operator(ctx) - sch, err := uc.Schema.FindByModel(ctx, request.ModelId, op) + sch, err := uc.Schema.FindByID(ctx, request.SchemaId, op) if err != nil { if errors.Is(err, rerror.ErrNotFound) { return FieldDelete400Response{}, err @@ -257,7 +267,7 @@ func (s *Server) FieldDelete(ctx context.Context, request FieldDeleteRequestObje return FieldDelete400Response{}, rerror.ErrNotFound } - err = uc.Schema.DeleteField(ctx, sch.Schema().ID(), f.ID(), op) + err = uc.Schema.DeleteField(ctx, sch.ID(), f.ID(), op) if err != nil { return FieldDelete400Response{}, err } diff --git a/server/internal/adapter/integration/server.gen.go b/server/internal/adapter/integration/server.gen.go index ac6b9f1f60..dc98ef71b4 100644 --- a/server/internal/adapter/integration/server.gen.go +++ b/server/internal/adapter/integration/server.gen.go @@ -74,15 +74,6 @@ type ServerInterface interface { // Update a model. // (PATCH /models/{modelId}) ModelUpdate(ctx echo.Context, modelId ModelIdParam) error - // create a field - // (POST /models/{modelId}/fields) - FieldCreate(ctx echo.Context, modelId ModelIdParam) error - // delete a field - // (DELETE /models/{modelId}/fields/{fieldIdOrKey}) - FieldDelete(ctx echo.Context, modelId ModelIdParam, fieldIdOrKey FieldIdOrKeyParam) error - // update a field - // (PATCH /models/{modelId}/fields/{fieldIdOrKey}) - FieldUpdate(ctx echo.Context, modelId ModelIdParam, fieldIdOrKey FieldIdOrKeyParam) error // Returns a list of items. // (GET /models/{modelId}/items) ItemFilter(ctx echo.Context, modelId ModelIdParam, params ItemFilterParams) error @@ -128,6 +119,15 @@ type ServerInterface interface { // Upload an asset. // (POST /projects/{projectId}/assets/uploads) AssetUploadCreate(ctx echo.Context, projectId ProjectIdParam) error + // create a field + // (POST /schemata/{schemaId}/fields) + FieldCreate(ctx echo.Context, schemaId SchemaIdParam) error + // delete a field + // (DELETE /schemata/{schemaId}/fields/{fieldIdOrKey}) + FieldDelete(ctx echo.Context, schemaId SchemaIdParam, fieldIdOrKey FieldIdOrKeyParam) error + // update a field + // (PATCH /schemata/{schemaId}/fields/{fieldIdOrKey}) + FieldUpdate(ctx echo.Context, schemaId SchemaIdParam, fieldIdOrKey FieldIdOrKeyParam) error // Returns a list of projects. // (GET /{workspaceId}/projects) ProjectFilter(ctx echo.Context, workspaceId WorkspaceIdParam, params ProjectFilterParams) error @@ -470,76 +470,6 @@ func (w *ServerInterfaceWrapper) ModelUpdate(ctx echo.Context) error { return err } -// FieldCreate converts echo context to params. -func (w *ServerInterfaceWrapper) FieldCreate(ctx echo.Context) error { - var err error - // ------------- Path parameter "modelId" ------------- - var modelId ModelIdParam - - err = runtime.BindStyledParameterWithOptions("simple", "modelId", ctx.Param("modelId"), &modelId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter modelId: %s", err)) - } - - ctx.Set(BearerAuthScopes, []string{}) - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.FieldCreate(ctx, modelId) - return err -} - -// FieldDelete converts echo context to params. -func (w *ServerInterfaceWrapper) FieldDelete(ctx echo.Context) error { - var err error - // ------------- Path parameter "modelId" ------------- - var modelId ModelIdParam - - err = runtime.BindStyledParameterWithOptions("simple", "modelId", ctx.Param("modelId"), &modelId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter modelId: %s", err)) - } - - // ------------- Path parameter "fieldIdOrKey" ------------- - var fieldIdOrKey FieldIdOrKeyParam - - err = runtime.BindStyledParameterWithOptions("simple", "fieldIdOrKey", ctx.Param("fieldIdOrKey"), &fieldIdOrKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter fieldIdOrKey: %s", err)) - } - - ctx.Set(BearerAuthScopes, []string{}) - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.FieldDelete(ctx, modelId, fieldIdOrKey) - return err -} - -// FieldUpdate converts echo context to params. -func (w *ServerInterfaceWrapper) FieldUpdate(ctx echo.Context) error { - var err error - // ------------- Path parameter "modelId" ------------- - var modelId ModelIdParam - - err = runtime.BindStyledParameterWithOptions("simple", "modelId", ctx.Param("modelId"), &modelId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter modelId: %s", err)) - } - - // ------------- Path parameter "fieldIdOrKey" ------------- - var fieldIdOrKey FieldIdOrKeyParam - - err = runtime.BindStyledParameterWithOptions("simple", "fieldIdOrKey", ctx.Param("fieldIdOrKey"), &fieldIdOrKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter fieldIdOrKey: %s", err)) - } - - ctx.Set(BearerAuthScopes, []string{}) - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.FieldUpdate(ctx, modelId, fieldIdOrKey) - return err -} - // ItemFilter converts echo context to params. func (w *ServerInterfaceWrapper) ItemFilter(ctx echo.Context) error { var err error @@ -1047,6 +977,76 @@ func (w *ServerInterfaceWrapper) AssetUploadCreate(ctx echo.Context) error { return err } +// FieldCreate converts echo context to params. +func (w *ServerInterfaceWrapper) FieldCreate(ctx echo.Context) error { + var err error + // ------------- Path parameter "schemaId" ------------- + var schemaId SchemaIdParam + + err = runtime.BindStyledParameterWithOptions("simple", "schemaId", ctx.Param("schemaId"), &schemaId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter schemaId: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.FieldCreate(ctx, schemaId) + return err +} + +// FieldDelete converts echo context to params. +func (w *ServerInterfaceWrapper) FieldDelete(ctx echo.Context) error { + var err error + // ------------- Path parameter "schemaId" ------------- + var schemaId SchemaIdParam + + err = runtime.BindStyledParameterWithOptions("simple", "schemaId", ctx.Param("schemaId"), &schemaId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter schemaId: %s", err)) + } + + // ------------- Path parameter "fieldIdOrKey" ------------- + var fieldIdOrKey FieldIdOrKeyParam + + err = runtime.BindStyledParameterWithOptions("simple", "fieldIdOrKey", ctx.Param("fieldIdOrKey"), &fieldIdOrKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter fieldIdOrKey: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.FieldDelete(ctx, schemaId, fieldIdOrKey) + return err +} + +// FieldUpdate converts echo context to params. +func (w *ServerInterfaceWrapper) FieldUpdate(ctx echo.Context) error { + var err error + // ------------- Path parameter "schemaId" ------------- + var schemaId SchemaIdParam + + err = runtime.BindStyledParameterWithOptions("simple", "schemaId", ctx.Param("schemaId"), &schemaId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter schemaId: %s", err)) + } + + // ------------- Path parameter "fieldIdOrKey" ------------- + var fieldIdOrKey FieldIdOrKeyParam + + err = runtime.BindStyledParameterWithOptions("simple", "fieldIdOrKey", ctx.Param("fieldIdOrKey"), &fieldIdOrKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter fieldIdOrKey: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.FieldUpdate(ctx, schemaId, fieldIdOrKey) + return err +} + // ProjectFilter converts echo context to params. func (w *ServerInterfaceWrapper) ProjectFilter(ctx echo.Context) error { var err error @@ -1125,9 +1125,6 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.DELETE(baseURL+"/models/:modelId", wrapper.ModelDelete) router.GET(baseURL+"/models/:modelId", wrapper.ModelGet) router.PATCH(baseURL+"/models/:modelId", wrapper.ModelUpdate) - router.POST(baseURL+"/models/:modelId/fields", wrapper.FieldCreate) - router.DELETE(baseURL+"/models/:modelId/fields/:fieldIdOrKey", wrapper.FieldDelete) - router.PATCH(baseURL+"/models/:modelId/fields/:fieldIdOrKey", wrapper.FieldUpdate) router.GET(baseURL+"/models/:modelId/items", wrapper.ItemFilter) router.POST(baseURL+"/models/:modelId/items", wrapper.ItemCreate) router.GET(baseURL+"/projects/:projectIdOrAlias/models", wrapper.ModelFilter) @@ -1143,6 +1140,9 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/projects/:projectId/assets", wrapper.AssetFilter) router.POST(baseURL+"/projects/:projectId/assets", wrapper.AssetCreate) router.POST(baseURL+"/projects/:projectId/assets/uploads", wrapper.AssetUploadCreate) + router.POST(baseURL+"/schemata/:schemaId/fields", wrapper.FieldCreate) + router.DELETE(baseURL+"/schemata/:schemaId/fields/:fieldIdOrKey", wrapper.FieldDelete) + router.PATCH(baseURL+"/schemata/:schemaId/fields/:fieldIdOrKey", wrapper.FieldUpdate) router.GET(baseURL+"/:workspaceId/projects", wrapper.ProjectFilter) } @@ -1826,108 +1826,6 @@ func (response ModelUpdate401Response) VisitModelUpdateResponse(w http.ResponseW return nil } -type FieldCreateRequestObject struct { - ModelId ModelIdParam `json:"modelId"` - Body *FieldCreateJSONRequestBody -} - -type FieldCreateResponseObject interface { - VisitFieldCreateResponse(w http.ResponseWriter) error -} - -type FieldCreate200JSONResponse SchemaField - -func (response FieldCreate200JSONResponse) VisitFieldCreateResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type FieldCreate400Response struct { -} - -func (response FieldCreate400Response) VisitFieldCreateResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type FieldCreate401Response = UnauthorizedErrorResponse - -func (response FieldCreate401Response) VisitFieldCreateResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type FieldDeleteRequestObject struct { - ModelId ModelIdParam `json:"modelId"` - FieldIdOrKey FieldIdOrKeyParam `json:"fieldIdOrKey"` -} - -type FieldDeleteResponseObject interface { - VisitFieldDeleteResponse(w http.ResponseWriter) error -} - -type FieldDelete200JSONResponse struct { - Id *id.FieldID `json:"id,omitempty"` -} - -func (response FieldDelete200JSONResponse) VisitFieldDeleteResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type FieldDelete400Response struct { -} - -func (response FieldDelete400Response) VisitFieldDeleteResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type FieldDelete401Response = UnauthorizedErrorResponse - -func (response FieldDelete401Response) VisitFieldDeleteResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type FieldUpdateRequestObject struct { - ModelId ModelIdParam `json:"modelId"` - FieldIdOrKey FieldIdOrKeyParam `json:"fieldIdOrKey"` - Body *FieldUpdateJSONRequestBody -} - -type FieldUpdateResponseObject interface { - VisitFieldUpdateResponse(w http.ResponseWriter) error -} - -type FieldUpdate200JSONResponse SchemaField - -func (response FieldUpdate200JSONResponse) VisitFieldUpdateResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type FieldUpdate400Response struct { -} - -func (response FieldUpdate400Response) VisitFieldUpdateResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type FieldUpdate401Response = UnauthorizedErrorResponse - -func (response FieldUpdate401Response) VisitFieldUpdateResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - type ItemFilterRequestObject struct { ModelId ModelIdParam `json:"modelId"` Params ItemFilterParams @@ -2599,6 +2497,108 @@ func (response AssetUploadCreate404Response) VisitAssetUploadCreateResponse(w ht return nil } +type FieldCreateRequestObject struct { + SchemaId SchemaIdParam `json:"schemaId"` + Body *FieldCreateJSONRequestBody +} + +type FieldCreateResponseObject interface { + VisitFieldCreateResponse(w http.ResponseWriter) error +} + +type FieldCreate200JSONResponse SchemaField + +func (response FieldCreate200JSONResponse) VisitFieldCreateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type FieldCreate400Response struct { +} + +func (response FieldCreate400Response) VisitFieldCreateResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type FieldCreate401Response = UnauthorizedErrorResponse + +func (response FieldCreate401Response) VisitFieldCreateResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type FieldDeleteRequestObject struct { + SchemaId SchemaIdParam `json:"schemaId"` + FieldIdOrKey FieldIdOrKeyParam `json:"fieldIdOrKey"` +} + +type FieldDeleteResponseObject interface { + VisitFieldDeleteResponse(w http.ResponseWriter) error +} + +type FieldDelete200JSONResponse struct { + Id *id.FieldID `json:"id,omitempty"` +} + +func (response FieldDelete200JSONResponse) VisitFieldDeleteResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type FieldDelete400Response struct { +} + +func (response FieldDelete400Response) VisitFieldDeleteResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type FieldDelete401Response = UnauthorizedErrorResponse + +func (response FieldDelete401Response) VisitFieldDeleteResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type FieldUpdateRequestObject struct { + SchemaId SchemaIdParam `json:"schemaId"` + FieldIdOrKey FieldIdOrKeyParam `json:"fieldIdOrKey"` + Body *FieldUpdateJSONRequestBody +} + +type FieldUpdateResponseObject interface { + VisitFieldUpdateResponse(w http.ResponseWriter) error +} + +type FieldUpdate200JSONResponse SchemaField + +func (response FieldUpdate200JSONResponse) VisitFieldUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type FieldUpdate400Response struct { +} + +func (response FieldUpdate400Response) VisitFieldUpdateResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type FieldUpdate401Response = UnauthorizedErrorResponse + +func (response FieldUpdate401Response) VisitFieldUpdateResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + type ProjectFilterRequestObject struct { WorkspaceId WorkspaceIdParam `json:"workspaceId"` Params ProjectFilterParams @@ -2702,15 +2702,6 @@ type StrictServerInterface interface { // Update a model. // (PATCH /models/{modelId}) ModelUpdate(ctx context.Context, request ModelUpdateRequestObject) (ModelUpdateResponseObject, error) - // create a field - // (POST /models/{modelId}/fields) - FieldCreate(ctx context.Context, request FieldCreateRequestObject) (FieldCreateResponseObject, error) - // delete a field - // (DELETE /models/{modelId}/fields/{fieldIdOrKey}) - FieldDelete(ctx context.Context, request FieldDeleteRequestObject) (FieldDeleteResponseObject, error) - // update a field - // (PATCH /models/{modelId}/fields/{fieldIdOrKey}) - FieldUpdate(ctx context.Context, request FieldUpdateRequestObject) (FieldUpdateResponseObject, error) // Returns a list of items. // (GET /models/{modelId}/items) ItemFilter(ctx context.Context, request ItemFilterRequestObject) (ItemFilterResponseObject, error) @@ -2756,6 +2747,15 @@ type StrictServerInterface interface { // Upload an asset. // (POST /projects/{projectId}/assets/uploads) AssetUploadCreate(ctx context.Context, request AssetUploadCreateRequestObject) (AssetUploadCreateResponseObject, error) + // create a field + // (POST /schemata/{schemaId}/fields) + FieldCreate(ctx context.Context, request FieldCreateRequestObject) (FieldCreateResponseObject, error) + // delete a field + // (DELETE /schemata/{schemaId}/fields/{fieldIdOrKey}) + FieldDelete(ctx context.Context, request FieldDeleteRequestObject) (FieldDeleteResponseObject, error) + // update a field + // (PATCH /schemata/{schemaId}/fields/{fieldIdOrKey}) + FieldUpdate(ctx context.Context, request FieldUpdateRequestObject) (FieldUpdateResponseObject, error) // Returns a list of projects. // (GET /{workspaceId}/projects) ProjectFilter(ctx context.Context, request ProjectFilterRequestObject) (ProjectFilterResponseObject, error) @@ -3214,95 +3214,6 @@ func (sh *strictHandler) ModelUpdate(ctx echo.Context, modelId ModelIdParam) err return nil } -// FieldCreate operation middleware -func (sh *strictHandler) FieldCreate(ctx echo.Context, modelId ModelIdParam) error { - var request FieldCreateRequestObject - - request.ModelId = modelId - - var body FieldCreateJSONRequestBody - if err := ctx.Bind(&body); err != nil { - return err - } - request.Body = &body - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.FieldCreate(ctx.Request().Context(), request.(FieldCreateRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "FieldCreate") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(FieldCreateResponseObject); ok { - return validResponse.VisitFieldCreateResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// FieldDelete operation middleware -func (sh *strictHandler) FieldDelete(ctx echo.Context, modelId ModelIdParam, fieldIdOrKey FieldIdOrKeyParam) error { - var request FieldDeleteRequestObject - - request.ModelId = modelId - request.FieldIdOrKey = fieldIdOrKey - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.FieldDelete(ctx.Request().Context(), request.(FieldDeleteRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "FieldDelete") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(FieldDeleteResponseObject); ok { - return validResponse.VisitFieldDeleteResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// FieldUpdate operation middleware -func (sh *strictHandler) FieldUpdate(ctx echo.Context, modelId ModelIdParam, fieldIdOrKey FieldIdOrKeyParam) error { - var request FieldUpdateRequestObject - - request.ModelId = modelId - request.FieldIdOrKey = fieldIdOrKey - - var body FieldUpdateJSONRequestBody - if err := ctx.Bind(&body); err != nil { - return err - } - request.Body = &body - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.FieldUpdate(ctx.Request().Context(), request.(FieldUpdateRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "FieldUpdate") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(FieldUpdateResponseObject); ok { - return validResponse.VisitFieldUpdateResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - // ItemFilter operation middleware func (sh *strictHandler) ItemFilter(ctx echo.Context, modelId ModelIdParam, params ItemFilterParams) error { var request ItemFilterRequestObject @@ -3755,6 +3666,95 @@ func (sh *strictHandler) AssetUploadCreate(ctx echo.Context, projectId ProjectId return nil } +// FieldCreate operation middleware +func (sh *strictHandler) FieldCreate(ctx echo.Context, schemaId SchemaIdParam) error { + var request FieldCreateRequestObject + + request.SchemaId = schemaId + + var body FieldCreateJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.FieldCreate(ctx.Request().Context(), request.(FieldCreateRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "FieldCreate") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(FieldCreateResponseObject); ok { + return validResponse.VisitFieldCreateResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// FieldDelete operation middleware +func (sh *strictHandler) FieldDelete(ctx echo.Context, schemaId SchemaIdParam, fieldIdOrKey FieldIdOrKeyParam) error { + var request FieldDeleteRequestObject + + request.SchemaId = schemaId + request.FieldIdOrKey = fieldIdOrKey + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.FieldDelete(ctx.Request().Context(), request.(FieldDeleteRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "FieldDelete") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(FieldDeleteResponseObject); ok { + return validResponse.VisitFieldDeleteResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// FieldUpdate operation middleware +func (sh *strictHandler) FieldUpdate(ctx echo.Context, schemaId SchemaIdParam, fieldIdOrKey FieldIdOrKeyParam) error { + var request FieldUpdateRequestObject + + request.SchemaId = schemaId + request.FieldIdOrKey = fieldIdOrKey + + var body FieldUpdateJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.FieldUpdate(ctx.Request().Context(), request.(FieldUpdateRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "FieldUpdate") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(FieldUpdateResponseObject); ok { + return validResponse.VisitFieldUpdateResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // ProjectFilter operation middleware func (sh *strictHandler) ProjectFilter(ctx echo.Context, workspaceId WorkspaceIdParam, params ProjectFilterParams) error { var request ProjectFilterRequestObject @@ -3784,62 +3784,63 @@ func (sh *strictHandler) ProjectFilter(ctx echo.Context, workspaceId WorkspaceId // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "ywMnMavV6v8BAAD//8rqAEeyfAAA", + "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", + "2HhV3vhI6Z5Thuv1+v8BAAD//8KIn+ESfwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/internal/infrastructure/memory/model.go b/server/internal/infrastructure/memory/model.go index f309fbfbdf..e405b54fc6 100644 --- a/server/internal/infrastructure/memory/model.go +++ b/server/internal/infrastructure/memory/model.go @@ -134,6 +134,21 @@ func (r *Model) FindByID(_ context.Context, mid id.ModelID) (*model.Model, error return nil, rerror.ErrNotFound } +func (r *Model) FindBySchema(_ context.Context, sid id.SchemaID) (*model.Model, error) { + if r.err != nil { + return nil, r.err + } + + m := r.data.Find(func(_ id.ModelID, m *model.Model) bool { + return (m.Schema() == sid || (m.Metadata() != nil && *m.Metadata() == sid)) && r.f.CanRead(m.Project()) + }) + + if m != nil { + return m, nil + } + return nil, rerror.ErrNotFound +} + func (r *Model) FindByIDs(_ context.Context, ids id.ModelIDList) (model.List, error) { if r.err != nil { return nil, r.err diff --git a/server/internal/infrastructure/mongo/model.go b/server/internal/infrastructure/mongo/model.go index dba7844729..41163387c3 100644 --- a/server/internal/infrastructure/mongo/model.go +++ b/server/internal/infrastructure/mongo/model.go @@ -2,6 +2,7 @@ package mongo import ( "context" + "github.com/reearth/reearth-cms/server/internal/infrastructure/mongo/mongodoc" "github.com/reearth/reearth-cms/server/internal/usecase/repo" "github.com/reearth/reearth-cms/server/pkg/id" @@ -43,6 +44,15 @@ func (r *Model) FindByID(ctx context.Context, modelID id.ModelID) (*model.Model, }) } +func (r *Model) FindBySchema(ctx context.Context, schemaID id.SchemaID) (*model.Model, error) { + return r.findOne(ctx, bson.M{ + "$or": bson.A{ + bson.M{"schema": schemaID.String()}, + bson.M{"metadata": schemaID.String()}, + }, + }) +} + func (r *Model) FindByIDs(ctx context.Context, ids id.ModelIDList) (model.List, error) { if len(ids) == 0 { return nil, nil diff --git a/server/internal/infrastructure/mongo/model_test.go b/server/internal/infrastructure/mongo/model_test.go index d7e16449c0..d593103700 100644 --- a/server/internal/infrastructure/mongo/model_test.go +++ b/server/internal/infrastructure/mongo/model_test.go @@ -141,6 +141,115 @@ func TestModelRepo_FindByID(t *testing.T) { } } +func TestModelRepo_FindBySchema(t *testing.T) { + mocknow := time.Now().Truncate(time.Millisecond).UTC() + pid1 := id.NewProjectID() + id1 := id.NewModelID() + sid1 := id.NewSchemaID() + k := key.New("T123456") + m1 := model.New().ID(id1).Project(pid1).Schema(sid1).Key(k).UpdatedAt(mocknow).MustBuild() + + tests := []struct { + name string + seeds model.List + arg id.SchemaID + filter *repo.ProjectFilter + want *model.Model + wantErr error + }{ + { + name: "Not found in empty db", + seeds: model.List{}, + arg: id.NewSchemaID(), + want: nil, + wantErr: rerror.ErrNotFound, + }, + { + name: "Not found", + seeds: model.List{ + model.New().NewID().Project(pid1).Schema(sid1).Key(k).UpdatedAt(mocknow).MustBuild(), + }, + arg: id.NewSchemaID(), + want: nil, + wantErr: rerror.ErrNotFound, + }, + { + name: "Found 1", + seeds: model.List{ + m1, + }, + arg: sid1, + want: m1, + wantErr: nil, + }, + { + name: "Found 2", + seeds: model.List{ + m1, + model.New().NewID().Project(id.NewProjectID()).Schema(id.NewSchemaID()).Key(k).UpdatedAt(mocknow).MustBuild(), + model.New().NewID().Project(id.NewProjectID()).Schema(id.NewSchemaID()).Key(k).UpdatedAt(mocknow).MustBuild(), + }, + arg: sid1, + want: m1, + wantErr: nil, + }, + { + name: "project filter operation success", + seeds: model.List{ + m1, + model.New().NewID().Project(id.NewProjectID()).Schema(id.NewSchemaID()).Key(k).UpdatedAt(mocknow).MustBuild(), + model.New().NewID().Project(id.NewProjectID()).Schema(id.NewSchemaID()).Key(k).UpdatedAt(mocknow).MustBuild(), + }, + arg: sid1, + filter: &repo.ProjectFilter{Readable: []id.ProjectID{pid1}, Writable: []id.ProjectID{pid1}}, + want: m1, + wantErr: nil, + }, + { + name: "project filter operation denied", + seeds: model.List{ + m1, + model.New().NewID().Project(id.NewProjectID()).Schema(id.NewSchemaID()).Key(k).UpdatedAt(mocknow).MustBuild(), + model.New().NewID().Project(id.NewProjectID()).Schema(id.NewSchemaID()).Key(k).UpdatedAt(mocknow).MustBuild(), + }, + arg: sid1, + filter: &repo.ProjectFilter{Readable: []id.ProjectID{}, Writable: []id.ProjectID{}}, + want: nil, + wantErr: nil, + }, + } + + initDB := mongotest.Connect(t) + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client := mongox.NewClientWithDatabase(initDB(t)) + + r := NewModel(client) + ctx := context.Background() + + for _, a := range tc.seeds { + err := r.Save(ctx, a.Clone()) + assert.NoError(t, err) + } + + if tc.filter != nil { + r = r.Filtered(*tc.filter) + } + + got, err := r.FindBySchema(ctx, tc.arg) + if tc.wantErr != nil { + assert.ErrorIs(t, err, tc.wantErr) + return + } + assert.Equal(t, tc.want, got) + }) + } +} + func TestModelRepo_FindByIDs(t *testing.T) { mocknow := time.Now().Truncate(time.Millisecond).UTC() pid1 := id.NewProjectID() diff --git a/server/internal/usecase/interactor/model.go b/server/internal/usecase/interactor/model.go index b3f2ac22f5..d2f26aafe1 100644 --- a/server/internal/usecase/interactor/model.go +++ b/server/internal/usecase/interactor/model.go @@ -34,6 +34,10 @@ func (i Model) FindByID(ctx context.Context, id id.ModelID, operator *usecase.Op return i.repos.Model.FindByID(ctx, id) } +func (i Model) FindBySchema(ctx context.Context, id id.SchemaID, operator *usecase.Operator) (*model.Model, error) { + return i.repos.Model.FindBySchema(ctx, id) +} + func (i Model) FindByIDs(ctx context.Context, ids []id.ModelID, operator *usecase.Operator) (model.List, error) { return i.repos.Model.FindByIDs(ctx, ids) } diff --git a/server/internal/usecase/interfaces/model.go b/server/internal/usecase/interfaces/model.go index 29ae420378..3eb2afffcf 100644 --- a/server/internal/usecase/interfaces/model.go +++ b/server/internal/usecase/interfaces/model.go @@ -2,6 +2,7 @@ package interfaces import ( "context" + "github.com/reearth/reearth-cms/server/internal/usecase" "github.com/reearth/reearth-cms/server/pkg/id" "github.com/reearth/reearth-cms/server/pkg/model" @@ -42,6 +43,7 @@ var ( type Model interface { FindByID(context.Context, id.ModelID, *usecase.Operator) (*model.Model, error) + FindBySchema(context.Context, id.SchemaID, *usecase.Operator) (*model.Model, error) FindByIDs(context.Context, []id.ModelID, *usecase.Operator) (model.List, error) FindByProject(context.Context, id.ProjectID, *usecasex.Pagination, *usecase.Operator) (model.List, *usecasex.PageInfo, error) FindByKey(context.Context, id.ProjectID, string, *usecase.Operator) (*model.Model, error) diff --git a/server/internal/usecase/repo/model.go b/server/internal/usecase/repo/model.go index 269f94e415..0c7456f1de 100644 --- a/server/internal/usecase/repo/model.go +++ b/server/internal/usecase/repo/model.go @@ -11,6 +11,7 @@ import ( type Model interface { Filtered(ProjectFilter) Model FindByID(context.Context, id.ModelID) (*model.Model, error) + FindBySchema(context.Context, id.SchemaID) (*model.Model, error) FindByIDs(context.Context, id.ModelIDList) (model.List, error) FindByProject(context.Context, id.ProjectID, *usecasex.Pagination) (model.List, *usecasex.PageInfo, error) FindByKey(context.Context, id.ProjectID, string) (*model.Model, error) diff --git a/server/pkg/integrationapi/schema.go b/server/pkg/integrationapi/schema.go index 53513dbfdf..3ef8e5bb8f 100644 --- a/server/pkg/integrationapi/schema.go +++ b/server/pkg/integrationapi/schema.go @@ -40,13 +40,13 @@ func NewItemModelSchema(i item.ItemModelSchema, assets *AssetContext) ItemModelS ReferencedItems: lo.Map(i.ReferencedItems, func(itm *version.Value[*item.Item], _ int) *VersionedItem { return lo.ToPtr(NewVersionedItem(itm, nil, nil, nil, nil, nil, nil)) }), - Model: NewModel(i.Model, time.Time{}), + Model: NewModel(i.Model, nil, time.Time{}), Schema: NewSchema(i.Schema), Changes: NewItemFieldChanges(i.Changes), } } -func NewModel(m *model.Model, lastModified time.Time) Model { +func NewModel(m *model.Model, sp *schema.Package, lastModified time.Time) Model { var metadata *id.SchemaID if m.Metadata() != nil { metadata = m.Metadata().Ref() @@ -59,7 +59,9 @@ func NewModel(m *model.Model, lastModified time.Time) Model { Public: util.ToPtrIfNotEmpty(m.Public()), ProjectId: m.Project().Ref(), SchemaId: m.Schema().Ref(), + Schema: util.ToPtrIfNotEmpty(NewSchema(sp.Schema())), MetadataSchemaId: metadata, + MetadataSchema: util.ToPtrIfNotEmpty(NewSchema(sp.MetaSchema())), CreatedAt: lo.ToPtr(m.ID().Timestamp()), UpdatedAt: lo.ToPtr(m.UpdatedAt()), LastModified: util.ToPtrIfNotEmpty(lastModified), @@ -67,6 +69,9 @@ func NewModel(m *model.Model, lastModified time.Time) Model { } func NewSchema(i *schema.Schema) Schema { + if i == nil { + return Schema{} + } fs := lo.Map(i.Fields(), func(f *schema.Field, _ int) SchemaField { return SchemaField{ Id: f.ID().Ref(), diff --git a/server/pkg/integrationapi/types.gen.go b/server/pkg/integrationapi/types.gen.go index bc7c70a272..d9fa886023 100644 --- a/server/pkg/integrationapi/types.gen.go +++ b/server/pkg/integrationapi/types.gen.go @@ -359,10 +359,12 @@ type Model struct { Id *id.ModelID `json:"id,omitempty"` Key *string `json:"key,omitempty"` LastModified *time.Time `json:"lastModified,omitempty"` + MetadataSchema *Schema `json:"metadataSchema,omitempty"` MetadataSchemaId *id.SchemaID `json:"metadataSchemaId,omitempty"` Name *string `json:"name,omitempty"` ProjectId *id.ProjectID `json:"projectId,omitempty"` Public *bool `json:"public,omitempty"` + Schema *Schema `json:"schema,omitempty"` SchemaId *id.SchemaID `json:"schemaId,omitempty"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` } @@ -475,6 +477,9 @@ type ProjectIdParam = id.ProjectID // RefParam defines model for refParam. type RefParam string +// SchemaIdParam defines model for schemaIdParam. +type SchemaIdParam = id.SchemaID + // SortDirParam defines model for sortDirParam. type SortDirParam string @@ -530,22 +535,6 @@ type ModelUpdateJSONBody struct { Name *string `json:"name,omitempty"` } -// FieldCreateJSONBody defines parameters for FieldCreate. -type FieldCreateJSONBody struct { - Key *string `json:"key,omitempty"` - Multiple *bool `json:"multiple,omitempty"` - Required *bool `json:"required,omitempty"` - Type *ValueType `json:"type,omitempty"` -} - -// FieldUpdateJSONBody defines parameters for FieldUpdate. -type FieldUpdateJSONBody struct { - Key *string `json:"key,omitempty"` - Multiple *bool `json:"multiple,omitempty"` - Required *bool `json:"required,omitempty"` - Type *ValueType `json:"type,omitempty"` -} - // ItemFilterJSONBody defines parameters for ItemFilter. type ItemFilterJSONBody struct { Filter *Condition `json:"filter,omitempty"` @@ -715,6 +704,22 @@ type AssetUploadCreateJSONBody struct { Name *string `json:"name,omitempty"` } +// FieldCreateJSONBody defines parameters for FieldCreate. +type FieldCreateJSONBody struct { + Key *string `json:"key,omitempty"` + Multiple *bool `json:"multiple,omitempty"` + Required *bool `json:"required,omitempty"` + Type *ValueType `json:"type,omitempty"` +} + +// FieldUpdateJSONBody defines parameters for FieldUpdate. +type FieldUpdateJSONBody struct { + Key *string `json:"key,omitempty"` + Multiple *bool `json:"multiple,omitempty"` + Required *bool `json:"required,omitempty"` + Type *ValueType `json:"type,omitempty"` +} + // ProjectFilterParams defines parameters for ProjectFilter. type ProjectFilterParams struct { // Page Used to select the page @@ -742,12 +747,6 @@ type ItemCommentUpdateJSONRequestBody ItemCommentUpdateJSONBody // ModelUpdateJSONRequestBody defines body for ModelUpdate for application/json ContentType. type ModelUpdateJSONRequestBody ModelUpdateJSONBody -// FieldCreateJSONRequestBody defines body for FieldCreate for application/json ContentType. -type FieldCreateJSONRequestBody FieldCreateJSONBody - -// FieldUpdateJSONRequestBody defines body for FieldUpdate for application/json ContentType. -type FieldUpdateJSONRequestBody FieldUpdateJSONBody - // ItemFilterJSONRequestBody defines body for ItemFilter for application/json ContentType. type ItemFilterJSONRequestBody ItemFilterJSONBody @@ -777,3 +776,9 @@ type AssetCreateMultipartRequestBody AssetCreateMultipartBody // AssetUploadCreateJSONRequestBody defines body for AssetUploadCreate for application/json ContentType. type AssetUploadCreateJSONRequestBody AssetUploadCreateJSONBody + +// FieldCreateJSONRequestBody defines body for FieldCreate for application/json ContentType. +type FieldCreateJSONRequestBody FieldCreateJSONBody + +// FieldUpdateJSONRequestBody defines body for FieldUpdate for application/json ContentType. +type FieldUpdateJSONRequestBody FieldUpdateJSONBody diff --git a/server/pkg/schema/schema.go b/server/pkg/schema/schema.go index c6551933c5..7251435405 100644 --- a/server/pkg/schema/schema.go +++ b/server/pkg/schema/schema.go @@ -2,8 +2,8 @@ package schema import ( "errors" - "github.com/reearth/reearth-cms/server/pkg/id" + "github.com/reearth/reearth-cms/server/pkg/id" "github.com/reearth/reearth-cms/server/pkg/key" "github.com/reearth/reearth-cms/server/pkg/value" "github.com/reearth/reearthx/account/accountdomain" diff --git a/server/schemas/integration.yml b/server/schemas/integration.yml index a06c32f930..ccb2ab9946 100644 --- a/server/schemas/integration.yml +++ b/server/schemas/integration.yml @@ -199,9 +199,9 @@ paths: description: Invalid request parameter value '401': $ref: '#/components/responses/UnauthorizedError' - '/models/{modelId}/fields': + '/schemata/{schemaId}/fields': parameters: - - $ref: '#/components/parameters/modelIdParam' + - $ref: '#/components/parameters/schemaIdParam' post: operationId: FieldCreate summary: create a field @@ -235,9 +235,9 @@ paths: description: Invalid request parameter value '401': $ref: '#/components/responses/UnauthorizedError' - '/models/{modelId}/fields/{fieldIdOrKey}': + '/schemata/{schemaId}/fields/{fieldIdOrKey}': parameters: - - $ref: '#/components/parameters/modelIdParam' + - $ref: '#/components/parameters/schemaIdParam' - $ref: '#/components/parameters/fieldIdOrKeyParam' patch: operationId: FieldUpdate @@ -1187,6 +1187,14 @@ components: schema: type: string x-go-type: id.ModelID + schemaIdParam: + name: schemaId + in: path + description: ID of the schema in the model + required: true + schema: + type: string + x-go-type: id.SchemaID fieldIdParam: name: fieldId in: path @@ -1324,9 +1332,13 @@ components: schemaId: x-go-type: id.SchemaID type: string + schema: + $ref: '#/components/schemas/schema' metadataSchemaId: x-go-type: id.SchemaID type: string + metadataSchema: + $ref: '#/components/schemas/schema' name: type: string description: From 7a253a035d43b193243ffe3c06160322a56823e8 Mon Sep 17 00:00:00 2001 From: caichi <54824604+caichi-t@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:42:57 +0900 Subject: [PATCH 3/5] fix(web): group field values sometimes disapper after refreshing page (#1202) fix --- web/e2e/project/item/fields/boolean.spec.ts | 8 +++++--- web/e2e/project/item/fields/int.spec.ts | 1 + web/e2e/project/item/fields/url.spec.ts | 1 + .../organisms/Project/Content/ContentDetails/hooks.ts | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/web/e2e/project/item/fields/boolean.spec.ts b/web/e2e/project/item/fields/boolean.spec.ts index 100d996afb..b3c6cc7aec 100644 --- a/web/e2e/project/item/fields/boolean.spec.ts +++ b/web/e2e/project/item/fields/boolean.spec.ts @@ -30,16 +30,18 @@ test("Boolean field creating and updating has succeeded", async ({ page }) => { await expect(page.locator("label")).toContainText("boolean1"); await expect(page.getByRole("main")).toContainText("boolean1 description"); + await page.getByLabel("boolean1").click(); await page.getByRole("button", { name: "Save" }).click(); await closeNotification(page); await page.getByLabel("Back").click(); - await expect(page.getByRole("switch", { name: "close" })).toBeVisible(); + await expect(page.getByRole("switch")).toHaveAttribute("aria-checked", "true"); await page.getByRole("cell").getByLabel("edit").locator("svg").click(); + await expect(page.getByRole("switch")).toHaveAttribute("aria-checked", "true"); await page.getByLabel("boolean1").click(); await page.getByRole("button", { name: "Save" }).click(); await closeNotification(page); await page.getByLabel("Back").click(); - await expect(page.getByRole("switch", { name: "check" })).toBeVisible(); + await expect(page.getByRole("switch")).toHaveAttribute("aria-checked", "false"); }); test("Boolean field editing has succeeded", async ({ page }) => { @@ -61,7 +63,7 @@ test("Boolean field editing has succeeded", async ({ page }) => { await page.getByRole("button", { name: "Save" }).click(); await closeNotification(page); await page.getByLabel("Back").click(); - await expect(page.getByRole("switch", { name: "check" })).toBeVisible(); + await expect(page.getByRole("switch")).toHaveAttribute("aria-checked", "true"); await page.getByText("Schema").click(); await page.getByRole("img", { name: "ellipsis" }).locator("svg").click(); await page.getByLabel("Display name").click(); diff --git a/web/e2e/project/item/fields/int.spec.ts b/web/e2e/project/item/fields/int.spec.ts index f20eab4dbd..42246a5a75 100644 --- a/web/e2e/project/item/fields/int.spec.ts +++ b/web/e2e/project/item/fields/int.spec.ts @@ -38,6 +38,7 @@ test("Int field creating and updating has succeeded", async ({ page }) => { await expect(page.getByRole("cell", { name: "1", exact: true })).toBeVisible(); await page.getByRole("cell").getByLabel("edit").locator("svg").click(); + await expect(page.getByLabel("int1")).toHaveValue("1"); await page.getByLabel("int1").click(); await page.getByLabel("int1").fill("2"); await page.getByRole("button", { name: "Save" }).click(); diff --git a/web/e2e/project/item/fields/url.spec.ts b/web/e2e/project/item/fields/url.spec.ts index cb57fa3eb0..a3dff8a3b2 100644 --- a/web/e2e/project/item/fields/url.spec.ts +++ b/web/e2e/project/item/fields/url.spec.ts @@ -38,6 +38,7 @@ test("URL field creating and updating has succeeded", async ({ page }) => { await expect(page.getByRole("cell", { name: "http://test1.com", exact: true })).toBeVisible(); await page.getByRole("cell").getByLabel("edit").locator("svg").click(); + await expect(page.getByLabel("url1")).toHaveValue("http://test1.com"); await page.getByLabel("url1").click(); await page.getByLabel("url1").fill("http://test2.com"); await page.getByRole("button", { name: "Save" }).click(); diff --git a/web/src/components/organisms/Project/Content/ContentDetails/hooks.ts b/web/src/components/organisms/Project/Content/ContentDetails/hooks.ts index d88b29fb6e..df7872ccfb 100644 --- a/web/src/components/organisms/Project/Content/ContentDetails/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentDetails/hooks.ts @@ -383,6 +383,7 @@ export default () => { const [initialFormValues, setInitialFormValues] = useState>({}); useEffect(() => { + if (itemLoading) return; const handleInitialValuesSet = async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const initialValues: Record = {}; @@ -430,7 +431,7 @@ export default () => { setInitialFormValues(initialValues); }; handleInitialValuesSet(); - }, [currentItem, currentModel, handleGroupGet, updateValueConvert, valueGet]); + }, [itemLoading, currentItem, currentModel, handleGroupGet, updateValueConvert, valueGet]); const initialMetaFormValues: Record = useMemo(() => { const initialValues: Record = {}; From fafa1bebaec0c62cd71f887931f34f7890416a9b Mon Sep 17 00:00:00 2001 From: Nour Balaha Date: Mon, 5 Aug 2024 15:23:12 +0900 Subject: [PATCH 4/5] feat(server): export items as geojson and csv via integration api (#1194) * wip: items with project as geojson and csv * update the csv format and description in the integration schema * fix: csv and geojson bugs * add unit tests for group * update geojson content type * update Japanese translations * remove group and reference fields from export * add check if result is empty --- server/e2e/integration_item_test.go | 149 ++++ server/i18n/en.yml | 3 + server/i18n/ja.yml | 3 + server/internal/adapter/integration/item.go | 151 ++++ .../adapter/integration/server.gen.go | 654 ++++++++++++++++-- server/pkg/exporters/csv.go | 182 +++++ server/pkg/exporters/csv_test.go | 353 ++++++++++ server/pkg/exporters/geojson.go | 276 ++++++++ server/pkg/exporters/geojson_test.go | 322 +++++++++ server/pkg/integrationapi/csv.go | 15 + server/pkg/integrationapi/geojson.go | 87 +++ server/pkg/integrationapi/types.gen.go | 347 +++++++++- server/pkg/item/field.go | 4 + server/pkg/item/item.go | 11 + server/pkg/item/item_test.go | 75 ++ server/pkg/schema/field.go | 17 + server/pkg/schema/schema.go | 19 + server/pkg/value/type.go | 4 + server/schemas/integration.yml | 214 ++++++ 19 files changed, 2827 insertions(+), 59 deletions(-) create mode 100644 server/pkg/exporters/csv.go create mode 100644 server/pkg/exporters/csv_test.go create mode 100644 server/pkg/exporters/geojson.go create mode 100644 server/pkg/exporters/geojson_test.go create mode 100644 server/pkg/integrationapi/csv.go create mode 100644 server/pkg/integrationapi/geojson.go diff --git a/server/e2e/integration_item_test.go b/server/e2e/integration_item_test.go index 02be0f341d..4103d41245 100644 --- a/server/e2e/integration_item_test.go +++ b/server/e2e/integration_item_test.go @@ -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) @@ -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) diff --git a/server/i18n/en.yml b/server/i18n/en.yml index 75316aab48..bac394f232 100644 --- a/server/i18n/en.yml +++ b/server/i18n/en.yml @@ -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: "" @@ -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: "" diff --git a/server/i18n/ja.yml b/server/i18n/ja.yml index d0b23bc412..17f76a022d 100644 --- a/server/i18n/ja.yml +++ b/server/i18n/ja.yml @@ -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: 未実装です。 @@ -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は必須です。 diff --git a/server/internal/adapter/integration/item.go b/server/internal/adapter/integration/item.go index 499f98a9ad..287e2787ea 100644 --- a/server/internal/adapter/integration/item.go +++ b/server/internal/adapter/integration/item.go @@ -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" @@ -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) @@ -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 + } + + 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) + 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) diff --git a/server/internal/adapter/integration/server.gen.go b/server/internal/adapter/integration/server.gen.go index dc98ef71b4..43ce513824 100644 --- a/server/internal/adapter/integration/server.gen.go +++ b/server/internal/adapter/integration/server.gen.go @@ -10,6 +10,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "mime/multipart" "net/http" "net/url" @@ -80,6 +81,12 @@ type ServerInterface interface { // create an item // (POST /models/{modelId}/items) ItemCreate(ctx echo.Context, modelId ModelIdParam) error + // Returns a CSV that has a list of items as features. + // (GET /models/{modelId}/items.csv) + ItemsAsCSV(ctx echo.Context, modelId ModelIdParam, params ItemsAsCSVParams) error + // Returns a GeoJSON that has a list of items as features. + // (GET /models/{modelId}/items.geojson) + ItemsAsGeoJSON(ctx echo.Context, modelId ModelIdParam, params ItemsAsGeoJSONParams) error // Returns a models. // (GET /projects/{projectIdOrAlias}/models) ModelFilter(ctx echo.Context, projectIdOrAlias ProjectIdOrAliasParam, params ModelFilterParams) error @@ -110,6 +117,12 @@ type ServerInterface interface { // (POST /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items) ItemCreateWithProject(ctx echo.Context, projectIdOrAlias ProjectIdOrAliasParam, modelIdOrKey ModelIdOrKeyParam) error + // Returns a CSV that has a list of items as features. + // (GET /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.csv) + ItemsWithProjectAsCSV(ctx echo.Context, projectIdOrAlias ProjectIdOrAliasParam, modelIdOrKey ModelIdOrKeyParam, params ItemsWithProjectAsCSVParams) error + // Returns a GeoJSON that has a list of items as features. + // (GET /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.geojson) + ItemsWithProjectAsGeoJSON(ctx echo.Context, projectIdOrAlias ProjectIdOrAliasParam, modelIdOrKey ModelIdOrKeyParam, params ItemsWithProjectAsGeoJSONParams) error // Returns a list of assets. // (GET /projects/{projectId}/assets) AssetFilter(ctx echo.Context, projectId ProjectIdParam, params AssetFilterParams) error @@ -557,6 +570,88 @@ func (w *ServerInterfaceWrapper) ItemCreate(ctx echo.Context) error { return err } +// ItemsAsCSV converts echo context to params. +func (w *ServerInterfaceWrapper) ItemsAsCSV(ctx echo.Context) error { + var err error + // ------------- Path parameter "modelId" ------------- + var modelId ModelIdParam + + err = runtime.BindStyledParameterWithOptions("simple", "modelId", ctx.Param("modelId"), &modelId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter modelId: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params ItemsAsCSVParams + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + } + + // ------------- Optional query parameter "perPage" ------------- + + err = runtime.BindQueryParameter("form", true, false, "perPage", ctx.QueryParams(), ¶ms.PerPage) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter perPage: %s", err)) + } + + // ------------- Optional query parameter "ref" ------------- + + err = runtime.BindQueryParameter("form", true, false, "ref", ctx.QueryParams(), ¶ms.Ref) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter ref: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ItemsAsCSV(ctx, modelId, params) + return err +} + +// ItemsAsGeoJSON converts echo context to params. +func (w *ServerInterfaceWrapper) ItemsAsGeoJSON(ctx echo.Context) error { + var err error + // ------------- Path parameter "modelId" ------------- + var modelId ModelIdParam + + err = runtime.BindStyledParameterWithOptions("simple", "modelId", ctx.Param("modelId"), &modelId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter modelId: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params ItemsAsGeoJSONParams + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + } + + // ------------- Optional query parameter "perPage" ------------- + + err = runtime.BindQueryParameter("form", true, false, "perPage", ctx.QueryParams(), ¶ms.PerPage) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter perPage: %s", err)) + } + + // ------------- Optional query parameter "ref" ------------- + + err = runtime.BindQueryParameter("form", true, false, "ref", ctx.QueryParams(), ¶ms.Ref) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter ref: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ItemsAsGeoJSON(ctx, modelId, params) + return err +} + // ModelFilter converts echo context to params. func (w *ServerInterfaceWrapper) ModelFilter(ctx echo.Context) error { var err error @@ -893,6 +988,104 @@ func (w *ServerInterfaceWrapper) ItemCreateWithProject(ctx echo.Context) error { return err } +// ItemsWithProjectAsCSV converts echo context to params. +func (w *ServerInterfaceWrapper) ItemsWithProjectAsCSV(ctx echo.Context) error { + var err error + // ------------- Path parameter "projectIdOrAlias" ------------- + var projectIdOrAlias ProjectIdOrAliasParam + + err = runtime.BindStyledParameterWithOptions("simple", "projectIdOrAlias", ctx.Param("projectIdOrAlias"), &projectIdOrAlias, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter projectIdOrAlias: %s", err)) + } + + // ------------- Path parameter "modelIdOrKey" ------------- + var modelIdOrKey ModelIdOrKeyParam + + err = runtime.BindStyledParameterWithOptions("simple", "modelIdOrKey", ctx.Param("modelIdOrKey"), &modelIdOrKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter modelIdOrKey: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params ItemsWithProjectAsCSVParams + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + } + + // ------------- Optional query parameter "perPage" ------------- + + err = runtime.BindQueryParameter("form", true, false, "perPage", ctx.QueryParams(), ¶ms.PerPage) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter perPage: %s", err)) + } + + // ------------- Optional query parameter "ref" ------------- + + err = runtime.BindQueryParameter("form", true, false, "ref", ctx.QueryParams(), ¶ms.Ref) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter ref: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ItemsWithProjectAsCSV(ctx, projectIdOrAlias, modelIdOrKey, params) + return err +} + +// ItemsWithProjectAsGeoJSON converts echo context to params. +func (w *ServerInterfaceWrapper) ItemsWithProjectAsGeoJSON(ctx echo.Context) error { + var err error + // ------------- Path parameter "projectIdOrAlias" ------------- + var projectIdOrAlias ProjectIdOrAliasParam + + err = runtime.BindStyledParameterWithOptions("simple", "projectIdOrAlias", ctx.Param("projectIdOrAlias"), &projectIdOrAlias, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter projectIdOrAlias: %s", err)) + } + + // ------------- Path parameter "modelIdOrKey" ------------- + var modelIdOrKey ModelIdOrKeyParam + + err = runtime.BindStyledParameterWithOptions("simple", "modelIdOrKey", ctx.Param("modelIdOrKey"), &modelIdOrKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter modelIdOrKey: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params ItemsWithProjectAsGeoJSONParams + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + } + + // ------------- Optional query parameter "perPage" ------------- + + err = runtime.BindQueryParameter("form", true, false, "perPage", ctx.QueryParams(), ¶ms.PerPage) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter perPage: %s", err)) + } + + // ------------- Optional query parameter "ref" ------------- + + err = runtime.BindQueryParameter("form", true, false, "ref", ctx.QueryParams(), ¶ms.Ref) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter ref: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ItemsWithProjectAsGeoJSON(ctx, projectIdOrAlias, modelIdOrKey, params) + return err +} + // AssetFilter converts echo context to params. func (w *ServerInterfaceWrapper) AssetFilter(ctx echo.Context) error { var err error @@ -1127,6 +1320,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PATCH(baseURL+"/models/:modelId", wrapper.ModelUpdate) router.GET(baseURL+"/models/:modelId/items", wrapper.ItemFilter) router.POST(baseURL+"/models/:modelId/items", wrapper.ItemCreate) + router.GET(baseURL+"/models/:modelId/items.csv", wrapper.ItemsAsCSV) + router.GET(baseURL+"/models/:modelId/items.geojson", wrapper.ItemsAsGeoJSON) router.GET(baseURL+"/projects/:projectIdOrAlias/models", wrapper.ModelFilter) router.POST(baseURL+"/projects/:projectIdOrAlias/models", wrapper.ModelCreate) router.DELETE(baseURL+"/projects/:projectIdOrAlias/models/:modelIdOrKey", wrapper.ModelDeleteWithProject) @@ -1137,6 +1332,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PATCH(baseURL+"/projects/:projectIdOrAlias/models/:modelIdOrKey/fields/:fieldIdOrKey", wrapper.FieldUpdateWithProject) router.GET(baseURL+"/projects/:projectIdOrAlias/models/:modelIdOrKey/items", wrapper.ItemFilterWithProject) router.POST(baseURL+"/projects/:projectIdOrAlias/models/:modelIdOrKey/items", wrapper.ItemCreateWithProject) + router.GET(baseURL+"/projects/:projectIdOrAlias/models/:modelIdOrKey/items.csv", wrapper.ItemsWithProjectAsCSV) + router.GET(baseURL+"/projects/:projectIdOrAlias/models/:modelIdOrKey/items.geojson", wrapper.ItemsWithProjectAsGeoJSON) router.GET(baseURL+"/projects/:projectId/assets", wrapper.AssetFilter) router.POST(baseURL+"/projects/:projectId/assets", wrapper.AssetCreate) router.POST(baseURL+"/projects/:projectId/assets/uploads", wrapper.AssetUploadCreate) @@ -1914,6 +2111,114 @@ func (response ItemCreate401Response) VisitItemCreateResponse(w http.ResponseWri return nil } +type ItemsAsCSVRequestObject struct { + ModelId ModelIdParam `json:"modelId"` + Params ItemsAsCSVParams +} + +type ItemsAsCSVResponseObject interface { + VisitItemsAsCSVResponse(w http.ResponseWriter) error +} + +type ItemsAsCSV200TextcsvResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response ItemsAsCSV200TextcsvResponse) VisitItemsAsCSVResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/csv") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type ItemsAsCSV400Response struct { +} + +func (response ItemsAsCSV400Response) VisitItemsAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type ItemsAsCSV401Response = UnauthorizedErrorResponse + +func (response ItemsAsCSV401Response) VisitItemsAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type ItemsAsCSV404Response struct { +} + +func (response ItemsAsCSV404Response) VisitItemsAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type ItemsAsCSV500Response struct { +} + +func (response ItemsAsCSV500Response) VisitItemsAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(500) + return nil +} + +type ItemsAsGeoJSONRequestObject struct { + ModelId ModelIdParam `json:"modelId"` + Params ItemsAsGeoJSONParams +} + +type ItemsAsGeoJSONResponseObject interface { + VisitItemsAsGeoJSONResponse(w http.ResponseWriter) error +} + +type ItemsAsGeoJSON200JSONResponse GeoJSON + +func (response ItemsAsGeoJSON200JSONResponse) VisitItemsAsGeoJSONResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ItemsAsGeoJSON400Response struct { +} + +func (response ItemsAsGeoJSON400Response) VisitItemsAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type ItemsAsGeoJSON401Response = UnauthorizedErrorResponse + +func (response ItemsAsGeoJSON401Response) VisitItemsAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type ItemsAsGeoJSON404Response struct { +} + +func (response ItemsAsGeoJSON404Response) VisitItemsAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type ItemsAsGeoJSON500Response struct { +} + +func (response ItemsAsGeoJSON500Response) VisitItemsAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(500) + return nil +} + type ModelFilterRequestObject struct { ProjectIdOrAlias ProjectIdOrAliasParam `json:"projectIdOrAlias"` Params ModelFilterParams @@ -2362,6 +2667,116 @@ func (response ItemCreateWithProject401Response) VisitItemCreateWithProjectRespo return nil } +type ItemsWithProjectAsCSVRequestObject struct { + ProjectIdOrAlias ProjectIdOrAliasParam `json:"projectIdOrAlias"` + ModelIdOrKey ModelIdOrKeyParam `json:"modelIdOrKey"` + Params ItemsWithProjectAsCSVParams +} + +type ItemsWithProjectAsCSVResponseObject interface { + VisitItemsWithProjectAsCSVResponse(w http.ResponseWriter) error +} + +type ItemsWithProjectAsCSV200TextcsvResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response ItemsWithProjectAsCSV200TextcsvResponse) VisitItemsWithProjectAsCSVResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/csv") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type ItemsWithProjectAsCSV400Response struct { +} + +func (response ItemsWithProjectAsCSV400Response) VisitItemsWithProjectAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type ItemsWithProjectAsCSV401Response = UnauthorizedErrorResponse + +func (response ItemsWithProjectAsCSV401Response) VisitItemsWithProjectAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type ItemsWithProjectAsCSV404Response struct { +} + +func (response ItemsWithProjectAsCSV404Response) VisitItemsWithProjectAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type ItemsWithProjectAsCSV500Response struct { +} + +func (response ItemsWithProjectAsCSV500Response) VisitItemsWithProjectAsCSVResponse(w http.ResponseWriter) error { + w.WriteHeader(500) + return nil +} + +type ItemsWithProjectAsGeoJSONRequestObject struct { + ProjectIdOrAlias ProjectIdOrAliasParam `json:"projectIdOrAlias"` + ModelIdOrKey ModelIdOrKeyParam `json:"modelIdOrKey"` + Params ItemsWithProjectAsGeoJSONParams +} + +type ItemsWithProjectAsGeoJSONResponseObject interface { + VisitItemsWithProjectAsGeoJSONResponse(w http.ResponseWriter) error +} + +type ItemsWithProjectAsGeoJSON200JSONResponse GeoJSON + +func (response ItemsWithProjectAsGeoJSON200JSONResponse) VisitItemsWithProjectAsGeoJSONResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ItemsWithProjectAsGeoJSON400Response struct { +} + +func (response ItemsWithProjectAsGeoJSON400Response) VisitItemsWithProjectAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type ItemsWithProjectAsGeoJSON401Response = UnauthorizedErrorResponse + +func (response ItemsWithProjectAsGeoJSON401Response) VisitItemsWithProjectAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type ItemsWithProjectAsGeoJSON404Response struct { +} + +func (response ItemsWithProjectAsGeoJSON404Response) VisitItemsWithProjectAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type ItemsWithProjectAsGeoJSON500Response struct { +} + +func (response ItemsWithProjectAsGeoJSON500Response) VisitItemsWithProjectAsGeoJSONResponse(w http.ResponseWriter) error { + w.WriteHeader(500) + return nil +} + type AssetFilterRequestObject struct { ProjectId ProjectIdParam `json:"projectId"` Params AssetFilterParams @@ -2708,6 +3123,12 @@ type StrictServerInterface interface { // create an item // (POST /models/{modelId}/items) ItemCreate(ctx context.Context, request ItemCreateRequestObject) (ItemCreateResponseObject, error) + // Returns a CSV that has a list of items as features. + // (GET /models/{modelId}/items.csv) + ItemsAsCSV(ctx context.Context, request ItemsAsCSVRequestObject) (ItemsAsCSVResponseObject, error) + // Returns a GeoJSON that has a list of items as features. + // (GET /models/{modelId}/items.geojson) + ItemsAsGeoJSON(ctx context.Context, request ItemsAsGeoJSONRequestObject) (ItemsAsGeoJSONResponseObject, error) // Returns a models. // (GET /projects/{projectIdOrAlias}/models) ModelFilter(ctx context.Context, request ModelFilterRequestObject) (ModelFilterResponseObject, error) @@ -2738,6 +3159,12 @@ type StrictServerInterface interface { // (POST /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items) ItemCreateWithProject(ctx context.Context, request ItemCreateWithProjectRequestObject) (ItemCreateWithProjectResponseObject, error) + // Returns a CSV that has a list of items as features. + // (GET /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.csv) + ItemsWithProjectAsCSV(ctx context.Context, request ItemsWithProjectAsCSVRequestObject) (ItemsWithProjectAsCSVResponseObject, error) + // Returns a GeoJSON that has a list of items as features. + // (GET /projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.geojson) + ItemsWithProjectAsGeoJSON(ctx context.Context, request ItemsWithProjectAsGeoJSONRequestObject) (ItemsWithProjectAsGeoJSONResponseObject, error) // Returns a list of assets. // (GET /projects/{projectId}/assets) AssetFilter(ctx context.Context, request AssetFilterRequestObject) (AssetFilterResponseObject, error) @@ -3277,6 +3704,58 @@ func (sh *strictHandler) ItemCreate(ctx echo.Context, modelId ModelIdParam) erro return nil } +// ItemsAsCSV operation middleware +func (sh *strictHandler) ItemsAsCSV(ctx echo.Context, modelId ModelIdParam, params ItemsAsCSVParams) error { + var request ItemsAsCSVRequestObject + + request.ModelId = modelId + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.ItemsAsCSV(ctx.Request().Context(), request.(ItemsAsCSVRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ItemsAsCSV") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(ItemsAsCSVResponseObject); ok { + return validResponse.VisitItemsAsCSVResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// ItemsAsGeoJSON operation middleware +func (sh *strictHandler) ItemsAsGeoJSON(ctx echo.Context, modelId ModelIdParam, params ItemsAsGeoJSONParams) error { + var request ItemsAsGeoJSONRequestObject + + request.ModelId = modelId + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.ItemsAsGeoJSON(ctx.Request().Context(), request.(ItemsAsGeoJSONRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ItemsAsGeoJSON") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(ItemsAsGeoJSONResponseObject); ok { + return validResponse.VisitItemsAsGeoJSONResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // ModelFilter operation middleware func (sh *strictHandler) ModelFilter(ctx echo.Context, projectIdOrAlias ProjectIdOrAliasParam, params ModelFilterParams) error { var request ModelFilterRequestObject @@ -3570,6 +4049,60 @@ func (sh *strictHandler) ItemCreateWithProject(ctx echo.Context, projectIdOrAlia return nil } +// ItemsWithProjectAsCSV operation middleware +func (sh *strictHandler) ItemsWithProjectAsCSV(ctx echo.Context, projectIdOrAlias ProjectIdOrAliasParam, modelIdOrKey ModelIdOrKeyParam, params ItemsWithProjectAsCSVParams) error { + var request ItemsWithProjectAsCSVRequestObject + + request.ProjectIdOrAlias = projectIdOrAlias + request.ModelIdOrKey = modelIdOrKey + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.ItemsWithProjectAsCSV(ctx.Request().Context(), request.(ItemsWithProjectAsCSVRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ItemsWithProjectAsCSV") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(ItemsWithProjectAsCSVResponseObject); ok { + return validResponse.VisitItemsWithProjectAsCSVResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// ItemsWithProjectAsGeoJSON operation middleware +func (sh *strictHandler) ItemsWithProjectAsGeoJSON(ctx echo.Context, projectIdOrAlias ProjectIdOrAliasParam, modelIdOrKey ModelIdOrKeyParam, params ItemsWithProjectAsGeoJSONParams) error { + var request ItemsWithProjectAsGeoJSONRequestObject + + request.ProjectIdOrAlias = projectIdOrAlias + request.ModelIdOrKey = modelIdOrKey + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.ItemsWithProjectAsGeoJSON(ctx.Request().Context(), request.(ItemsWithProjectAsGeoJSONRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ItemsWithProjectAsGeoJSON") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(ItemsWithProjectAsGeoJSONResponseObject); ok { + return validResponse.VisitItemsWithProjectAsGeoJSONResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // AssetFilter operation middleware func (sh *strictHandler) AssetFilter(ctx echo.Context, projectId ProjectIdParam, params AssetFilterParams) error { var request AssetFilterRequestObject @@ -3784,63 +4317,70 @@ func (sh *strictHandler) ProjectFilter(ctx echo.Context, workspaceId WorkspaceId // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "2HhV3vhI6Z5Thuv1+v8BAAD//8KIn+ESfwAA", + "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", + "jmQ8SspwtVr9PwAA///+UHkIBpMAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/pkg/exporters/csv.go b/server/pkg/exporters/csv.go new file mode 100644 index 0000000000..862b8d7794 --- /dev/null +++ b/server/pkg/exporters/csv.go @@ -0,0 +1,182 @@ +package exporters + +import ( + "encoding/csv" + "strconv" + "strings" + "time" + + "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/reearthx/i18n" + "github.com/reearth/reearthx/rerror" + "github.com/samber/lo" +) + +var ( + noPointFieldError = rerror.NewE(i18n.T("no point field in this model")) + pointFieldIsNotSupportedError = rerror.NewE(i18n.T("point type is not supported in any geometry field in this model")) +) + +func CSVFromItems(items item.VersionedList, s *schema.Schema) (string, error) { + if !s.IsPointFieldSupported() { + return "", pointFieldIsNotSupportedError + } + + keys, nonGeoFields := buildCSVHeaders(s) + data := [][]string{} + data = append(data, keys) + for _, ver := range items { + row, ok := rowFromItem(ver.Value(), nonGeoFields) + if ok { + data = append(data, row) + } + } + + if len(data) == 1 { + return "", noPointFieldError + } + + csv, err := convertToCSV(data) + if err != nil { + return "", err + } + + return csv, nil +} + +func buildCSVHeaders(s *schema.Schema) ([]string, []*schema.Field) { + keys := []string{"id", "location_lat", "location_lng"} + nonGeoFields := lo.Filter(s.Fields(), func(f *schema.Field, _ int) bool { + return !f.IsGeometryField() + }) + for _, f := range nonGeoFields { + keys = append(keys, f.Name()) + } + return keys, nonGeoFields +} + +func rowFromItem(itm *item.Item, nonGeoFields []*schema.Field) ([]string, bool) { + geoField, err := extractFirstPointField(itm) + if err != nil { + return nil, false + } + + id := itm.ID().String() + lat, lng := float64ToString(geoField[0]), float64ToString(geoField[1]) + row := []string{id, lat, lng} + + for _, sf := range nonGeoFields { + f := itm.Field(sf.ID()) + v := ToCSVProp(f) + row = append(row, v) + } + + return row, true +} + +func extractFirstPointField(itm *item.Item) ([]float64, error) { + geoFields := lo.Filter(itm.Fields(), func(f *item.Field, _ int) bool { + return f.Type().IsGeometryFieldType() + }) + + for _, f := range geoFields { + ss, ok := f.Value().First().ValueString() + if !ok { + continue + } + g, err := StringToGeometry(ss) + if err != nil || g == nil { + continue + } + if *g.Type != GeometryTypePoint { + continue + } + return g.Coordinates.AsPoint() + } + return nil, noPointFieldError +} + +func convertToCSV(data [][]string) (string, error) { + var sb strings.Builder + w := csv.NewWriter(&sb) + for _, row := range data { + if err := w.Write(row); err != nil { + return "", err + } + } + w.Flush() + if err := w.Error(); err != nil { + return "", err + } + return sb.String(), nil +} + +func float64ToString(f float64) string { + return strconv.FormatFloat(f, 'f', -1, 64) +} + +func ToCSVProp(f *item.Field) string { + if f == nil { + return "" + } + vv := f.Value().First() + if vv == nil { + return "" + } + return ToCSVValue(vv) +} + +func ToCSVValue(vv *value.Value) string { + if vv == nil { + return "" + } + + switch vv.Type() { + case value.TypeText, value.TypeTextArea, value.TypeRichText, value.TypeMarkdown, value.TypeSelect, value.TypeTag: + v, ok := vv.ValueString() + if !ok { + return "" + } + return v + case value.TypeURL: + v, ok := vv.ValueURL() + if !ok { + return "" + } + return v.String() + case value.TypeAsset: + v, ok := vv.ValueAsset() + if !ok { + return "" + } + return v.String() + case value.TypeInteger: + v, ok := vv.ValueInteger() + if !ok { + return "" + } + return strconv.FormatInt(v, 10) + case value.TypeNumber: + v, ok := vv.ValueNumber() + if !ok { + return "" + } + return strconv.FormatFloat(v, 'f', -1, 64) + case value.TypeBool, value.TypeCheckbox: + v, ok := vv.ValueBool() + if !ok { + return "" + } + return strconv.FormatBool(v) + case value.TypeDateTime: + v, ok := vv.ValueDateTime() + if !ok { + return "" + } + return v.Format(time.RFC3339) + default: + return "" + } +} diff --git a/server/pkg/exporters/csv_test.go b/server/pkg/exporters/csv_test.go new file mode 100644 index 0000000000..87758fbbe5 --- /dev/null +++ b/server/pkg/exporters/csv_test.go @@ -0,0 +1,353 @@ +package exporters + +import ( + "fmt" + "net/url" + "testing" + "time" + + "github.com/reearth/reearth-cms/server/pkg/id" + "github.com/reearth/reearth-cms/server/pkg/item" + "github.com/reearth/reearth-cms/server/pkg/key" + "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(key.Random()).MustBuild() + sf3 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(key.Random()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf4 := schema.NewField(tp4).NewID().Name("age").Key(key.Random()).MustBuild() + sf5 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("isMarried").Key(key.Random()).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} + csvString, err := CSVFromItems(ver1, s1) + expected1 := fmt.Sprintf("id,location_lat,location_lng,age,isMarried\n%s,139.28179282584915,36.58570985749664,30,true\n", vi1.Value().ID()) + assert.Nil(t, err) + assert.Equal(t, expected1, csvString) + + // 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(key.Random()).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 + csvString, err = CSVFromItems(ver2, s2) + assert.Equal(t, expectErr2, err) + assert.Empty(t, csvString) + + // 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(key.Random()).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 + csvString, err = CSVFromItems(ver3, s3) + assert.Equal(t, expectErr3, err) + assert.Empty(t, csvString) +} + +func TestBuildCSVHeaders(t *testing.T) { + sid := id.NewSchemaID() + 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(key.Random()).MustBuild() + sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(key.Random()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf3 := schema.NewField(tp4).NewID().Name("age").Key(key.Random()).MustBuild() + sf4 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("isMarried").Key(key.Random()).MustBuild() + s1 := schema.New().ID(sid).Fields([]*schema.Field{sf1, sf3, sf4}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() + s2 := schema.New().ID(sid).Fields([]*schema.Field{sf1, sf2, sf3, sf4}).Workspace(accountdomain.NewWorkspaceID()).Project(pid).MustBuild() + + // Test with geometry fields + headers1, ff := buildCSVHeaders(s1) + assert.Equal(t, []string{"id", "location_lat", "location_lng", "age", "isMarried"}, headers1) + assert.Equal(t, []*schema.Field{sf3, sf4}, ff) + + // Test with mixed fields + headers2, _ := buildCSVHeaders(s2) + assert.Equal(t, []string{"id", "location_lat", "location_lng", "age", "isMarried"}, headers2) + assert.Equal(t, []*schema.Field{sf3, sf4}, ff) +} + +func TestRowFromItem(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(key.Random()).MustBuild() + sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(key.Random()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf3 := schema.NewField(tp4).NewID().Name("age").Key(key.Random()).MustBuild() + sf4 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("isMarried").Key(key.Random()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fi2 := item.NewField(sf2.ID(), value.TypeGeometryEditor.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + fi3 := item.NewField(sf3.ID(), value.TypeInteger.Value(30).AsMultiple(), nil) + fi4 := item.NewField(sf4.ID(), value.TypeBool.Value(true).AsMultiple(), nil) + i1 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{}). + Model(mid). + Thread(tid). + MustBuild() + + // Test with no fields + row1, ok1 := rowFromItem(i1, []*schema.Field{sf3, sf4}) + assert.False(t, ok1) + assert.Nil(t, row1) + + // Test with item containing no geometry field + i2 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi3, fi4}). + Model(mid). + Thread(tid). + MustBuild() + row2, ok2 := rowFromItem(i2, []*schema.Field{sf3, sf4}) + assert.False(t, ok2) + assert.Nil(t, row2) + + // Test with item containing multiple fields including a geometry field + i3 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi1, fi2, fi3, fi4}). + Model(mid). + Thread(tid). + MustBuild() + row3, ok3 := rowFromItem(i3, []*schema.Field{sf3, sf4}) + assert.True(t, ok3) + assert.Equal(t, []string{i1.ID().String(), "139.28179282584915", "36.58570985749664", "30", "true"}, row3) +} + +func TestExtractFirstPointField(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(key.Random()).MustBuild() + sf2 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("geo2").Key(key.Random()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf3 := schema.NewField(tp4).NewID().Name("age").Key(key.Random()).MustBuild() + sf4 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("isMarried").Key(key.Random()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[139.28179282584915,36.58570985749664],\"type\":\"Point\"}").AsMultiple(), nil) + 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) + fi3 := item.NewField(sf3.ID(), value.TypeInteger.Value(30).AsMultiple(), nil) + fi4 := item.NewField(sf4.ID(), value.TypeBool.Value(true).AsMultiple(), nil) + i1 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi1, fi3, fi4}). + Model(mid). + Thread(tid). + MustBuild() + i2 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi3, fi4}). + Model(mid). + Thread(tid). + MustBuild() + i3 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi2, fi3, fi4}). + Model(mid). + Thread(tid). + MustBuild() + + // Test with valid geometry field + point1, err1 := extractFirstPointField(i1) + assert.NoError(t, err1) + assert.Equal(t, []float64{139.28179282584915, 36.58570985749664}, point1) + + // Test with no geometry field + point2, err2 := extractFirstPointField(i2) + assert.Error(t, err2) + assert.Equal(t, noPointFieldError, err2) + assert.Nil(t, point2) + + // Test with non-point geometry field + point3, err3 := extractFirstPointField(i3) + assert.Error(t, err3) + assert.Equal(t, noPointFieldError, err3) + assert.Nil(t, point3) +} + +func TestToCSVProp(t *testing.T) { + sf1 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if1 := item.NewField(sf1.ID(), value.TypeText.Value("test").AsMultiple(), nil) + s1 := ToCSVProp(if1) + assert.Equal(t, "test", s1) + + var if2 *item.Field + s2 := ToCSVProp(if2) + assert.Empty(t, s2) + + v3 := int64(30) + in3, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp3 := in3.TypeProperty() + sf3 := schema.NewField(tp3).NewID().Name("age").Key(key.Random()).MustBuild() + if3 := item.NewField(sf3.ID(), value.TypeInteger.Value(v3).AsMultiple(), nil) + s3, ok3 := ToGeoJsonSingleValue(if3.Value().First()) + assert.Equal(t, int64(30), s3) + assert.True(t, ok3) + + v4 := true + sf4 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("age").Key(key.Random()).MustBuild() + if4 := item.NewField(sf4.ID(), value.TypeBool.Value(v4).AsMultiple(), nil) + s4, ok4 := ToGeoJsonSingleValue(if4.Value().First()) + assert.Equal(t, true, s4) + assert.True(t, ok4) + + v5 := false + sf5 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("age").Key(key.Random()).MustBuild() + if5 := item.NewField(sf5.ID(), value.TypeBool.Value(v5).AsMultiple(), nil) + s5, ok5 := ToGeoJsonSingleValue(if5.Value().First()) + assert.Equal(t, false, s5) + assert.True(t, ok5) +} + +func TestToCSVValue(t *testing.T) { + sf1 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if1 := item.NewField(sf1.ID(), value.TypeText.Value("test").AsMultiple(), nil) + s1 := ToCSVValue(if1.Value().First()) + assert.Equal(t, "test", s1) + + sf2 := schema.NewField(schema.NewTextArea(lo.ToPtr(10)).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if2 := item.NewField(sf2.ID(), value.TypeTextArea.Value("test").AsMultiple(), nil) + s2 := ToCSVValue(if2.Value().First()) + assert.Equal(t, "test", s2) + + sf3 := schema.NewField(schema.NewURL().TypeProperty()).NewID().Key(key.Random()).MustBuild() + v3 := url.URL{Scheme: "https", Host: "reearth.io"} + if3 := item.NewField(sf3.ID(), value.TypeURL.Value(v3).AsMultiple(), nil) + s3 := ToCSVValue(if3.Value().First()) + assert.Equal(t, "https://reearth.io", s3) + + sf4 := schema.NewField(schema.NewAsset().TypeProperty()).NewID().Key(key.Random()).MustBuild() + v4 := id.NewAssetID() + if4 := item.NewField(sf4.ID(), value.TypeAsset.Value(v4).AsMultiple(), nil) + s4 := ToCSVValue(if4.Value().First()) + assert.Equal(t, v4.String(), s4) + + gid := id.NewGroupID() + igid := id.NewItemGroupID() + sf5 := schema.NewField(schema.NewGroup(gid).TypeProperty()).NewID().Key(key.Random()).Multiple(true).MustBuild() + if5 := item.NewField(sf5.ID(), value.MultipleFrom(value.TypeGroup, []*value.Value{value.TypeGroup.Value(igid)}), nil) + s5 := ToCSVValue(if5.Value().First()) + assert.Empty(t, s5) + + v6 := id.NewItemID() + sf6 := schema.NewField(schema.NewReference(id.NewModelID(), id.NewSchemaID(), nil, nil).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if6 := item.NewField(sf6.ID(), value.TypeReference.Value(v6).AsMultiple(), nil) + s6 := ToCSVValue(if6.Value().First()) + assert.Empty(t, s6) + + v7 := int64(30) + in7, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp7 := in7.TypeProperty() + sf7 := schema.NewField(tp7).NewID().Name("age").Key(key.Random()).MustBuild() + if7 := item.NewField(sf7.ID(), value.TypeInteger.Value(v7).AsMultiple(), nil) + s7 := ToCSVValue(if7.Value().First()) + assert.Equal(t, "30", s7) + + v8 := float64(30.123) + in8, _ := schema.NewNumber(lo.ToPtr(float64(1)), lo.ToPtr(float64(100))) + tp8 := in8.TypeProperty() + sf8 := schema.NewField(tp8).NewID().Name("age").Key(key.Random()).MustBuild() + if8 := item.NewField(sf8.ID(), value.TypeNumber.Value(v8).AsMultiple(), nil) + s8 := ToCSVValue(if8.Value().First()) + assert.Equal(t, "30.123", s8) + + v9 := true + sf9 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("age").Key(key.Random()).MustBuild() + if9 := item.NewField(sf9.ID(), value.TypeBool.Value(v9).AsMultiple(), nil) + s9 := ToCSVValue(if9.Value().First()) + assert.Equal(t, "true", s9) + + v10 := time.Now() + sf10 := schema.NewField(schema.NewDateTime().TypeProperty()).NewID().Name("age").Key(key.Random()).MustBuild() + if10 := item.NewField(sf10.ID(), value.TypeDateTime.Value(v10).AsMultiple(), nil) + s10 := ToCSVValue(if10.Value().First()) + assert.Equal(t, v10.Format(time.RFC3339), s10) + + var if11 *item.Field + s11 := ToCSVValue(if11.Value().First()) + assert.Empty(t, s11) +} + diff --git a/server/pkg/exporters/geojson.go b/server/pkg/exporters/geojson.go new file mode 100644 index 0000000000..48b8f4a2bf --- /dev/null +++ b/server/pkg/exporters/geojson.go @@ -0,0 +1,276 @@ +package exporters + +import ( + "encoding/json" + "time" + + "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/reearthx/i18n" + "github.com/reearth/reearthx/rerror" + "github.com/samber/lo" +) + +var ( + noGeometryFieldError = rerror.NewE(i18n.T("no geometry field in this model")) +) + +type GeoJSON = FeatureCollection + +type FeatureCollectionType string + +const FeatureCollectionTypeFeatureCollection FeatureCollectionType = "FeatureCollection" + +type FeatureCollection struct { + Features *[]Feature `json:"features,omitempty"` + Type *FeatureCollectionType `json:"type,omitempty"` +} + +type FeatureType string + +const FeatureTypeFeature FeatureType = "Feature" + +type Feature struct { + Geometry *Geometry `json:"geometry,omitempty"` + Id *string `json:"id,omitempty"` + Properties *map[string]interface{} `json:"properties,omitempty"` + Type *FeatureType `json:"type,omitempty"` +} + +type GeometryCollectionType string + +const GeometryCollectionTypeGeometryCollection GeometryCollectionType = "GeometryCollection" + +type GeometryCollection struct { + Geometries *[]Geometry `json:"geometries,omitempty"` + Type *GeometryCollectionType `json:"type,omitempty"` +} + +type GeometryType string + +const ( + GeometryTypeGeometryCollection GeometryType = "GeometryCollection" + GeometryTypeLineString GeometryType = "LineString" + GeometryTypeMultiLineString GeometryType = "MultiLineString" + GeometryTypeMultiPoint GeometryType = "MultiPoint" + GeometryTypeMultiPolygon GeometryType = "MultiPolygon" + GeometryTypePoint GeometryType = "Point" + GeometryTypePolygon GeometryType = "Polygon" +) + +type Geometry struct { + Coordinates *Geometry_Coordinates `json:"coordinates,omitempty"` + Geometries *[]Geometry `json:"geometries,omitempty"` + Type *GeometryType `json:"type,omitempty"` +} +type Geometry_Coordinates struct { + union json.RawMessage +} + +type LineString = []Point +type MultiLineString = []LineString +type MultiPoint = []Point +type MultiPolygon = []Polygon +type Point = []float64 +type Polygon = [][]Point + +func (t Geometry_Coordinates) AsPoint() (Point, error) { + var body Point + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsMultiPoint() (MultiPoint, error) { + var body MultiPoint + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsLineString() (LineString, error) { + var body LineString + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsMultiLineString() (MultiLineString, error) { + var body MultiLineString + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsPolygon() (Polygon, error) { + var body Polygon + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsMultiPolygon() (MultiPolygon, error) { + var body MultiPolygon + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *Geometry_Coordinates) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +func FeatureCollectionFromItems(ver item.VersionedList, s *schema.Schema) (*FeatureCollection, error) { + if !s.HasGeometryFields() { + return nil, noGeometryFieldError + } + + features := lo.FilterMap(ver, func(v item.Versioned, _ int) (Feature, bool) { + return FeatureFromItem(v, s) + }) + + if len(features) == 0 { + return nil, noGeometryFieldError + } + + return &FeatureCollection{ + Type: lo.ToPtr(FeatureCollectionTypeFeatureCollection), + Features: &features, + }, nil +} + +func FeatureFromItem(ver item.Versioned, s *schema.Schema) (Feature, bool) { + if s == nil { + return Feature{}, false + } + itm := ver.Value() + geoField, ok := itm.GetFirstGeometryField() + if !ok { + return Feature{}, false + } + geometry, ok := ExtractGeometry(geoField) + if !ok { + return Feature{}, false + } + + return Feature{ + Type: lo.ToPtr(FeatureTypeFeature), + Id: itm.ID().Ref().StringRef(), + Geometry: geometry, + Properties: extractProperties(itm, s), + }, true +} + +func extractProperties(itm *item.Item, s *schema.Schema) *map[string]any { + if itm == nil || s == nil { + return nil + } + properties := make(map[string]any) + nonGeoFields := lo.Filter(s.Fields(), func(f *schema.Field, _ int) bool { + return f.Type() != value.TypeGeometryObject && f.Type() != value.TypeGeometryEditor + }) + for _, field := range nonGeoFields { + key := field.Name() + itmField := itm.Field(field.ID()) + val, ok := ToGeoJSONProp(itmField) + if ok { + properties[key] = val + } + } + return &properties +} + +func ExtractGeometry(field *item.Field) (*Geometry, bool) { + geoStr, ok := field.Value().First().ValueString() + if !ok { + return nil, false + } + geometry, err := StringToGeometry(geoStr) + if err != nil { + return nil, false + } + return geometry, true +} + +func StringToGeometry(geoString string) (*Geometry, error) { + var geometry Geometry + if err := json.Unmarshal([]byte(geoString), &geometry); err != nil { + return nil, err + } + return &geometry, nil +} + +func ToGeoJSONProp(f *item.Field) (any, bool) { + if f == nil { + return nil, false + } + if len(f.Value().Values()) == 1 { + return ToGeoJsonSingleValue(f.Value().First()) + } + f.Type() + m := value.MultipleFrom(f.Type(), f.Value().Values()) + return ToGeoJSONMultipleValues(m) +} + +func ToGeoJSONMultipleValues(m *value.Multiple) ([]any, bool) { + if len(m.Values()) == 0 { + return nil, false + } + return lo.FilterMap(m.Values(), func(v *value.Value, _ int) (any, bool) { + return ToGeoJsonSingleValue(v) + }), true +} + +func ToGeoJsonSingleValue(vv *value.Value) (any, bool) { + if vv == nil { + return "", false + } + + switch vv.Type() { + case value.TypeText, value.TypeTextArea, value.TypeRichText, value.TypeMarkdown, value.TypeSelect, value.TypeTag: + v, ok := vv.ValueString() + if !ok { + return "", false + } + return v, true + case value.TypeURL: + v, ok := vv.ValueURL() + if !ok { + return "", false + } + return v.String(), true + case value.TypeAsset: + v, ok := vv.ValueAsset() + if !ok { + return "", false + } + return v.String(), true + case value.TypeInteger: + v, ok := vv.ValueInteger() + if !ok { + return "", false + } + return v, true + case value.TypeNumber: + v, ok := vv.ValueNumber() + if !ok { + return "", false + } + return v, true + case value.TypeBool, value.TypeCheckbox: + v, ok := vv.ValueBool() + if !ok { + return "", false + } + return v, true + case value.TypeDateTime: + v, ok := vv.ValueDateTime() + if !ok { + return "", false + } + return v.Format(time.RFC3339), true + default: + return "", false + } +} diff --git a/server/pkg/exporters/geojson_test.go b/server/pkg/exporters/geojson_test.go new file mode 100644 index 0000000000..0a536a0259 --- /dev/null +++ b/server/pkg/exporters/geojson_test.go @@ -0,0 +1,322 @@ +package exporters + +import ( + "encoding/json" + "net/url" + "testing" + "time" + + "github.com/reearth/reearth-cms/server/pkg/id" + "github.com/reearth/reearth-cms/server/pkg/item" + "github.com/reearth/reearth-cms/server/pkg/key" + "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 TestFeatureCollectionFromItems(t *testing.T) { + iid := id.NewItemID() + sid := id.NewSchemaID() + mid := id.NewModelID() + uid := accountdomain.NewUserID() + nid := id.NewIntegrationID() + tid := id.NewThreadID() + pid := id.NewProjectID() + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString, schema.GeometryObjectSupportedTypePolygon} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString, schema.GeometryEditorSupportedTypePolygon} + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("LineString").Key(key.Random()).MustBuild() + sf2 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Name("Name").Key(key.Random()).Multiple(true).MustBuild() + sf3 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("Polygon").Key(key.Random()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf4 := schema.NewField(tp4).NewID().Name("Age").Key(key.Random()).MustBuild() + sf5 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("IsMarried").Key(key.Random()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}").AsMultiple(), nil) + fi2 := item.NewField(sf2.ID(), value.MultipleFrom(value.TypeText, []*value.Value{value.TypeText.Value("a"), value.TypeText.Value("b"), value.TypeText.Value("c")}), nil) + fi3 := item.NewField(sf3.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) + fi4 := item.NewField(sf4.ID(), value.TypeInteger.Value(30).AsMultiple(), nil) + fi5 := item.NewField(sf5.ID(), value.TypeBool.Value(true).AsMultiple(), nil) + s1 := schema.New().ID(sid).Fields([]*schema.Field{sf1, sf2, sf3, sf4, sf5}).Workspace(accountdomain.NewWorkspaceID()).TitleField(sf1.ID().Ref()).Project(pid).MustBuild() + s2 := schema.New().ID(sid).Fields([]*schema.Field{sf2}).Workspace(accountdomain.NewWorkspaceID()).TitleField(sf2.ID().Ref()).Project(pid).MustBuild() + i1 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi1, fi2, fi3, fi4, fi5}). + Model(mid). + Thread(tid). + User(uid). + Integration(nid). + MustBuild() + v1 := version.New() + vi1 := version.MustBeValue(v1, nil, version.NewRefs(version.Latest), util.Now(), i1) + i2 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{item.NewField(sf1.ID(), value.TypeText.Value("test").AsMultiple(), nil)}). + Model(mid). + Thread(tid). + User(uid). + Integration(nid). + MustBuild() + v2 := version.New() + vi2 := version.MustBeValue(v2, nil, version.NewRefs(version.Latest), util.Now(), i2) + + // with geometry fields + ver1 := item.VersionedList{vi1} + lineString := [][]float64{ + {139.65439725962517, 36.34793305387103}, + {139.61688622815393, 35.910803456352724}, + } + jsonBytes, err := json.Marshal(lineString) + assert.Nil(t, err) + c := Geometry_Coordinates{ + union: jsonBytes, + } + g := Geometry{ + Type: lo.ToPtr(GeometryTypeLineString), + Coordinates: &c, + } + p := make(map[string]interface{}) + p["Name"] = []any{"a", "b", "c"} + p["Age"] = int64(30) + p["IsMarried"] = true + + f := Feature{ + Type: lo.ToPtr(FeatureTypeFeature), + Geometry: &g, + Properties: &p, + Id: vi1.Value().ID().Ref().StringRef(), + } + + expected1 := &FeatureCollection{ + Type: lo.ToPtr(FeatureCollectionTypeFeatureCollection), + Features: &[]Feature{f}, + } + + fc1, err1 := FeatureCollectionFromItems(ver1, s1) + assert.Nil(t, err1) + assert.Equal(t, expected1, fc1) + + // no geometry fields + ver2 := item.VersionedList{vi2} + expectErr2 := noGeometryFieldError + + fc, err := FeatureCollectionFromItems(ver2, s2) + assert.Equal(t, expectErr2, err) + assert.Nil(t, fc) +} + +func TestExtractGeometry(t *testing.T) { + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString, schema.GeometryObjectSupportedTypePolygon} + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("LineString").Key(key.Random()).MustBuild() + sf2 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Name("Name").Key(key.Random()).Multiple(true).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}").AsMultiple(), nil) + fi2 := item.NewField(sf2.ID(), value.MultipleFrom(value.TypeText, []*value.Value{value.TypeText.Value("a"), value.TypeText.Value("b"), value.TypeText.Value("c")}), nil) + + // Test with valid geometry field + geometry1, ok1 := ExtractGeometry(fi1) + assert.True(t, ok1) + assert.NotNil(t, geometry1) + assert.Equal(t, GeometryTypeLineString, *geometry1.Type) + + // Test with non-geometry field + geometry2, ok2 := ExtractGeometry(fi2) + assert.False(t, ok2) + assert.Nil(t, geometry2) +} + +func TestExtractProperties(t *testing.T) { + iid := id.NewItemID() + sid := id.NewSchemaID() + mid := id.NewModelID() + uid := accountdomain.NewUserID() + nid := id.NewIntegrationID() + tid := id.NewThreadID() + pid := id.NewProjectID() + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString, schema.GeometryObjectSupportedTypePolygon} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString, schema.GeometryEditorSupportedTypePolygon} + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("LineString").Key(key.Random()).MustBuild() + sf2 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Name("Name").Key(key.Random()).Multiple(true).MustBuild() + sf3 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("Polygon").Key(key.Random()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf4 := schema.NewField(tp4).NewID().Name("Age").Key(key.Random()).MustBuild() + sf5 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("IsMarried").Key(key.Random()).MustBuild() + fi1 := item.NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}").AsMultiple(), nil) + fi2 := item.NewField(sf2.ID(), value.MultipleFrom(value.TypeText, []*value.Value{value.TypeText.Value("a"), value.TypeText.Value("b"), value.TypeText.Value("c")}), nil) + fi3 := item.NewField(sf3.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) + fi4 := item.NewField(sf4.ID(), value.TypeInteger.Value(30).AsMultiple(), nil) + fi5 := item.NewField(sf5.ID(), value.TypeBool.Value(true).AsMultiple(), nil) + s1 := schema.New().ID(sid).Fields([]*schema.Field{sf1, sf2, sf3, sf4, sf5}).Workspace(accountdomain.NewWorkspaceID()).TitleField(sf1.ID().Ref()).Project(pid).MustBuild() + i1 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi1, fi2, fi3, fi4, fi5}). + Model(mid). + Thread(tid). + User(uid). + Integration(nid). + MustBuild() + i2 := item.New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*item.Field{fi1, fi3}). + Model(mid). + Thread(tid). + User(uid). + Integration(nid). + MustBuild() + + // Test with item containing geometry fields and non geometry fields + properties1 := extractProperties(i1, s1) + expectedProperties1 := map[string]interface{}{ + "Name": []any{"a", "b", "c"}, + "Age": int64(30), + "IsMarried": true, + } + assert.NotNil(t, properties1) + assert.Equal(t, expectedProperties1, *properties1) + + // Test with item containing only geometry fields + properties3 := extractProperties(i2, s1) + expectedProperties3 := map[string]interface{}{} + assert.NotNil(t, properties3) + assert.Equal(t, expectedProperties3, *properties3) + + // Test with nil item + properties4 := extractProperties(nil, s1) + assert.Nil(t, properties4) + + // Test with nil schema + properties5 := extractProperties(i1, nil) + assert.Nil(t, properties5) +} + +func TestStringToGeometry(t *testing.T) { + validGeoStringPoint := ` + { + "type": "Point", + "coordinates": [139.7112596, 35.6424892] + }` + geo, err := StringToGeometry(validGeoStringPoint) + assert.NoError(t, err) + assert.NotNil(t, geo) + assert.Equal(t, GeometryTypePoint, *geo.Type) + expected := []float64{139.7112596, 35.6424892} + actual, err := geo.Coordinates.AsPoint() + assert.NoError(t, err) + assert.Equal(t, expected, actual) + + // Invalid Geometry string + invalidGeometryString := "InvalidGeometry" + geo, err = StringToGeometry(invalidGeometryString) + assert.Error(t, err) + assert.Nil(t, geo) +} + +func TestToGeoJSONProp(t *testing.T) { + sf1 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if1 := item.NewField(sf1.ID(), value.TypeText.Value("test").AsMultiple(), nil) + s1, ok1 := ToGeoJSONProp(if1) + assert.Equal(t, "test", s1) + assert.True(t, ok1) + + var if2 *item.Field + s2, ok2 := ToGeoJSONProp(if2) + assert.Empty(t, s2) + assert.False(t, ok2) + + sf3 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if3 := item.NewField(sf3.ID(), value.MultipleFrom(value.TypeText, []*value.Value{value.TypeText.Value("a"), value.TypeText.Value("b"), value.TypeText.Value("c")}), nil) + s3, ok3 := ToGeoJSONProp(if3) + assert.Equal(t, []any{"a", "b", "c"}, s3) + assert.True(t, ok3) +} + +func TestToGeoJsonSingleValue(t *testing.T) { + sf1 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if1 := item.NewField(sf1.ID(), value.TypeText.Value("test").AsMultiple(), nil) + s1, ok1 := ToGeoJsonSingleValue(if1.Value().First()) + assert.Equal(t, "test", s1) + assert.True(t, ok1) + + sf2 := schema.NewField(schema.NewTextArea(lo.ToPtr(10)).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if2 := item.NewField(sf2.ID(), value.TypeTextArea.Value("test").AsMultiple(), nil) + s2, ok2 := ToGeoJsonSingleValue(if2.Value().First()) + assert.Equal(t, "test", s2) + assert.True(t, ok2) + + sf3 := schema.NewField(schema.NewURL().TypeProperty()).NewID().Key(key.Random()).MustBuild() + v3 := url.URL{Scheme: "https", Host: "reearth.io"} + if3 := item.NewField(sf3.ID(), value.TypeURL.Value(v3).AsMultiple(), nil) + s3, ok3 := ToGeoJsonSingleValue(if3.Value().First()) + assert.Equal(t, "https://reearth.io", s3) + assert.True(t, ok3) + + sf4 := schema.NewField(schema.NewAsset().TypeProperty()).NewID().Key(key.Random()).MustBuild() + v4 := id.NewAssetID() + if4 := item.NewField(sf4.ID(), value.TypeAsset.Value(v4).AsMultiple(), nil) + s4, ok4 := ToGeoJsonSingleValue(if4.Value().First()) + assert.Equal(t, v4.String(), s4) + assert.True(t, ok4) + + gid := id.NewGroupID() + igid := id.NewItemGroupID() + sf5 := schema.NewField(schema.NewGroup(gid).TypeProperty()).NewID().Key(key.Random()).Multiple(true).MustBuild() + if5 := item.NewField(sf5.ID(), value.MultipleFrom(value.TypeGroup, []*value.Value{value.TypeGroup.Value(igid)}), nil) + s5, ok5 := ToGeoJsonSingleValue(if5.Value().First()) + assert.Empty(t, s5) + assert.False(t, ok5) + + v6 := id.NewItemID() + sf6 := schema.NewField(schema.NewReference(id.NewModelID(), id.NewSchemaID(), nil, nil).TypeProperty()).NewID().Key(key.Random()).MustBuild() + if6 := item.NewField(sf6.ID(), value.TypeReference.Value(v6).AsMultiple(), nil) + s6, ok6 := ToGeoJsonSingleValue(if6.Value().First()) + assert.Empty(t, s6) + assert.False(t, ok6) + + v7 := int64(30) + in7, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp7 := in7.TypeProperty() + sf7 := schema.NewField(tp7).NewID().Name("age").Key(key.Random()).MustBuild() + if7 := item.NewField(sf7.ID(), value.TypeInteger.Value(v7).AsMultiple(), nil) + s7, ok7 := ToGeoJsonSingleValue(if7.Value().First()) + assert.Equal(t, int64(30), s7) + assert.True(t, ok7) + + v8 := float64(30.123) + in8, _ := schema.NewNumber(lo.ToPtr(float64(1)), lo.ToPtr(float64(100))) + tp8 := in8.TypeProperty() + sf8 := schema.NewField(tp8).NewID().Name("age").Key(key.Random()).MustBuild() + if8 := item.NewField(sf8.ID(), value.TypeNumber.Value(v8).AsMultiple(), nil) + s8, ok8 := ToGeoJsonSingleValue(if8.Value().First()) + assert.Equal(t, 30.123, s8) + assert.True(t, ok8) + + v9 := true + sf9 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("age").Key(key.Random()).MustBuild() + if9 := item.NewField(sf9.ID(), value.TypeBool.Value(v9).AsMultiple(), nil) + s9, ok9 := ToGeoJsonSingleValue(if9.Value().First()) + assert.Equal(t, true, s9) + assert.True(t, ok9) + + v10 := time.Now() + sf10 := schema.NewField(schema.NewDateTime().TypeProperty()).NewID().Name("age").Key(key.Random()).MustBuild() + if10 := item.NewField(sf10.ID(), value.TypeDateTime.Value(v10).AsMultiple(), nil) + s10, ok10 := ToGeoJsonSingleValue(if10.Value().First()) + assert.Equal(t, v10.Format(time.RFC3339), s10) + assert.True(t, ok10) + + var if11 *item.Field + s11, ok11 := ToGeoJsonSingleValue(if11.Value().First()) + assert.Empty(t, s11) + assert.False(t, ok11) +} diff --git a/server/pkg/integrationapi/csv.go b/server/pkg/integrationapi/csv.go new file mode 100644 index 0000000000..29edaf2bc6 --- /dev/null +++ b/server/pkg/integrationapi/csv.go @@ -0,0 +1,15 @@ +package integrationapi + +import ( + "github.com/reearth/reearth-cms/server/pkg/exporters" + "github.com/reearth/reearth-cms/server/pkg/item" + "github.com/reearth/reearth-cms/server/pkg/schema" +) + +func CSVFromItems(items item.VersionedList, s *schema.Schema) (string, error) { + csv, err := exporters.CSVFromItems(items, s) + if err != nil { + return "", err + } + return csv, nil +} diff --git a/server/pkg/integrationapi/geojson.go b/server/pkg/integrationapi/geojson.go new file mode 100644 index 0000000000..21705a70f8 --- /dev/null +++ b/server/pkg/integrationapi/geojson.go @@ -0,0 +1,87 @@ +package integrationapi + +import ( + "github.com/reearth/reearth-cms/server/pkg/exporters" + "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/samber/lo" +) + +func FeatureCollectionFromItems(ver item.VersionedList, s *schema.Schema) (*FeatureCollection, error) { + fc, err := exporters.FeatureCollectionFromItems(ver, s) + if err != nil { + return nil, err + } + return NewFeatureCollection(fc), nil +} + +func NewFeatureCollection(fc *exporters.FeatureCollection) *FeatureCollection { + if fc == nil || fc.Features == nil { + return nil + } + + features := lo.Map(*fc.Features, func(f exporters.Feature, _ int) Feature { + return *NewFeature(&f) + }) + + return &FeatureCollection{ + Type: lo.ToPtr(FeatureCollectionTypeFeatureCollection), + Features: &features, + } +} + +func NewFeature(f *exporters.Feature) *Feature { + if f == nil { + return nil + } + + return &Feature{ + Type: lo.ToPtr(FeatureTypeFeature), + Id: id.ItemIDFromRef(f.Id), + Geometry: NewGeometry(f.Geometry), + Properties: f.Properties, + } +} + +func NewGeometry(g *exporters.Geometry) *Geometry { + if g == nil { + return nil + } + + return &Geometry{ + Type: toGeometryType(*g.Type), + Coordinates: toCoordinates(*g.Coordinates), + } +} + +func toGeometryType(t exporters.GeometryType) *GeometryType { + switch t { + case exporters.GeometryTypePoint: + return lo.ToPtr(GeometryTypePoint) + case exporters.GeometryTypeMultiPoint: + return lo.ToPtr(GeometryTypeMultiPoint) + case exporters.GeometryTypeLineString: + return lo.ToPtr(GeometryTypeLineString) + case exporters.GeometryTypeMultiLineString: + return lo.ToPtr(GeometryTypeMultiLineString) + case exporters.GeometryTypePolygon: + return lo.ToPtr(GeometryTypePolygon) + case exporters.GeometryTypeMultiPolygon: + return lo.ToPtr(GeometryTypeMultiPolygon) + case exporters.GeometryTypeGeometryCollection: + return lo.ToPtr(GeometryTypeGeometryCollection) + default: + return nil + } +} + +func toCoordinates(c exporters.Geometry_Coordinates) *Geometry_Coordinates { + union, err := c.MarshalJSON() + if err != nil { + return nil + } + return &Geometry_Coordinates{ + union: union, + } +} diff --git a/server/pkg/integrationapi/types.gen.go b/server/pkg/integrationapi/types.gen.go index d9fa886023..29f3246163 100644 --- a/server/pkg/integrationapi/types.gen.go +++ b/server/pkg/integrationapi/types.gen.go @@ -4,8 +4,10 @@ package integrationapi import ( + "encoding/json" "time" + "github.com/oapi-codegen/runtime" openapi_types "github.com/oapi-codegen/runtime/types" "github.com/reearth/reearth-cms/server/pkg/id" "github.com/reearth/reearth-cms/server/pkg/model" @@ -18,6 +20,32 @@ const ( BearerAuthScopes = "bearerAuth.Scopes" ) +// Defines values for FeatureType. +const ( + FeatureTypeFeature FeatureType = "Feature" +) + +// Defines values for FeatureCollectionType. +const ( + FeatureCollectionTypeFeatureCollection FeatureCollectionType = "FeatureCollection" +) + +// Defines values for GeometryType. +const ( + GeometryTypeGeometryCollection GeometryType = "GeometryCollection" + GeometryTypeLineString GeometryType = "LineString" + GeometryTypeMultiLineString GeometryType = "MultiLineString" + GeometryTypeMultiPoint GeometryType = "MultiPoint" + GeometryTypeMultiPolygon GeometryType = "MultiPolygon" + GeometryTypePoint GeometryType = "Point" + GeometryTypePolygon GeometryType = "Polygon" +) + +// Defines values for GeometryCollectionType. +const ( + GeometryCollectionTypeGeometryCollection GeometryCollectionType = "GeometryCollection" +) + // Defines values for AssetArchiveExtractionStatus. const ( Done AssetArchiveExtractionStatus = "done" @@ -186,6 +214,18 @@ const ( ItemFilterParamsRefPublic ItemFilterParamsRef = "public" ) +// Defines values for ItemsAsCSVParamsRef. +const ( + ItemsAsCSVParamsRefLatest ItemsAsCSVParamsRef = "latest" + ItemsAsCSVParamsRefPublic ItemsAsCSVParamsRef = "public" +) + +// Defines values for ItemsAsGeoJSONParamsRef. +const ( + ItemsAsGeoJSONParamsRefLatest ItemsAsGeoJSONParamsRef = "latest" + ItemsAsGeoJSONParamsRefPublic ItemsAsGeoJSONParamsRef = "public" +) + // Defines values for ItemFilterWithProjectParamsSort. const ( ItemFilterWithProjectParamsSortCreatedAt ItemFilterWithProjectParamsSort = "createdAt" @@ -200,8 +240,20 @@ const ( // Defines values for ItemFilterWithProjectParamsRef. const ( - Latest ItemFilterWithProjectParamsRef = "latest" - Public ItemFilterWithProjectParamsRef = "public" + ItemFilterWithProjectParamsRefLatest ItemFilterWithProjectParamsRef = "latest" + ItemFilterWithProjectParamsRefPublic ItemFilterWithProjectParamsRef = "public" +) + +// Defines values for ItemsWithProjectAsCSVParamsRef. +const ( + ItemsWithProjectAsCSVParamsRefLatest ItemsWithProjectAsCSVParamsRef = "latest" + ItemsWithProjectAsCSVParamsRefPublic ItemsWithProjectAsCSVParamsRef = "public" +) + +// Defines values for ItemsWithProjectAsGeoJSONParamsRef. +const ( + Latest ItemsWithProjectAsGeoJSONParamsRef = "latest" + Public ItemsWithProjectAsGeoJSONParamsRef = "public" ) // Defines values for AssetFilterParamsSort. @@ -216,6 +268,71 @@ const ( AssetFilterParamsDirDesc AssetFilterParamsDir = "desc" ) +// Feature defines model for Feature. +type Feature struct { + Geometry *Geometry `json:"geometry,omitempty"` + Id *id.ItemID `json:"id,omitempty"` + Properties *map[string]interface{} `json:"properties,omitempty"` + Type *FeatureType `json:"type,omitempty"` +} + +// FeatureType defines model for Feature.Type. +type FeatureType string + +// FeatureCollection defines model for FeatureCollection. +type FeatureCollection struct { + Features *[]Feature `json:"features,omitempty"` + Type *FeatureCollectionType `json:"type,omitempty"` +} + +// FeatureCollectionType defines model for FeatureCollection.Type. +type FeatureCollectionType string + +// GeoJSON defines model for GeoJSON. +type GeoJSON = FeatureCollection + +// Geometry defines model for Geometry. +type Geometry struct { + Coordinates *Geometry_Coordinates `json:"coordinates,omitempty"` + Geometries *[]Geometry `json:"geometries,omitempty"` + Type *GeometryType `json:"type,omitempty"` +} + +// Geometry_Coordinates defines model for Geometry.Coordinates. +type Geometry_Coordinates struct { + union json.RawMessage +} + +// GeometryType defines model for Geometry.Type. +type GeometryType string + +// GeometryCollection defines model for GeometryCollection. +type GeometryCollection struct { + Geometries *[]Geometry `json:"geometries,omitempty"` + Type *GeometryCollectionType `json:"type,omitempty"` +} + +// GeometryCollectionType defines model for GeometryCollection.Type. +type GeometryCollectionType string + +// LineString defines model for LineString. +type LineString = []Point + +// MultiLineString defines model for MultiLineString. +type MultiLineString = []LineString + +// MultiPoint defines model for MultiPoint. +type MultiPoint = []Point + +// MultiPolygon defines model for MultiPolygon. +type MultiPolygon = []Polygon + +// Point defines model for Point. +type Point = []float64 + +// Polygon defines model for Polygon. +type Polygon = [][]Point + // Asset defines model for asset. type Asset struct { ArchiveExtractionStatus *AssetArchiveExtractionStatus `json:"archiveExtractionStatus,omitempty"` @@ -579,6 +696,36 @@ type ItemCreateJSONBody struct { MetadataFields *[]Field `json:"metadataFields,omitempty"` } +// ItemsAsCSVParams defines parameters for ItemsAsCSV. +type ItemsAsCSVParams struct { + // Page Used to select the page + Page *PageParam `form:"page,omitempty" json:"page,omitempty"` + + // PerPage Used to select the page + PerPage *PerPageParam `form:"perPage,omitempty" json:"perPage,omitempty"` + + // Ref Used to select a ref or ver + Ref *ItemsAsCSVParamsRef `form:"ref,omitempty" json:"ref,omitempty"` +} + +// ItemsAsCSVParamsRef defines parameters for ItemsAsCSV. +type ItemsAsCSVParamsRef string + +// ItemsAsGeoJSONParams defines parameters for ItemsAsGeoJSON. +type ItemsAsGeoJSONParams struct { + // Page Used to select the page + Page *PageParam `form:"page,omitempty" json:"page,omitempty"` + + // PerPage Used to select the page + PerPage *PerPageParam `form:"perPage,omitempty" json:"perPage,omitempty"` + + // Ref Used to select a ref or ver + Ref *ItemsAsGeoJSONParamsRef `form:"ref,omitempty" json:"ref,omitempty"` +} + +// ItemsAsGeoJSONParamsRef defines parameters for ItemsAsGeoJSON. +type ItemsAsGeoJSONParamsRef string + // ModelFilterParams defines parameters for ModelFilter. type ModelFilterParams struct { // Page Used to select the page @@ -663,6 +810,36 @@ type ItemCreateWithProjectJSONBody struct { MetadataFields *[]Field `json:"metadataFields,omitempty"` } +// ItemsWithProjectAsCSVParams defines parameters for ItemsWithProjectAsCSV. +type ItemsWithProjectAsCSVParams struct { + // Page Used to select the page + Page *PageParam `form:"page,omitempty" json:"page,omitempty"` + + // PerPage Used to select the page + PerPage *PerPageParam `form:"perPage,omitempty" json:"perPage,omitempty"` + + // Ref Used to select a ref or ver + Ref *ItemsWithProjectAsCSVParamsRef `form:"ref,omitempty" json:"ref,omitempty"` +} + +// ItemsWithProjectAsCSVParamsRef defines parameters for ItemsWithProjectAsCSV. +type ItemsWithProjectAsCSVParamsRef string + +// ItemsWithProjectAsGeoJSONParams defines parameters for ItemsWithProjectAsGeoJSON. +type ItemsWithProjectAsGeoJSONParams struct { + // Page Used to select the page + Page *PageParam `form:"page,omitempty" json:"page,omitempty"` + + // PerPage Used to select the page + PerPage *PerPageParam `form:"perPage,omitempty" json:"perPage,omitempty"` + + // Ref Used to select a ref or ver + Ref *ItemsWithProjectAsGeoJSONParamsRef `form:"ref,omitempty" json:"ref,omitempty"` +} + +// ItemsWithProjectAsGeoJSONParamsRef defines parameters for ItemsWithProjectAsGeoJSON. +type ItemsWithProjectAsGeoJSONParamsRef string + // AssetFilterParams defines parameters for AssetFilter. type AssetFilterParams struct { // Sort Used to define the order of the response list @@ -782,3 +959,169 @@ type FieldCreateJSONRequestBody FieldCreateJSONBody // FieldUpdateJSONRequestBody defines body for FieldUpdate for application/json ContentType. type FieldUpdateJSONRequestBody FieldUpdateJSONBody + +// AsPoint returns the union data inside the Geometry_Coordinates as a Point +func (t Geometry_Coordinates) AsPoint() (Point, error) { + var body Point + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromPoint overwrites any union data inside the Geometry_Coordinates as the provided Point +func (t *Geometry_Coordinates) FromPoint(v Point) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergePoint performs a merge with any union data inside the Geometry_Coordinates, using the provided Point +func (t *Geometry_Coordinates) MergePoint(v Point) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsMultiPoint returns the union data inside the Geometry_Coordinates as a MultiPoint +func (t Geometry_Coordinates) AsMultiPoint() (MultiPoint, error) { + var body MultiPoint + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromMultiPoint overwrites any union data inside the Geometry_Coordinates as the provided MultiPoint +func (t *Geometry_Coordinates) FromMultiPoint(v MultiPoint) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeMultiPoint performs a merge with any union data inside the Geometry_Coordinates, using the provided MultiPoint +func (t *Geometry_Coordinates) MergeMultiPoint(v MultiPoint) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsLineString returns the union data inside the Geometry_Coordinates as a LineString +func (t Geometry_Coordinates) AsLineString() (LineString, error) { + var body LineString + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromLineString overwrites any union data inside the Geometry_Coordinates as the provided LineString +func (t *Geometry_Coordinates) FromLineString(v LineString) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeLineString performs a merge with any union data inside the Geometry_Coordinates, using the provided LineString +func (t *Geometry_Coordinates) MergeLineString(v LineString) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsMultiLineString returns the union data inside the Geometry_Coordinates as a MultiLineString +func (t Geometry_Coordinates) AsMultiLineString() (MultiLineString, error) { + var body MultiLineString + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromMultiLineString overwrites any union data inside the Geometry_Coordinates as the provided MultiLineString +func (t *Geometry_Coordinates) FromMultiLineString(v MultiLineString) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeMultiLineString performs a merge with any union data inside the Geometry_Coordinates, using the provided MultiLineString +func (t *Geometry_Coordinates) MergeMultiLineString(v MultiLineString) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsPolygon returns the union data inside the Geometry_Coordinates as a Polygon +func (t Geometry_Coordinates) AsPolygon() (Polygon, error) { + var body Polygon + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromPolygon overwrites any union data inside the Geometry_Coordinates as the provided Polygon +func (t *Geometry_Coordinates) FromPolygon(v Polygon) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergePolygon performs a merge with any union data inside the Geometry_Coordinates, using the provided Polygon +func (t *Geometry_Coordinates) MergePolygon(v Polygon) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsMultiPolygon returns the union data inside the Geometry_Coordinates as a MultiPolygon +func (t Geometry_Coordinates) AsMultiPolygon() (MultiPolygon, error) { + var body MultiPolygon + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromMultiPolygon overwrites any union data inside the Geometry_Coordinates as the provided MultiPolygon +func (t *Geometry_Coordinates) FromMultiPolygon(v MultiPolygon) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeMultiPolygon performs a merge with any union data inside the Geometry_Coordinates, using the provided MultiPolygon +func (t *Geometry_Coordinates) MergeMultiPolygon(v MultiPolygon) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t Geometry_Coordinates) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *Geometry_Coordinates) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} diff --git a/server/pkg/item/field.go b/server/pkg/item/field.go index 16bf57c948..72d27dacf3 100644 --- a/server/pkg/item/field.go +++ b/server/pkg/item/field.go @@ -44,3 +44,7 @@ func (f *Field) ItemGroup() *ItemGroupID { } return util.CloneRef(f.group) } + +func (f *Field) IsGeometryField() bool { + return f.Type() == value.TypeGeometryObject || f.Type() == value.TypeGeometryEditor +} diff --git a/server/pkg/item/item.go b/server/pkg/item/item.go index 5c35e4fcc1..7dde8d18b5 100644 --- a/server/pkg/item/item.go +++ b/server/pkg/item/item.go @@ -255,3 +255,14 @@ func (i *Item) SetMetadataItem(iid id.ItemID) { func (i *Item) SetOriginalItem(iid id.ItemID) { i.originalItem = &iid } + +func (i *Item) GetFirstGeometryField() (*Field, bool) { + if i == nil { + return nil, false + } + geoFields := append(i.Fields().FieldsByType(value.TypeGeometryObject), i.Fields().FieldsByType(value.TypeGeometryEditor)...) + if len(geoFields) == 0 { + return nil, false + } + return geoFields[0], true +} diff --git a/server/pkg/item/item_test.go b/server/pkg/item/item_test.go index 8b49bc45c2..78b4de5575 100644 --- a/server/pkg/item/item_test.go +++ b/server/pkg/item/item_test.go @@ -289,3 +289,78 @@ func TestItem_GetTitle(t *testing.T) { title = i1.GetTitle(s1) assert.Equal(t, "test", *title) } + +func TestGetFirstGeometryField(t *testing.T) { + iid := id.NewItemID() + sid := id.NewSchemaID() + mid := id.NewModelID() + uid := accountdomain.NewUserID() + nid := id.NewIntegrationID() + tid := id.NewThreadID() + pid := id.NewProjectID() + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString, schema.GeometryObjectSupportedTypePolygon} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString, schema.GeometryEditorSupportedTypePolygon} + sf1 := schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name("LineString").Key(key.Random()).MustBuild() + sf2 := schema.NewField(schema.NewText(lo.ToPtr(10)).TypeProperty()).NewID().Name("Name").Key(key.Random()).Multiple(true).MustBuild() + sf3 := schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name("Polygon").Key(key.Random()).MustBuild() + in4, _ := schema.NewInteger(lo.ToPtr(int64(1)), lo.ToPtr(int64(100))) + tp4 := in4.TypeProperty() + sf4 := schema.NewField(tp4).NewID().Name("Age").Key(key.Random()).MustBuild() + sf5 := schema.NewField(schema.NewBool().TypeProperty()).NewID().Name("IsMarried").Key(key.Random()).MustBuild() + fi1 := NewField(sf1.ID(), value.TypeGeometryObject.Value("{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}").AsMultiple(), nil) + fi2 := NewField(sf2.ID(), value.MultipleFrom(value.TypeText, []*value.Value{value.TypeText.Value("a"), value.TypeText.Value("b"), value.TypeText.Value("c")}), nil) + fi3 := NewField(sf3.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) + fi4 := NewField(sf4.ID(), value.TypeInteger.Value(30).AsMultiple(), nil) + fi5 := NewField(sf5.ID(), value.TypeBool.Value(true).AsMultiple(), nil) + i1 := New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*Field{fi1, fi2, fi3, fi4, fi5}). + Model(mid). + Thread(tid). + User(uid). + Integration(nid). + MustBuild() + i2 := New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*Field{fi1, fi2, fi4, fi5}). + Model(mid). + Thread(tid). + User(uid). + Integration(nid). + MustBuild() + i3 := New(). + ID(iid). + Schema(sid). + Project(pid). + Fields([]*Field{fi2, fi4, fi5}). + Model(mid). + Thread(tid). + User(uid). + Integration(nid). + MustBuild() + + // Test with item that has two geometry fields + geometry1, ok1 := i1.GetFirstGeometryField() + assert.True(t, ok1) + assert.NotNil(t, geometry1) + + // Test with item that has one geometry field + geometry2, ok2 := i2.GetFirstGeometryField() + assert.True(t, ok2) + assert.NotNil(t, geometry2) + + // Test with item that has no geometry fields + geometry3, ok3 := i3.GetFirstGeometryField() + assert.False(t, ok3) + assert.Nil(t, geometry3) + + // Test with item that equals nil + var i4 *Item + geometry4, ok4 := i4.GetFirstGeometryField() + assert.False(t, ok4) + assert.Nil(t, geometry4) +} diff --git a/server/pkg/schema/field.go b/server/pkg/schema/field.go index 53a413cfd1..83e573a56d 100644 --- a/server/pkg/schema/field.go +++ b/server/pkg/schema/field.go @@ -190,3 +190,20 @@ func (f *Field) ValidateValue(m *value.Multiple) error { } return f.typeProperty.ValidateMultiple(m) } + +func (f *Field) IsGeometryField() bool { + return f.Type() == value.TypeGeometryObject || f.Type() == value.TypeGeometryEditor +} + +func (f *Field) SupportsPointField() bool { + var supported bool + f.TypeProperty().Match(TypePropertyMatch{ + GeometryObject: func(f *FieldGeometryObject) { + supported = f.SupportedTypes().Has(GeometryObjectSupportedTypePoint) + }, + GeometryEditor: func(f *FieldGeometryEditor) { + supported = f.SupportedTypes().Has(GeometryEditorSupportedTypePoint) + }, + }) + return supported +} diff --git a/server/pkg/schema/schema.go b/server/pkg/schema/schema.go index 7251435405..53acbd1adc 100644 --- a/server/pkg/schema/schema.go +++ b/server/pkg/schema/schema.go @@ -154,3 +154,22 @@ func (s *Schema) Clone() *Schema { titleField: s.TitleField().CloneRef(), } } + +func (s *Schema) HasGeometryFields() bool { + if s == nil { + return false + } + return len(s.FieldsByType(value.TypeGeometryObject)) > 0 || len(s.FieldsByType(value.TypeGeometryEditor)) > 0 +} + +func (s *Schema) IsPointFieldSupported() bool { + if s == nil { + return false + } + for _, f := range s.Fields() { + if f.SupportsPointField() { + return true + } + } + return false +} diff --git a/server/pkg/value/type.go b/server/pkg/value/type.go index a3eca8c05d..67803f2344 100644 --- a/server/pkg/value/type.go +++ b/server/pkg/value/type.go @@ -25,3 +25,7 @@ func (t Type) ValueFrom(i any, p TypeRegistry) *Value { } return nil } + +func (t Type) IsGeometryFieldType() bool { + return t == TypeGeometryObject || t == TypeGeometryEditor +} diff --git a/server/schemas/integration.yml b/server/schemas/integration.yml index ccb2ab9946..0d45672f67 100644 --- a/server/schemas/integration.yml +++ b/server/schemas/integration.yml @@ -389,6 +389,70 @@ paths: description: Invalid request parameter value '401': $ref: '#/components/responses/UnauthorizedError' + '/models/{modelId}/items.geojson': + parameters: + - $ref: '#/components/parameters/modelIdParam' + get: + operationId: ItemsAsGeoJSON + security: + - bearerAuth: [] + summary: Returns a GeoJSON that has a list of items as features. + tags: + - Items + - GeoJSON + description: Returns a GeoJSON that has a list of items as features. + parameters: + - $ref: '#/components/parameters/pageParam' + - $ref: '#/components/parameters/perPageParam' + - $ref: '#/components/parameters/refParam' + responses: + '200': + description: A GeoJSON object + content: + application/json: + schema: + $ref: '#/components/schemas/GeoJSON' + format: binary + '400': + description: Invalid request parameter value + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + description: Not found + '500': + description: Internal server error + '/models/{modelId}/items.csv': + parameters: + - $ref: '#/components/parameters/modelIdParam' + get: + operationId: ItemsAsCSV + security: + - bearerAuth: [] + summary: Returns a CSV that has a list of items as features. + tags: + - Items project + - CSV + description: Returns a CSV that has a list of items as features. + parameters: + - $ref: '#/components/parameters/pageParam' + - $ref: '#/components/parameters/perPageParam' + - $ref: '#/components/parameters/refParam' + responses: + '200': + description: A string in CSV format + content: + text/csv: + schema: + type: string + format: binary + '400': + description: Invalid request parameter value + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + description: Not found + '500': + description: Internal server error '/projects/{projectIdOrAlias}/models/{modelIdOrKey}': parameters: - $ref: '#/components/parameters/projectIdOrAliasParam' @@ -661,6 +725,72 @@ paths: description: Invalid request parameter value '401': $ref: '#/components/responses/UnauthorizedError' + '/projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.geojson': + parameters: + - $ref: '#/components/parameters/projectIdOrAliasParam' + - $ref: '#/components/parameters/modelIdOrKeyParam' + get: + operationId: ItemsWithProjectAsGeoJSON + security: + - bearerAuth: [] + summary: Returns a GeoJSON that has a list of items as features. + tags: + - Items project + - GeoJSON + description: Returns a GeoJSON that has a list of items as features. + parameters: + - $ref: '#/components/parameters/pageParam' + - $ref: '#/components/parameters/perPageParam' + - $ref: '#/components/parameters/refParam' + responses: + '200': + description: A GeoJSON object + content: + application/json: + schema: + $ref: '#/components/schemas/GeoJSON' + format: binary + '400': + description: Invalid request parameter value + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + description: Not found + '500': + description: Internal server error + '/projects/{projectIdOrAlias}/models/{modelIdOrKey}/items.csv': + parameters: + - $ref: '#/components/parameters/projectIdOrAliasParam' + - $ref: '#/components/parameters/modelIdOrKeyParam' + get: + operationId: ItemsWithProjectAsCSV + security: + - bearerAuth: [] + summary: Returns a CSV that has a list of items as features. + tags: + - Items project + - CSV + description: Returns a CSV that has a list of items as features. + parameters: + - $ref: '#/components/parameters/pageParam' + - $ref: '#/components/parameters/perPageParam' + - $ref: '#/components/parameters/refParam' + responses: + '200': + description: A string in CSV format + content: + text/csv: + schema: + type: string + format: binary + '400': + description: Invalid request parameter value + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + description: Not found + '500': + description: Internal server error '/items/{itemId}': parameters: - $ref: '#/components/parameters/itemIdParam' @@ -1460,6 +1590,90 @@ components: updatedAt: type: string format: date-time + GeoJSON: + $ref: '#/components/schemas/FeatureCollection' + FeatureCollection: + type: object + properties: + type: + type: string + enum: [FeatureCollection] + features: + type: array + items: + $ref: '#/components/schemas/Feature' + Feature: + type: object + properties: + id: + x-go-type: id.ItemID + type: string + type: + type: string + enum: [Feature] + geometry: + $ref: '#/components/schemas/Geometry' + properties: + type: object + Geometry: + type: object + properties: + type: + type: string + enum: [Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection] + coordinates: + oneOf: + - $ref: '#/components/schemas/Point' + - $ref: '#/components/schemas/MultiPoint' + - $ref: '#/components/schemas/LineString' + - $ref: '#/components/schemas/MultiLineString' + - $ref: '#/components/schemas/Polygon' + - $ref: '#/components/schemas/MultiPolygon' + geometries: + type: array + items: + $ref: '#/components/schemas/Geometry' + GeometryCollection: + type: object + properties: + type: + type: string + enum: [GeometryCollection] + geometries: + type: array + items: + $ref: '#/components/schemas/Geometry' + Point: + type: array + items: + type: number + format: double + minItems: 2 + maxItems: 3 + MultiPoint: + type: array + items: + $ref: '#/components/schemas/Point' + LineString: + type: array + items: + $ref: '#/components/schemas/Point' + minItems: 2 + MultiLineString: + type: array + items: + $ref: '#/components/schemas/LineString' + Polygon: + type: array + items: + type: array + items: + $ref: '#/components/schemas/Point' + minItems: 4 + MultiPolygon: + type: array + items: + $ref: '#/components/schemas/Polygon' versionedItem: type: object properties: From f3fba16a664c49d71e014f5704232e8909178b88 Mon Sep 17 00:00:00 2001 From: Nour Balaha Date: Tue, 6 Aug 2024 10:36:12 +0900 Subject: [PATCH 5/5] feat(server): export items as geojson and csv via public api (#1198) * add geojson types to public api * support multiple values in csv * support multiple geo fields in geojson * add e2e test * improve error handling * make the files downloadable --- server/e2e/publicapi_test.go | 135 +++++++++++++++++-- server/internal/adapter/integration/item.go | 18 +-- server/internal/adapter/publicapi/api.go | 66 ++------- server/internal/adapter/publicapi/csv.go | 39 ++++++ server/internal/adapter/publicapi/geojson.go | 118 ++++++++++++++++ server/internal/adapter/publicapi/item.go | 28 ++++ server/internal/adapter/publicapi/types.go | 106 +++++++++++++++ server/pkg/exporters/csv.go | 68 ++++------ server/pkg/exporters/csv_test.go | 21 ++- server/pkg/exporters/geojson.go | 6 - server/pkg/exporters/geojson_test.go | 7 +- server/pkg/integrationapi/csv.go | 10 +- 12 files changed, 478 insertions(+), 144 deletions(-) create mode 100644 server/internal/adapter/publicapi/csv.go create mode 100644 server/internal/adapter/publicapi/geojson.go diff --git a/server/e2e/publicapi_test.go b/server/e2e/publicapi_test.go index 021a51f97b..847fb8d703 100644 --- a/server/e2e/publicapi_test.go +++ b/server/e2e/publicapi_test.go @@ -29,16 +29,21 @@ var ( publicAPIItem2ID = id.NewItemID() publicAPIItem3ID = id.NewItemID() publicAPIItem4ID = id.NewItemID() + publicAPIItem6ID = id.NewItemID() + publicAPIItem7ID = id.NewItemID() publicAPIAsset1ID = id.NewAssetID() publicAPIAsset2ID = id.NewAssetID() publicAPIAssetUUID = uuid.NewString() publicAPIProjectAlias = "test-project" publicAPIModelKey = "test-model" publicAPIModelKey2 = "test-model-2" + publicAPIModelKey3 = "test-model-3" publicAPIField1Key = "test-field-1" publicAPIField2Key = "asset" publicAPIField3Key = "test-field-2" publicAPIField4Key = "asset2" + publicAPIField5Key = "geometry-object" + publicAPIField6Key = "geometry-editor" ) func TestPublicAPI(t *testing.T) { @@ -103,8 +108,26 @@ func TestPublicAPI(t *testing.T) { }, }, }, + { + "id": publicAPIItem6ID.String(), + publicAPIField1Key: "ccc", + publicAPIField3Key: []string{"aaa", "bbb", "ccc"}, + publicAPIField4Key: []any{ + map[string]any{ + "type": "asset", + "id": publicAPIAsset1ID.String(), + "url": fmt.Sprintf("https://example.com/assets/%s/%s/aaa.zip", publicAPIAssetUUID[:2], publicAPIAssetUUID[2:]), + }, + }, + publicAPIField5Key: "{\n\"type\": \"Point\",\n\t\"coordinates\": [102.0, 0.5]\n}", + publicAPIField6Key: "{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}", + }, + { + "id": publicAPIItem7ID.String(), + publicAPIField1Key: "ccc", + }, }, - "totalCount": 3, + "totalCount": 5, "hasMore": false, "limit": 50, "offset": 0, @@ -125,8 +148,8 @@ func TestPublicAPI(t *testing.T) { publicAPIField1Key: "bbb", }, }, - "totalCount": 3, - "hasMore": false, + "totalCount": 5, + "hasMore": true, "limit": 1, "offset": 1, "page": 2, @@ -146,7 +169,7 @@ func TestPublicAPI(t *testing.T) { publicAPIField1Key: "bbb", }, }, - "totalCount": 3, + "totalCount": 5, "hasMore": true, "nextCursor": publicAPIItem2ID.String(), }) @@ -221,14 +244,78 @@ func TestPublicAPI(t *testing.T) { publicAPIField3Key: []string{"aaa", "bbb", "ccc"}, // publicAPIField4Key should be removed }, + { + "id": publicAPIItem6ID.String(), + publicAPIField1Key: "ccc", + publicAPIField3Key: []string{"aaa", "bbb", "ccc"}, + publicAPIField5Key: "{\n\"type\": \"Point\",\n\t\"coordinates\": [102.0, 0.5]\n}", + publicAPIField6Key: "{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}", + }, + { + "id": publicAPIItem7ID.String(), + publicAPIField1Key: "ccc", + }, }, - "totalCount": 3, + "totalCount": 5, "hasMore": false, "limit": 50, "offset": 0, "page": 1, }) + e.GET("/api/p/{project}/{model}.geojson", publicAPIProjectAlias, publicAPIModelKey). + Expect(). + Status(http.StatusOK). + JSON(). + IsEqual(map[string]interface{}{ + "type": "FeatureCollection", + "features": []map[string]interface{}{ + { + "type": "Feature", + "id": publicAPIItem6ID.String(), + "geometry": map[string]interface{}{ + "type": "Point", + "coordinates": []interface{}{ + 102, + 0.5, + }, + }, + "properties": map[string]interface{}{ + "test-field-1": "ccc", + "test-field-2": []interface{}{ + "aaa", + "bbb", + "ccc", + }, + }, + }, + }, + }) + + // no geometry field + e.GET("/api/p/{project}/{model}.geojson", publicAPIProjectAlias, publicAPIModelKey3). + Expect(). + Status(http.StatusNotFound). + JSON(). + IsEqual(map[string]interface{}{ + "error": "not found", + }) + + e.GET("/api/p/{project}/{model}.csv", publicAPIProjectAlias, publicAPIModelKey). + Expect(). + Status(http.StatusOK). + Body(). + IsEqual(fmt.Sprintf("id,location_lat,location_lng,test-field-1,asset,test-field-2,asset2\n%s,102,0.5,ccc,,aaa,\n", publicAPIItem6ID.String())) + + // no geometry field + e.GET("/api/p/{project}/{model}.csv", publicAPIProjectAlias, publicAPIModelKey3). + Expect(). + Status(http.StatusNotFound). + JSON(). + IsEqual(map[string]interface{}{ + "error": "not found", + }) + e.GET("/api/p/{project}/{model}/{item}", publicAPIProjectAlias, publicAPIModelKey, publicAPIItem1ID). Expect(). Status(http.StatusOK). @@ -271,16 +358,25 @@ func publicAPISeeder(ctx context.Context, r *repo.Container) error { af := asset.NewFile().Name("bbb.txt").Path("aaa/bbb.txt").Build() fid := id.NewFieldID() + gst := schema.GeometryObjectSupportedTypeList{schema.GeometryObjectSupportedTypePoint, schema.GeometryObjectSupportedTypeLineString} + gest := schema.GeometryEditorSupportedTypeList{schema.GeometryEditorSupportedTypePoint, schema.GeometryEditorSupportedTypeLineString} s := schema.New().NewID().Project(p1.ID()).Workspace(p1.Workspace()).Fields(schema.FieldList{ - schema.NewField(schema.NewText(nil).TypeProperty()).ID(fid).Key(key.New(publicAPIField1Key)).MustBuild(), - schema.NewField(schema.NewAsset().TypeProperty()).NewID().Key(key.New(publicAPIField2Key)).MustBuild(), - schema.NewField(schema.NewText(nil).TypeProperty()).NewID().Key(key.New(publicAPIField3Key)).Multiple(true).MustBuild(), - schema.NewField(schema.NewAsset().TypeProperty()).NewID().Key(key.New(publicAPIField4Key)).Multiple(true).MustBuild(), + schema.NewField(schema.NewText(nil).TypeProperty()).ID(fid).Name(publicAPIField1Key).Key(key.New(publicAPIField1Key)).MustBuild(), + schema.NewField(schema.NewAsset().TypeProperty()).NewID().Name(publicAPIField2Key).Key(key.New(publicAPIField2Key)).MustBuild(), + schema.NewField(schema.NewText(nil).TypeProperty()).NewID().Name(publicAPIField3Key).Key(key.New(publicAPIField3Key)).Multiple(true).MustBuild(), + schema.NewField(schema.NewAsset().TypeProperty()).NewID().Name(publicAPIField4Key).Key(key.New(publicAPIField4Key)).Multiple(true).MustBuild(), + schema.NewField(schema.NewGeometryObject(gst).TypeProperty()).NewID().Name(publicAPIField5Key).Key(key.New(publicAPIField5Key)).MustBuild(), + schema.NewField(schema.NewGeometryEditor(gest).TypeProperty()).NewID().Name(publicAPIField6Key).Key(key.New(publicAPIField6Key)).MustBuild(), }).TitleField(fid.Ref()).MustBuild() + s2 := schema.New().NewID().Project(p1.ID()).Workspace(p1.Workspace()).Fields(schema.FieldList{ + schema.NewField(schema.NewText(nil).TypeProperty()).ID(fid).Name(publicAPIField1Key).Key(key.New(publicAPIField1Key)).MustBuild(), + }).MustBuild() + m := model.New().ID(publicAPIModelID).Project(p1.ID()).Schema(s.ID()).Public(true).Key(key.New(publicAPIModelKey)).MustBuild() - // not public model - m2 := model.New().ID(publicAPIModelID).Project(p1.ID()).Schema(s.ID()).Key(key.New(publicAPIModelKey2)).Public(false).MustBuild() + // m2 is not a public model + m2 := model.New().ID(publicAPIModelID).Project(p1.ID()).Schema(s.ID()).Name(publicAPIModelKey2).Key(key.New(publicAPIModelKey2)).Public(false).MustBuild() + m3 := model.New().ID(publicAPIModelID).Project(p1.ID()).Schema(s2.ID()).Name(publicAPIModelKey3).Key(key.New(publicAPIModelKey3)).Public(true).MustBuild() i1 := item.New().ID(publicAPIItem1ID).Model(m.ID()).Schema(s.ID()).Project(p1.ID()).Thread(id.NewThreadID()).User(uid).Fields([]*item.Field{ item.NewField(s.Fields()[0].ID(), value.TypeText.Value("aaa").AsMultiple(), nil), @@ -308,6 +404,19 @@ func publicAPISeeder(ctx context.Context, r *repo.Container) error { item.NewField(s.Fields()[1].ID(), value.TypeAsset.Value(a.ID()).AsMultiple(), nil), }).MustBuild() + i6 := item.New().ID(publicAPIItem6ID).Model(m.ID()).Schema(s.ID()).Project(p1.ID()).Thread(id.NewThreadID()).User(uid).Fields([]*item.Field{ + item.NewField(s.Fields()[0].ID(), value.TypeText.Value("ccc").AsMultiple(), nil), + item.NewField(s.Fields()[1].ID(), value.TypeAsset.Value(publicAPIAsset2ID).AsMultiple(), nil), + item.NewField(s.Fields()[2].ID(), value.NewMultiple(value.TypeText, []any{"aaa", "bbb", "ccc"}), nil), + item.NewField(s.Fields()[3].ID(), value.TypeAsset.Value(a.ID()).AsMultiple(), nil), + item.NewField(s.Fields()[4].ID(), value.TypeGeometryObject.Value("{\n\"type\": \"Point\",\n\t\"coordinates\": [102.0, 0.5]\n}").AsMultiple(), nil), + item.NewField(s.Fields()[5].ID(), value.TypeGeometryEditor.Value("{\"coordinates\":[[139.65439725962517,36.34793305387103],[139.61688622815393,35.910803456352724]],\"type\":\"LineString\"}").AsMultiple(), nil), + }).MustBuild() + + i7 := item.New().ID(publicAPIItem7ID).Model(m3.ID()).Schema(s2.ID()).Project(p1.ID()).Thread(id.NewThreadID()).User(uid).Fields([]*item.Field{ + item.NewField(s.Fields()[0].ID(), value.TypeText.Value("ccc").AsMultiple(), nil), + }).MustBuild() + lo.Must0(r.Project.Save(ctx, p1)) lo.Must0(r.Asset.Save(ctx, a)) lo.Must0(r.AssetFile.Save(ctx, a.ID(), af)) @@ -318,9 +427,13 @@ func publicAPISeeder(ctx context.Context, r *repo.Container) error { lo.Must0(r.Item.Save(ctx, i3)) lo.Must0(r.Item.Save(ctx, i4)) lo.Must0(r.Item.Save(ctx, i5)) + lo.Must0(r.Item.Save(ctx, i6)) + lo.Must0(r.Item.Save(ctx, i7)) lo.Must0(r.Item.UpdateRef(ctx, i1.ID(), version.Public, version.Latest.OrVersion().Ref())) lo.Must0(r.Item.UpdateRef(ctx, i2.ID(), version.Public, version.Latest.OrVersion().Ref())) lo.Must0(r.Item.UpdateRef(ctx, i3.ID(), version.Public, version.Latest.OrVersion().Ref())) + lo.Must0(r.Item.UpdateRef(ctx, i6.ID(), version.Public, version.Latest.OrVersion().Ref())) + lo.Must0(r.Item.UpdateRef(ctx, i7.ID(), version.Public, version.Latest.OrVersion().Ref())) return nil } diff --git a/server/internal/adapter/integration/item.go b/server/internal/adapter/integration/item.go index 287e2787ea..94429266df 100644 --- a/server/internal/adapter/integration/item.go +++ b/server/internal/adapter/integration/item.go @@ -3,7 +3,7 @@ package integration import ( "context" "errors" - "strings" + "io" "github.com/reearth/reearth-cms/server/internal/usecase" "github.com/reearth/reearth-cms/server/pkg/model" @@ -117,15 +117,13 @@ func (s *Server) ItemsAsCSV(ctx context.Context, request ItemsAsCSVRequestObject return ItemsAsCSV400Response{}, err } - csvString, err := integrationapi.CSVFromItems(items, sp.Schema()) + pr, pw := io.Pipe() + err = integrationapi.CSVFromItems(pw, items, sp.Schema()) if err != nil { return nil, err } - reader := strings.NewReader(csvString) - contentLength := reader.Len() return ItemsAsCSV200TextcsvResponse{ - Body: reader, - ContentLength: int64(contentLength), + Body: pr, }, nil } @@ -277,15 +275,13 @@ func (s *Server) ItemsWithProjectAsCSV(ctx context.Context, request ItemsWithPro return ItemsWithProjectAsCSV400Response{}, err } - csvString, err := integrationapi.CSVFromItems(items, sp.Schema()) + pr, pw := io.Pipe() + err = integrationapi.CSVFromItems(pw, items, sp.Schema()) if err != nil { return nil, err } - reader := strings.NewReader(csvString) - contentLength := reader.Len() return ItemsWithProjectAsCSV200TextcsvResponse{ - Body: reader, - ContentLength: int64(contentLength), + Body: pr, }, nil } diff --git a/server/internal/adapter/publicapi/api.go b/server/internal/adapter/publicapi/api.go index 2c075358d3..ebc9a26117 100644 --- a/server/internal/adapter/publicapi/api.go +++ b/server/internal/adapter/publicapi/api.go @@ -2,18 +2,12 @@ package publicapi import ( "context" - "encoding/csv" - "encoding/json" - "fmt" - "io" "net/http" "strconv" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" - "github.com/reearth/reearth-cms/server/pkg/schema" - "github.com/reearth/reearthx/log" "github.com/reearth/reearthx/usecasex" "github.com/samber/lo" ) @@ -74,22 +68,29 @@ func PublicApiItemList() echo.HandlerFunc { if strings.Contains(m, ".") { m, resType, _ = strings.Cut(m, ".") } - if resType != "csv" && resType != "json" { + if resType != "csv" && resType != "json" && resType != "geojson" { resType = "json" } - res, s, err := ctrl.GetItems(ctx, c.Param("project"), m, p) + items, _, err := ctrl.GetItems(ctx, c.Param("project"), m, p) if err != nil { return err } + vi, s, err1 := ctrl.GetVersionedItems(ctx, c.Param("project"), m, p) + if err1 != nil { + return err1 + } + switch resType { case "csv": - return toCSV(c, res, s) + return toCSV(c, vi, s) + case "geojson": + return toGeoJSON(c, vi, s) case "json": - return c.JSON(http.StatusOK, res) + return c.JSON(http.StatusOK, items) default: - return c.JSON(http.StatusOK, res) + return c.JSON(http.StatusOK, items) } } } @@ -157,46 +158,3 @@ func intParams(c echo.Context, params ...string) (int64, bool) { } return 0, false } - -func toCSV(c echo.Context, l ListResult[Item], s *schema.Schema) error { - pr, pw := io.Pipe() - - go func() { - var err error - defer func() { - _ = pw.CloseWithError(err) - }() - - w := csv.NewWriter(pw) - keys := lo.Map(s.Fields(), func(f *schema.Field, _ int) string { - return f.Key().String() - }) - err = w.Write(append([]string{"id"}, keys...)) - if err != nil { - log.Errorf("filed to write csv headers, err: %+v", err) - return - } - - for _, itm := range l.Results { - values := []string{itm.ID} - for _, k := range keys { - // values = append(values, fmt.Sprintf("%v", itm.Fields[k])) - var v []byte - v, err = json.Marshal(itm.Fields[k]) - if err != nil { - log.Errorf("filed to json marshal field value, err: %+v", err) - return - } - values = append(values, fmt.Sprintf("%v", string(v))) - } - err = w.Write(values) - if err != nil { - log.Errorf("filed to write csv value, err: %+v", err) - return - } - } - w.Flush() - }() - - return c.Stream(http.StatusOK, "text/csv", pr) -} diff --git a/server/internal/adapter/publicapi/csv.go b/server/internal/adapter/publicapi/csv.go new file mode 100644 index 0000000000..291088a46f --- /dev/null +++ b/server/internal/adapter/publicapi/csv.go @@ -0,0 +1,39 @@ +package publicapi + +import ( + "io" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/reearth/reearth-cms/server/pkg/exporters" + "github.com/reearth/reearth-cms/server/pkg/item" + "github.com/reearth/reearth-cms/server/pkg/schema" + "github.com/reearth/reearthx/log" +) + +func toCSV(c echo.Context, l item.VersionedList, s *schema.Schema) error { + if !s.IsPointFieldSupported() { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "error": "point type is not supported in this model", + }) + } + + pr, pw := io.Pipe() + err := generateCSV(pw, l, s) + if err != nil { + log.Errorf("failed to generate CSV: %+v", err) + } + + c.Response().Header().Set(echo.HeaderContentDisposition, "attachment;") + c.Response().Header().Set(echo.HeaderContentType, "text/csv") + return c.Stream(http.StatusOK, "text/csv", pr) +} + +func generateCSV(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) error { + err := exporters.CSVFromItems(pw, l, s) + if err != nil { + return err + } + + return nil +} diff --git a/server/internal/adapter/publicapi/geojson.go b/server/internal/adapter/publicapi/geojson.go new file mode 100644 index 0000000000..79f155da0d --- /dev/null +++ b/server/internal/adapter/publicapi/geojson.go @@ -0,0 +1,118 @@ +package publicapi + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/reearth/reearth-cms/server/pkg/exporters" + "github.com/reearth/reearth-cms/server/pkg/item" + "github.com/reearth/reearth-cms/server/pkg/schema" + "github.com/reearth/reearthx/log" + "github.com/samber/lo" +) + +func toGeoJSON(c echo.Context, l item.VersionedList, s *schema.Schema) error { + if !s.HasGeometryFields() { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "error": "no geometry field in this model", + }) + } + + pr, pw := io.Pipe() + go handleGeoJSONGeneration(pw, l, s) + + c.Response().Header().Set(echo.HeaderContentDisposition, "attachment;") + c.Response().Header().Set(echo.HeaderContentType, "application/json") + return c.Stream(http.StatusOK, "application/json", pr) +} + +func handleGeoJSONGeneration(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) { + err := generateGeoJSON(pw, l, s) + if err != nil { + log.Errorf("failed to generate GeoJSON: %+v", err) + } + _ = pw.CloseWithError(err) +} + +func generateGeoJSON(pw *io.PipeWriter, l item.VersionedList, s *schema.Schema) error { + features, err := exporters.FeatureCollectionFromItems(l, s) + + if err != nil { + return err + } + + featureCollection := ToFeatureCollection(features) + return json.NewEncoder(pw).Encode(featureCollection) +} + +func ToFeatureCollection(fc *exporters.FeatureCollection) *FeatureCollection { + if fc == nil || fc.Features == nil { + return nil + } + + features := lo.Map(*fc.Features, func(f exporters.Feature, _ int) Feature { + return *ToFeature(&f) + }) + + return &FeatureCollection{ + Type: lo.ToPtr(FeatureCollectionTypeFeatureCollection), + Features: &features, + } +} + +func ToFeature(f *exporters.Feature) *Feature { + if f == nil { + return nil + } + + return &Feature{ + Type: lo.ToPtr(FeatureTypeFeature), + Id: f.Id, + Geometry: ToGeometry(f.Geometry), + Properties: f.Properties, + } +} + +func ToGeometry(g *exporters.Geometry) *Geometry { + if g == nil { + return nil + } + + return &Geometry{ + Type: toGeometryType(*g.Type), + Coordinates: toCoordinates(*g.Coordinates), + } +} + +func toGeometryType(t exporters.GeometryType) *GeometryType { + switch t { + case exporters.GeometryTypePoint: + return lo.ToPtr(GeometryTypePoint) + case exporters.GeometryTypeMultiPoint: + return lo.ToPtr(GeometryTypeMultiPoint) + case exporters.GeometryTypeLineString: + return lo.ToPtr(GeometryTypeLineString) + case exporters.GeometryTypeMultiLineString: + return lo.ToPtr(GeometryTypeMultiLineString) + case exporters.GeometryTypePolygon: + return lo.ToPtr(GeometryTypePolygon) + case exporters.GeometryTypeMultiPolygon: + return lo.ToPtr(GeometryTypeMultiPolygon) + case exporters.GeometryTypeGeometryCollection: + return lo.ToPtr(GeometryTypeGeometryCollection) + default: + return nil + } +} + +func toCoordinates(c exporters.Geometry_Coordinates) *Geometry_Coordinates { + union, err := c.MarshalJSON() + if err != nil { + return nil + } + return &Geometry_Coordinates{ + union: union, + } +} diff --git a/server/internal/adapter/publicapi/item.go b/server/internal/adapter/publicapi/item.go index 72e7b2908d..d78b962b3a 100644 --- a/server/internal/adapter/publicapi/item.go +++ b/server/internal/adapter/publicapi/item.go @@ -3,6 +3,7 @@ package publicapi import ( "context" "errors" + "github.com/reearth/reearth-cms/server/internal/adapter" "github.com/reearth/reearth-cms/server/pkg/asset" "github.com/reearth/reearth-cms/server/pkg/id" @@ -113,6 +114,33 @@ func (c *Controller) GetItems(ctx context.Context, prj, model string, p ListPara return res, sp.Schema(), nil } +func (c *Controller) GetVersionedItems(ctx context.Context, prj, model string, p ListParam) (item.VersionedList, *schema.Schema, error) { + pr, err := c.checkProject(ctx, prj) + if err != nil { + return item.VersionedList{}, nil, err + } + + m, err := c.usecases.Model.FindByKey(ctx, pr.ID(), model, nil) + if err != nil { + return item.VersionedList{}, nil, err + } + if !m.Public() { + return item.VersionedList{}, nil, rerror.ErrNotFound + } + + sp, err := c.usecases.Schema.FindByModel(ctx, m.ID(), nil) + if err != nil { + return item.VersionedList{}, nil, err + } + + items, _, err := c.usecases.Item.FindPublicByModel(ctx, m.ID(), p.Pagination, nil) + if err != nil { + return item.VersionedList{}, nil, err + } + + return items, sp.Schema(), nil +} + func getReferencedItems(ctx context.Context, i *item.Item, prp bool, urlResolver asset.URLResolver) []Item { op := adapter.Operator(ctx) uc := adapter.Usecases(ctx) diff --git a/server/internal/adapter/publicapi/types.go b/server/internal/adapter/publicapi/types.go index 8cb0c089aa..020fcd3c2b 100644 --- a/server/internal/adapter/publicapi/types.go +++ b/server/internal/adapter/publicapi/types.go @@ -211,3 +211,109 @@ func NewItemAsset(a *asset.Asset, urlResolver asset.URLResolver) ItemAsset { URL: u, } } + +// GeoJSON +type GeoJSON = FeatureCollection + +type FeatureCollectionType string + +const FeatureCollectionTypeFeatureCollection FeatureCollectionType = "FeatureCollection" + +type FeatureCollection struct { + Features *[]Feature `json:"features,omitempty"` + Type *FeatureCollectionType `json:"type,omitempty"` +} + +type FeatureType string + +const FeatureTypeFeature FeatureType = "Feature" + +type Feature struct { + Geometry *Geometry `json:"geometry,omitempty"` + Id *string `json:"id,omitempty"` + Properties *map[string]interface{} `json:"properties,omitempty"` + Type *FeatureType `json:"type,omitempty"` +} + +type GeometryCollectionType string + +const GeometryCollectionTypeGeometryCollection GeometryCollectionType = "GeometryCollection" + +type GeometryCollection struct { + Geometries *[]Geometry `json:"geometries,omitempty"` + Type *GeometryCollectionType `json:"type,omitempty"` +} + +type GeometryType string + +const ( + GeometryTypeGeometryCollection GeometryType = "GeometryCollection" + GeometryTypeLineString GeometryType = "LineString" + GeometryTypeMultiLineString GeometryType = "MultiLineString" + GeometryTypeMultiPoint GeometryType = "MultiPoint" + GeometryTypeMultiPolygon GeometryType = "MultiPolygon" + GeometryTypePoint GeometryType = "Point" + GeometryTypePolygon GeometryType = "Polygon" +) + +type Geometry struct { + Coordinates *Geometry_Coordinates `json:"coordinates,omitempty"` + Geometries *[]Geometry `json:"geometries,omitempty"` + Type *GeometryType `json:"type,omitempty"` +} +type Geometry_Coordinates struct { + union json.RawMessage +} + +type LineString = []Point +type MultiLineString = []LineString +type MultiPoint = []Point +type MultiPolygon = []Polygon +type Point = []float64 +type Polygon = [][]Point + +func (t Geometry_Coordinates) AsPoint() (Point, error) { + var body Point + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsMultiPoint() (MultiPoint, error) { + var body MultiPoint + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsLineString() (LineString, error) { + var body LineString + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsMultiLineString() (MultiLineString, error) { + var body MultiLineString + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsPolygon() (Polygon, error) { + var body Polygon + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) AsMultiPolygon() (MultiPolygon, error) { + var body MultiPolygon + err := json.Unmarshal(t.union, &body) + return body, err +} + +func (t Geometry_Coordinates) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *Geometry_Coordinates) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} diff --git a/server/pkg/exporters/csv.go b/server/pkg/exporters/csv.go index 862b8d7794..d63b2b37d5 100644 --- a/server/pkg/exporters/csv.go +++ b/server/pkg/exporters/csv.go @@ -2,8 +2,8 @@ package exporters import ( "encoding/csv" + "io" "strconv" - "strings" "time" "github.com/reearth/reearth-cms/server/pkg/item" @@ -19,31 +19,36 @@ var ( pointFieldIsNotSupportedError = rerror.NewE(i18n.T("point type is not supported in any geometry field in this model")) ) -func CSVFromItems(items item.VersionedList, s *schema.Schema) (string, error) { +func CSVFromItems(pw *io.PipeWriter, items item.VersionedList, s *schema.Schema) error { if !s.IsPointFieldSupported() { - return "", pointFieldIsNotSupportedError + return pointFieldIsNotSupportedError } - keys, nonGeoFields := buildCSVHeaders(s) - data := [][]string{} - data = append(data, keys) - for _, ver := range items { - row, ok := rowFromItem(ver.Value(), nonGeoFields) - if ok { - data = append(data, row) - } - } + w := csv.NewWriter(pw) + go func() { + defer pw.Close() - if len(data) == 1 { - return "", noPointFieldError - } - - csv, err := convertToCSV(data) - if err != nil { - return "", err - } + keys, nonGeoFields := buildCSVHeaders(s) + if err := w.Write(keys); err != nil { + pw.CloseWithError(err) + return + } + for _, ver := range items { + row, ok := rowFromItem(ver.Value(), nonGeoFields) + if ok { + if err := w.Write(row); err != nil { + pw.CloseWithError(err) + return + } + } + } + w.Flush() + if err := w.Error(); err != nil { + pw.CloseWithError(err) + } + }() - return csv, nil + return nil } func buildCSVHeaders(s *schema.Schema) ([]string, []*schema.Field) { @@ -98,21 +103,6 @@ func extractFirstPointField(itm *item.Item) ([]float64, error) { return nil, noPointFieldError } -func convertToCSV(data [][]string) (string, error) { - var sb strings.Builder - w := csv.NewWriter(&sb) - for _, row := range data { - if err := w.Write(row); err != nil { - return "", err - } - } - w.Flush() - if err := w.Error(); err != nil { - return "", err - } - return sb.String(), nil -} - func float64ToString(f float64) string { return strconv.FormatFloat(f, 'f', -1, 64) } @@ -146,12 +136,6 @@ func ToCSVValue(vv *value.Value) string { return "" } return v.String() - case value.TypeAsset: - v, ok := vv.ValueAsset() - if !ok { - return "" - } - return v.String() case value.TypeInteger: v, ok := vv.ValueInteger() if !ok { diff --git a/server/pkg/exporters/csv_test.go b/server/pkg/exporters/csv_test.go index 87758fbbe5..cfd686d7dc 100644 --- a/server/pkg/exporters/csv_test.go +++ b/server/pkg/exporters/csv_test.go @@ -1,7 +1,7 @@ package exporters import ( - "fmt" + "io" "net/url" "testing" "time" @@ -50,10 +50,9 @@ func TestCSVFromItems(t *testing.T) { // with geometry fields ver1 := item.VersionedList{vi1} - csvString, err := CSVFromItems(ver1, s1) - expected1 := fmt.Sprintf("id,location_lat,location_lng,age,isMarried\n%s,139.28179282584915,36.58570985749664,30,true\n", vi1.Value().ID()) + _, pw := io.Pipe() + err := CSVFromItems(pw, ver1, s1) assert.Nil(t, err) - assert.Equal(t, expected1, csvString) // no geometry fields iid2 := id.NewItemID() @@ -74,9 +73,9 @@ func TestCSVFromItems(t *testing.T) { vi2 := version.MustBeValue(v2, nil, version.NewRefs(version.Latest), util.Now(), i2) ver2 := item.VersionedList{vi2} expectErr2 := pointFieldIsNotSupportedError - csvString, err = CSVFromItems(ver2, s2) + _, pw1 := io.Pipe() + err = CSVFromItems(pw1, ver2, s2) assert.Equal(t, expectErr2, err) - assert.Empty(t, csvString) // point field is not supported iid3 := id.NewItemID() @@ -98,9 +97,9 @@ func TestCSVFromItems(t *testing.T) { vi3 := version.MustBeValue(v3, nil, version.NewRefs(version.Latest), util.Now(), i3) ver3 := item.VersionedList{vi3} expectErr3 := pointFieldIsNotSupportedError - csvString, err = CSVFromItems(ver3, s3) + _, pw2 := io.Pipe() + err = CSVFromItems(pw2, ver3, s3) assert.Equal(t, expectErr3, err) - assert.Empty(t, csvString) } func TestBuildCSVHeaders(t *testing.T) { @@ -300,10 +299,9 @@ func TestToCSVValue(t *testing.T) { assert.Equal(t, "https://reearth.io", s3) sf4 := schema.NewField(schema.NewAsset().TypeProperty()).NewID().Key(key.Random()).MustBuild() - v4 := id.NewAssetID() - if4 := item.NewField(sf4.ID(), value.TypeAsset.Value(v4).AsMultiple(), nil) + if4 := item.NewField(sf4.ID(), value.TypeAsset.Value(id.NewAssetID()).AsMultiple(), nil) s4 := ToCSVValue(if4.Value().First()) - assert.Equal(t, v4.String(), s4) + assert.Empty(t, s4) gid := id.NewGroupID() igid := id.NewItemGroupID() @@ -350,4 +348,3 @@ func TestToCSVValue(t *testing.T) { s11 := ToCSVValue(if11.Value().First()) assert.Empty(t, s11) } - diff --git a/server/pkg/exporters/geojson.go b/server/pkg/exporters/geojson.go index 48b8f4a2bf..2cbdc91787 100644 --- a/server/pkg/exporters/geojson.go +++ b/server/pkg/exporters/geojson.go @@ -240,12 +240,6 @@ func ToGeoJsonSingleValue(vv *value.Value) (any, bool) { return "", false } return v.String(), true - case value.TypeAsset: - v, ok := vv.ValueAsset() - if !ok { - return "", false - } - return v.String(), true case value.TypeInteger: v, ok := vv.ValueInteger() if !ok { diff --git a/server/pkg/exporters/geojson_test.go b/server/pkg/exporters/geojson_test.go index 0a536a0259..5308b5c59a 100644 --- a/server/pkg/exporters/geojson_test.go +++ b/server/pkg/exporters/geojson_test.go @@ -262,11 +262,10 @@ func TestToGeoJsonSingleValue(t *testing.T) { assert.True(t, ok3) sf4 := schema.NewField(schema.NewAsset().TypeProperty()).NewID().Key(key.Random()).MustBuild() - v4 := id.NewAssetID() - if4 := item.NewField(sf4.ID(), value.TypeAsset.Value(v4).AsMultiple(), nil) + if4 := item.NewField(sf4.ID(), value.TypeAsset.Value(id.NewAssetID()).AsMultiple(), nil) s4, ok4 := ToGeoJsonSingleValue(if4.Value().First()) - assert.Equal(t, v4.String(), s4) - assert.True(t, ok4) + assert.Empty(t, s4) + assert.False(t, ok4) gid := id.NewGroupID() igid := id.NewItemGroupID() diff --git a/server/pkg/integrationapi/csv.go b/server/pkg/integrationapi/csv.go index 29edaf2bc6..242d959806 100644 --- a/server/pkg/integrationapi/csv.go +++ b/server/pkg/integrationapi/csv.go @@ -1,15 +1,17 @@ package integrationapi import ( + "io" + "github.com/reearth/reearth-cms/server/pkg/exporters" "github.com/reearth/reearth-cms/server/pkg/item" "github.com/reearth/reearth-cms/server/pkg/schema" ) -func CSVFromItems(items item.VersionedList, s *schema.Schema) (string, error) { - csv, err := exporters.CSVFromItems(items, s) +func CSVFromItems(pw *io.PipeWriter, items item.VersionedList, s *schema.Schema) error { + err := exporters.CSVFromItems(pw, items, s) if err != nil { - return "", err + return err } - return csv, nil + return nil }