diff --git a/server/Makefile b/server/Makefile index ea89ff1aca..2bd22f3f3d 100644 --- a/server/Makefile +++ b/server/Makefile @@ -1,3 +1,5 @@ +TEST_DIR ?= ./... + help: @echo "Usage:" @echo " make " @@ -5,6 +7,7 @@ help: @echo "Targets:" @echo " lint Run golangci-lint with auto-fix" @echo " test Run unit tests with race detector in short mode" + @echo " failcheck Run unit tests with fail-fast and no parallel execution" @echo " e2e Run end-to-end tests" @echo " build Build the project" @echo " run-app Run the application" @@ -17,6 +20,9 @@ lint: test: go test -race -short -v ./... +failcheck: + go test -race -short -failfast -p 1 $(TEST_DIR) + e2e: go test -v ./e2e/... @@ -32,4 +38,4 @@ run-db: gql: go generate ./internal/adapter/gql -.PHONY: lint test e2e build run-app run-db gql +.PHONY: lint test failcheck e2e build run-app run-db gql diff --git a/server/e2e/gql_import_export_test.go b/server/e2e/gql_import_export_test.go new file mode 100644 index 0000000000..3631dded3d --- /dev/null +++ b/server/e2e/gql_import_export_test.go @@ -0,0 +1,471 @@ +package e2e + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "testing" + + "github.com/reearth/reearth/server/internal/app/config" + "github.com/stretchr/testify/assert" +) + +// go test -v -run TestCallExportProject ./e2e/... + +func TestCallExportProject(t *testing.T) { + + e := StartServer(t, &config.Config{ + Origins: []string{"https://example.com"}, + AuthSrv: config.AuthSrvConfig{ + Disabled: true, + }, + }, true, baseSeeder) + + pID := createProject(e, "test") + + _, _, sID := createScene(e, pID) + + createStory(e, sID, "test", 0) + + requestBody := GraphQLRequest{ + OperationName: "ExportProject", + Query: "mutation ExportProject($projectId: ID!) { exportProject(input: {projectId: $projectId}) { projectData __typename } }", + Variables: map[string]any{ + "projectId": pID, + }, + } + + res := e.POST("/api/graphql"). + WithHeader("Origin", "https://example.com"). + WithHeader("authorization", "Bearer test"). + WithHeader("X-Reearth-Debug-User", uID.String()). + WithHeader("Content-Type", "application/json"). + WithJSON(requestBody). + Expect(). + Status(http.StatusOK). + JSON(). + Object(). + Value("data").Object(). + Value("exportProject").Object(). + Value("projectData").Object() + + res.Value("scene").Object() + res.Value("project").Object() + res.Value("plugins").Array() + +} + +// go test -v -run TestCallImportProject ./e2e/... + +func TestCallImportProject(t *testing.T) { + + e := StartServer(t, &config.Config{ + Origins: []string{"https://example.com"}, + AuthSrv: config.AuthSrvConfig{ + Disabled: true, + }, + }, true, baseSeeder) + + pID := createProject(e, "test") + + _, _, sID := createScene(e, pID) + + _, _, storyID := createStory(e, sID, "test", 0) + + testFilePath := "test_project.json" + err := writeTestJSONFile(testFilePath, pID, wID.String(), sID, storyID) + assert.NoError(t, err) + defer func() { + if err := os.Remove(testFilePath); err != nil { + t.Fatalf("failed to delete test file: %v", err) + } + }() + + res := e.POST("/api/graphql"). + WithHeader("Origin", "https://example.com"). + WithHeader("authorization", "Bearer test"). + WithHeader("X-Reearth-Debug-User", uID.String()). + WithMultipart(). + WithFormField("operations", `{ + "operationName": "ImportProject", + "variables": {"file": null}, + "query": "mutation ImportProject($file: Upload!) { importProject(input: {file: $file}) { projectData __typename }}" + }`). + WithFormField("map", `{"1":["variables.file"]}`). + WithFile("1", testFilePath). + Expect(). + Status(http.StatusOK). + JSON(). + Object(). + Value("data").Object(). + Value("importProject").Object(). + Value("projectData").Object() + + res.Value("scene").Object() + res.Value("project").Object() + res.Value("plugins").Array() + +} + +func writeTestJSONFile(filePath string, pID string, wID string, sID string, storyID string) error { + + var data map[string]any + err := json.Unmarshal([]byte(fmt.Sprintf(`{ + "plugins": [ + { + "id": "%s~reearth-plugin-communication-demo-beta~1.0.0", + "sceneId": "%s", + "name": "Plugin Communication Demo for Beta", + "version": "1.0.0", + "description": "", + "author": "", + "repositoryUrl": "", + "extensions": [ + { + "extensionId": "widgetcomm", + "pluginId": "%s~reearth-plugin-communication-demo-beta~1.0.0", + "type": "WIDGET", + "name": "Communication Demo Widget", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "%s~reearth-plugin-communication-demo-beta~1.0.0/widgetcomm", + "allTranslatedName": { + "en": "Communication Demo Widget" + }, + "translatedName": "", + "translatedDescription": "" + }, + { + "extensionId": "storyblockcomm", + "pluginId": "%s~reearth-plugin-communication-demo-beta~1.0.0", + "type": "StoryBlock", + "name": "Communication Demo Story Block", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "%s~reearth-plugin-communication-demo-beta~1.0.0/storyblockcomm", + "allTranslatedName": { + "en": "Communication Demo Story Block" + }, + "translatedName": "", + "translatedDescription": "" + }, + { + "extensionId": "infoboxblockcomm", + "pluginId": "%s~reearth-plugin-communication-demo-beta~1.0.0", + "type": "InfoboxBlock", + "name": "Communication Demo Infobox Block", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "%s~reearth-plugin-communication-demo-beta~1.0.0/infoboxblockcomm", + "allTranslatedName": { + "en": "Communication Demo Infobox Block" + }, + "translatedName": "", + "translatedDescription": "" + } + ], + "allTranslatedName": { + "en": "Plugin Communication Demo for Beta" + }, + "translatedName": "", + "translatedDescription": "" + } + ], + "project": { + "id": "%s", + "isArchived": false, + "isBasicAuthActive": false, + "basicAuthUsername": "", + "basicAuthPassword": "", + "createdAt": "2024-09-11T19:17:39.418+09:00", + "updatedAt": "2024-09-12T02:07:39.09Z", + "name": "ProjectName1", + "description": "ProjectOverview1", + "alias": "", + "publicTitle": "", + "publicDescription": "", + "publicImage": "", + "publicNoIndex": false, + "imageUrl": { + "Scheme": "http", + "Opaque": "", + "User": null, + "Host": "localhost:8080", + "Path": "/assets/01j7g9d988ct8hajjxfsb6e1n6.jpeg", + "RawPath": "", + "OmitHost": false, + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "teamId": "%s", + "visualizer": "cesium", + "publishmentStatus": "PRIVATE", + "coreSupport": true, + "enableGa": false, + "trackingId": "", + "starred": false + }, + "scene": { + "schemaVersion": 1, + "id": "%s", + "publishedAt": "2024-09-12T11:07:46.117668+09:00", + "property": { + "default": { + "ion": { + "type": "string", + "value": "" + } + }, + "tiles": [ + { + "id": "01j7g9ddv4sbf8tgt5cbjxrksh", + "tile_opacity": { + "type": "number", + "value": 1 + } + }, + { + "id": "01j7hzp3akr64nz033582braxa", + "tile_type": { + "type": "string", + "value": "default_label" + }, + "tile_zoomLevel": { + "type": "array", + "value": [ + null, + null + ] + } + } + ] + }, + "plugins": {}, + "layers": null, + "widgets": [ + { + "id": "01j7g9h4f1k93vspn3gdtz67az", + "pluginId": "reearth", + "extensionId": "button", + "property": { + "default": { + "buttonBgcolor": { + "type": "string", + "value": "#79b4beff" + }, + "buttonColor": { + "type": "string", + "value": "#171289ff" + }, + "buttonTitle": { + "type": "string", + "value": "TestButton1" + } + } + }, + "enabled": false, + "extended": false + }, + { + "id": "01j7g9jckefd0zxyy34bbygmhy", + "pluginId": "reearth", + "extensionId": "navigator", + "property": { + "default": { + "visible": { + "type": "string", + "value": "desktop" + } + } + }, + "enabled": false, + "extended": false + } + ], + "widgetAlignSystem": { + "inner": null, + "outer": { + "left": { + "top": { + "widgetIds": [ + "01j7g9h4f1k93vspn3gdtz67az", + "01j7g9jr89rjq1egrb1hhcd8jy" + ], + "align": "start", + "padding": { + "top": 0, + "bottom": 0, + "left": 0, + "right": 0 + }, + "gap": null, + "centered": false, + "background": null + }, + "middle": null, + "bottom": null + }, + "center": null, + "right": { + "top": { + "widgetIds": [ + "01j7g9jckefd0zxyy34bbygmhy" + ], + "align": "start", + "padding": { + "top": 0, + "bottom": 0, + "left": 0, + "right": 0 + }, + "gap": null, + "centered": false, + "background": null + }, + "middle": null, + "bottom": null + } + } + }, + "tags": [], + "clusters": [], + "story": { + "id": "%s", + "property": {}, + "pages": [ + { + "id": "01j7g9ddwk4a12x1t8wm865s6h", + "property": { + "title": { + "color": { + "type": "string", + "value": "#9a19bfff" + }, + "title": { + "type": "string", + "value": "Title1" + } + } + }, + "title": "Untitled", + "blocks": [ + { + "id": "01j7g9mdnjk1jafw592btqx6t7", + "property": { + "default": { + "text": { + "type": "string", + "value": "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Block1\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}" + } + } + }, + "plugins": null, + "extensionId": "textStoryBlock", + "pluginId": "reearth" + }, + { + "id": "01j7g9n3x4yqae71crdjcpeyc0", + "property": { + "default": { + "text": { + "type": "string", + "value": "## MarkDown1" + } + } + }, + "plugins": null, + "extensionId": "mdTextStoryBlock", + "pluginId": "reearth" + }, + { + "id": "01j7g9nnnap0cwa1farwd841xc", + "property": { + "default": { + "src": { + "type": "url", + "value": "http://localhost:8080/assets/01j7g9nwtq1zqc7ex5gfvd1mbe.jpeg" + } + } + }, + "plugins": null, + "extensionId": "imageStoryBlock", + "pluginId": "reearth" + } + ], + "swipeable": false, + "swipeableLayers": null, + "layers": [] + } + ], + "position": "right", + "bgColor": "#b2efd8ff" + }, + "nlsLayers": [ + { + "id": "01j7g9gwj6qbv286pcwwmwq5ds", + "title": "japan_architecture (2).csv", + "layerType": "simple", + "config": { + "data": { + "csv": { + "latColumn": "lat", + "lngColumn": "lng" + }, + "type": "csv", + "url": "http://localhost:8080/assets/01j7g9gpba44e0nxwc727nax0q.csv" + }, + "layerStyleId": "" + }, + "isVisible": true, + "nlsInfobox": { + "id": "01j7hzs4e48p8s1cw7thep56b4", + "property": {}, + "blocks": [] + }, + "isSketch": false + } + ], + "layerStyles": [ + { + "id": "01j7hzqgycv76hxsygmcrb47m6", + "name": "スタイル_0", + "value": { + "color": "red" + } + }, + { + "id": "01j7hzrgc3ag8m1ftzye05csgx", + "name": "スタイル_1", + "value": { + "font": "bold" + } + } + ], + "coreSupport": true, + "enableGa": false, + "trackingId": "" + } +}`, sID, sID, sID, sID, sID, sID, sID, sID, pID, wID, sID, storyID)), &data) + if err != nil { + return err + } + file, err := os.Create(filePath) + if err != nil { + return err + } + defer func() { + if cerr := file.Close(); cerr != nil { + err = cerr + } + }() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(data) +} diff --git a/server/gql/project.graphql b/server/gql/project.graphql index 42a2245082..448131b39b 100644 --- a/server/gql/project.graphql +++ b/server/gql/project.graphql @@ -97,6 +97,14 @@ input ProjectSort { direction: SortDirection! } +input ExportProjectInput { + projectId: ID! +} + +input ImportProjectInput { + file: Upload! +} + # Payload type ProjectPayload { @@ -107,6 +115,14 @@ type DeleteProjectPayload { projectId: ID! } +type ExportProjectPayload { + projectData: JSON! +} + +type ImportProjectPayload { + projectData: JSON! +} + # Connection type ProjectConnection { @@ -121,8 +137,14 @@ type ProjectEdge { node: Project } -extend type Query{ - projects(teamId: ID!, includeArchived: Boolean, pagination: Pagination, keyword: String, sort: ProjectSort): ProjectConnection! +extend type Query { + projects( + teamId: ID! + includeArchived: Boolean + pagination: Pagination + keyword: String + sort: ProjectSort + ): ProjectConnection! checkProjectAlias(alias: String!): ProjectAliasAvailability! starredProjects(teamId: ID!): ProjectConnection! } @@ -132,4 +154,6 @@ extend type Mutation { updateProject(input: UpdateProjectInput!): ProjectPayload publishProject(input: PublishProjectInput!): ProjectPayload deleteProject(input: DeleteProjectInput!): DeleteProjectPayload -} \ No newline at end of file + exportProject(input: ExportProjectInput!): ExportProjectPayload + importProject(input: ImportProjectInput!): ImportProjectPayload +} diff --git a/server/internal/adapter/gql/generated.go b/server/internal/adapter/gql/generated.go index eb21021dfc..f1e90fb50c 100644 --- a/server/internal/adapter/gql/generated.go +++ b/server/internal/adapter/gql/generated.go @@ -338,6 +338,10 @@ type ComplexityRoot struct { Style func(childComplexity int) int } + ExportProjectPayload struct { + ProjectData func(childComplexity int) int + } + Feature struct { Geometry func(childComplexity int) int ID func(childComplexity int) int @@ -364,6 +368,10 @@ type ComplexityRoot struct { ParentLayer func(childComplexity int) int } + ImportProjectPayload struct { + ProjectData func(childComplexity int) int + } + Infobox struct { Fields func(childComplexity int) int Layer func(childComplexity int) int @@ -652,9 +660,11 @@ type ComplexityRoot struct { DuplicateNLSLayer func(childComplexity int, input gqlmodel.DuplicateNLSLayerInput) int DuplicateStoryPage func(childComplexity int, input gqlmodel.DuplicateStoryPageInput) int DuplicateStyle func(childComplexity int, input gqlmodel.DuplicateStyleInput) int + ExportProject func(childComplexity int, input gqlmodel.ExportProjectInput) int ImportDataset func(childComplexity int, input gqlmodel.ImportDatasetInput) int ImportDatasetFromGoogleSheet func(childComplexity int, input gqlmodel.ImportDatasetFromGoogleSheetInput) int ImportLayer func(childComplexity int, input gqlmodel.ImportLayerInput) int + ImportProject func(childComplexity int, input gqlmodel.ImportProjectInput) int InstallPlugin func(childComplexity int, input gqlmodel.InstallPluginInput) int LinkDatasetToPropertyValue func(childComplexity int, input gqlmodel.LinkDatasetToPropertyValueInput) int MoveInfoboxField func(childComplexity int, input gqlmodel.MoveInfoboxFieldInput) int @@ -1561,6 +1571,8 @@ type MutationResolver interface { UpdateProject(ctx context.Context, input gqlmodel.UpdateProjectInput) (*gqlmodel.ProjectPayload, error) PublishProject(ctx context.Context, input gqlmodel.PublishProjectInput) (*gqlmodel.ProjectPayload, error) DeleteProject(ctx context.Context, input gqlmodel.DeleteProjectInput) (*gqlmodel.DeleteProjectPayload, error) + ExportProject(ctx context.Context, input gqlmodel.ExportProjectInput) (*gqlmodel.ExportProjectPayload, error) + ImportProject(ctx context.Context, input gqlmodel.ImportProjectInput) (*gqlmodel.ImportProjectPayload, error) UpdatePropertyValue(ctx context.Context, input gqlmodel.UpdatePropertyValueInput) (*gqlmodel.PropertyFieldPayload, error) RemovePropertyField(ctx context.Context, input gqlmodel.RemovePropertyFieldInput) (*gqlmodel.PropertyFieldPayload, error) UploadFileToProperty(ctx context.Context, input gqlmodel.UploadFileToPropertyInput) (*gqlmodel.PropertyFieldPayload, error) @@ -2576,6 +2588,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DuplicateStylePayload.Style(childComplexity), true + case "ExportProjectPayload.projectData": + if e.complexity.ExportProjectPayload.ProjectData == nil { + break + } + + return e.complexity.ExportProjectPayload.ProjectData(childComplexity), true + case "Feature.geometry": if e.complexity.Feature.Geometry == nil { break @@ -2653,6 +2672,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImportLayerPayload.ParentLayer(childComplexity), true + case "ImportProjectPayload.projectData": + if e.complexity.ImportProjectPayload.ProjectData == nil { + break + } + + return e.complexity.ImportProjectPayload.ProjectData(childComplexity), true + case "Infobox.fields": if e.complexity.Infobox.Fields == nil { break @@ -4322,6 +4348,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DuplicateStyle(childComplexity, args["input"].(gqlmodel.DuplicateStyleInput)), true + case "Mutation.exportProject": + if e.complexity.Mutation.ExportProject == nil { + break + } + + args, err := ec.field_Mutation_exportProject_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ExportProject(childComplexity, args["input"].(gqlmodel.ExportProjectInput)), true + case "Mutation.importDataset": if e.complexity.Mutation.ImportDataset == nil { break @@ -4358,6 +4396,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ImportLayer(childComplexity, args["input"].(gqlmodel.ImportLayerInput)), true + case "Mutation.importProject": + if e.complexity.Mutation.ImportProject == nil { + break + } + + args, err := ec.field_Mutation_importProject_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ImportProject(childComplexity, args["input"].(gqlmodel.ImportProjectInput)), true + case "Mutation.installPlugin": if e.complexity.Mutation.InstallPlugin == nil { break @@ -8221,9 +8271,11 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputDuplicateNLSLayerInput, ec.unmarshalInputDuplicateStoryPageInput, ec.unmarshalInputDuplicateStyleInput, + ec.unmarshalInputExportProjectInput, ec.unmarshalInputImportDatasetFromGoogleSheetInput, ec.unmarshalInputImportDatasetInput, ec.unmarshalInputImportLayerInput, + ec.unmarshalInputImportProjectInput, ec.unmarshalInputInstallPluginInput, ec.unmarshalInputLinkDatasetToPropertyValueInput, ec.unmarshalInputMoveInfoboxFieldInput, @@ -9540,6 +9592,14 @@ input ProjectSort { direction: SortDirection! } +input ExportProjectInput { + projectId: ID! +} + +input ImportProjectInput { + file: Upload! +} + # Payload type ProjectPayload { @@ -9550,6 +9610,14 @@ type DeleteProjectPayload { projectId: ID! } +type ExportProjectPayload { + projectData: JSON! +} + +type ImportProjectPayload { + projectData: JSON! +} + # Connection type ProjectConnection { @@ -9564,8 +9632,14 @@ type ProjectEdge { node: Project } -extend type Query{ - projects(teamId: ID!, includeArchived: Boolean, pagination: Pagination, keyword: String, sort: ProjectSort): ProjectConnection! +extend type Query { + projects( + teamId: ID! + includeArchived: Boolean + pagination: Pagination + keyword: String + sort: ProjectSort + ): ProjectConnection! checkProjectAlias(alias: String!): ProjectAliasAvailability! starredProjects(teamId: ID!): ProjectConnection! } @@ -9575,7 +9649,10 @@ extend type Mutation { updateProject(input: UpdateProjectInput!): ProjectPayload publishProject(input: PublishProjectInput!): ProjectPayload deleteProject(input: DeleteProjectInput!): DeleteProjectPayload -}`, BuiltIn: false}, + exportProject(input: ExportProjectInput!): ExportProjectPayload + importProject(input: ImportProjectInput!): ImportProjectPayload +} +`, BuiltIn: false}, {Name: "../../../gql/property.graphql", Input: `type PropertySchema { id: ID! groups: [PropertySchemaGroup!]! @@ -11314,6 +11391,21 @@ func (ec *executionContext) field_Mutation_duplicateStyle_args(ctx context.Conte return args, nil } +func (ec *executionContext) field_Mutation_exportProject_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 gqlmodel.ExportProjectInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNExportProjectInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐExportProjectInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_importDatasetFromGoogleSheet_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -11359,6 +11451,21 @@ func (ec *executionContext) field_Mutation_importLayer_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_importProject_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 gqlmodel.ImportProjectInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNImportProjectInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐImportProjectInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_installPlugin_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -18983,6 +19090,50 @@ func (ec *executionContext) fieldContext_DuplicateStylePayload_style(ctx context return fc, nil } +func (ec *executionContext) _ExportProjectPayload_projectData(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.ExportProjectPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ExportProjectPayload_projectData(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ProjectData, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(gqlmodel.JSON) + fc.Result = res + return ec.marshalNJSON2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐJSON(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ExportProjectPayload_projectData(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ExportProjectPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type JSON does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Feature_type(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Feature) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Feature_type(ctx, field) if err != nil { @@ -19542,6 +19693,50 @@ func (ec *executionContext) fieldContext_ImportLayerPayload_parentLayer(ctx cont return fc, nil } +func (ec *executionContext) _ImportProjectPayload_projectData(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.ImportProjectPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImportProjectPayload_projectData(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ProjectData, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(gqlmodel.JSON) + fc.Result = res + return ec.marshalNJSON2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐJSON(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImportProjectPayload_projectData(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImportProjectPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type JSON does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Infobox_sceneId(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Infobox) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Infobox_sceneId(ctx, field) if err != nil { @@ -31588,6 +31783,118 @@ func (ec *executionContext) fieldContext_Mutation_deleteProject(ctx context.Cont return fc, nil } +func (ec *executionContext) _Mutation_exportProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_exportProject(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ExportProject(rctx, fc.Args["input"].(gqlmodel.ExportProjectInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*gqlmodel.ExportProjectPayload) + fc.Result = res + return ec.marshalOExportProjectPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐExportProjectPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_exportProject(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "projectData": + return ec.fieldContext_ExportProjectPayload_projectData(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ExportProjectPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_exportProject_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_importProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_importProject(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ImportProject(rctx, fc.Args["input"].(gqlmodel.ImportProjectInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*gqlmodel.ImportProjectPayload) + fc.Result = res + return ec.marshalOImportProjectPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐImportProjectPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_importProject(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "projectData": + return ec.fieldContext_ImportProjectPayload_projectData(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImportProjectPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_importProject_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_updatePropertyValue(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_updatePropertyValue(ctx, field) if err != nil { @@ -59928,6 +60235,33 @@ func (ec *executionContext) unmarshalInputDuplicateStyleInput(ctx context.Contex return it, nil } +func (ec *executionContext) unmarshalInputExportProjectInput(ctx context.Context, obj interface{}) (gqlmodel.ExportProjectInput, error) { + var it gqlmodel.ExportProjectInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"projectId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalNID2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐID(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputImportDatasetFromGoogleSheetInput(ctx context.Context, obj interface{}) (gqlmodel.ImportDatasetFromGoogleSheetInput, error) { var it gqlmodel.ImportDatasetFromGoogleSheetInput asMap := map[string]interface{}{} @@ -60065,6 +60399,33 @@ func (ec *executionContext) unmarshalInputImportLayerInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputImportProjectInput(ctx context.Context, obj interface{}) (gqlmodel.ImportProjectInput, error) { + var it gqlmodel.ImportProjectInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"file"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "file": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("file")) + data, err := ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v) + if err != nil { + return it, err + } + it.File = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputInstallPluginInput(ctx context.Context, obj interface{}) (gqlmodel.InstallPluginInput, error) { var it gqlmodel.InstallPluginInput asMap := map[string]interface{}{} @@ -65374,6 +65735,45 @@ func (ec *executionContext) _DuplicateStylePayload(ctx context.Context, sel ast. return out } +var exportProjectPayloadImplementors = []string{"ExportProjectPayload"} + +func (ec *executionContext) _ExportProjectPayload(ctx context.Context, sel ast.SelectionSet, obj *gqlmodel.ExportProjectPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, exportProjectPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ExportProjectPayload") + case "projectData": + out.Values[i] = ec._ExportProjectPayload_projectData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var featureImplementors = []string{"Feature"} func (ec *executionContext) _Feature(ctx context.Context, sel ast.SelectionSet, obj *gqlmodel.Feature) graphql.Marshaler { @@ -65596,6 +65996,45 @@ func (ec *executionContext) _ImportLayerPayload(ctx context.Context, sel ast.Sel return out } +var importProjectPayloadImplementors = []string{"ImportProjectPayload"} + +func (ec *executionContext) _ImportProjectPayload(ctx context.Context, sel ast.SelectionSet, obj *gqlmodel.ImportProjectPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, importProjectPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ImportProjectPayload") + case "projectData": + out.Values[i] = ec._ImportProjectPayload_projectData(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var infoboxImplementors = []string{"Infobox"} func (ec *executionContext) _Infobox(ctx context.Context, sel ast.SelectionSet, obj *gqlmodel.Infobox) graphql.Marshaler { @@ -69125,6 +69564,14 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_deleteProject(ctx, field) }) + case "exportProject": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_exportProject(ctx, field) + }) + case "importProject": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_importProject(ctx, field) + }) case "updatePropertyValue": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_updatePropertyValue(ctx, field) @@ -77759,6 +78206,11 @@ func (ec *executionContext) unmarshalNDuplicateStyleInput2githubᚗcomᚋreearth return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNExportProjectInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐExportProjectInput(ctx context.Context, v interface{}) (gqlmodel.ExportProjectInput, error) { + res, err := ec.unmarshalInputExportProjectInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNFeature2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐFeature(ctx context.Context, sel ast.SelectionSet, v gqlmodel.Feature) graphql.Marshaler { return ec._Feature(ctx, sel, &v) } @@ -78092,6 +78544,11 @@ func (ec *executionContext) unmarshalNImportLayerInput2githubᚗcomᚋreearthᚋ return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNImportProjectInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐImportProjectInput(ctx context.Context, v interface{}) (gqlmodel.ImportProjectInput, error) { + res, err := ec.unmarshalInputImportProjectInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNInfobox2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐInfobox(ctx context.Context, sel ast.SelectionSet, v gqlmodel.Infobox) graphql.Marshaler { return ec._Infobox(ctx, sel, &v) } @@ -81158,6 +81615,13 @@ func (ec *executionContext) marshalODuplicateStylePayload2ᚖgithubᚗcomᚋreea return ec._DuplicateStylePayload(ctx, sel, v) } +func (ec *executionContext) marshalOExportProjectPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐExportProjectPayload(ctx context.Context, sel ast.SelectionSet, v *gqlmodel.ExportProjectPayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ExportProjectPayload(ctx, sel, v) +} + func (ec *executionContext) marshalOFeatureCollection2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐFeatureCollection(ctx context.Context, sel ast.SelectionSet, v *gqlmodel.FeatureCollection) graphql.Marshaler { if v == nil { return graphql.Null @@ -81266,6 +81730,13 @@ func (ec *executionContext) marshalOImportLayerPayload2ᚖgithubᚗcomᚋreearth return ec._ImportLayerPayload(ctx, sel, v) } +func (ec *executionContext) marshalOImportProjectPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐImportProjectPayload(ctx context.Context, sel ast.SelectionSet, v *gqlmodel.ImportProjectPayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ImportProjectPayload(ctx, sel, v) +} + func (ec *executionContext) marshalOInfobox2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐInfobox(ctx context.Context, sel ast.SelectionSet, v *gqlmodel.Infobox) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/server/internal/adapter/gql/gqlmodel/convert.go b/server/internal/adapter/gql/gqlmodel/convert.go index 9376ac757e..f2e2f4d50a 100644 --- a/server/internal/adapter/gql/gqlmodel/convert.go +++ b/server/internal/adapter/gql/gqlmodel/convert.go @@ -6,6 +6,7 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/reearth/reearth/server/internal/usecase/interfaces" "github.com/reearth/reearth/server/pkg/file" + "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearth/server/pkg/visualizer" "github.com/reearth/reearthx/usecasex" "github.com/samber/lo" @@ -64,6 +65,33 @@ func ToVisualizerRef(v visualizer.Visualizer) *Visualizer { return &v2 } +func FromPluginExtension(p PluginExtensionType) plugin.ExtensionType { + switch p { + case PluginExtensionTypeBlock: + return plugin.ExtensionTypeBlock + case PluginExtensionTypeCluster: + return plugin.ExtensionTypeCluster + case PluginExtensionTypeInfobox: + return plugin.ExtensionTypeInfobox + case PluginExtensionTypeInfoboxBlock: + return plugin.ExtensionTypeInfoboxBlock + case PluginExtensionTypePrimitive: + return plugin.ExtensionTypePrimitive + case PluginExtensionTypeStory: + return plugin.ExtensionTypeStory + case PluginExtensionTypeStoryBlock: + return plugin.ExtensionTypeStoryBlock + case PluginExtensionTypeStoryPage: + return plugin.ExtensionTypeStoryPage + case PluginExtensionTypeVisualizer: + return plugin.ExtensionTypeVisualizer + case PluginExtensionTypeWidget: + return plugin.ExtensionTypeWidget + default: + return plugin.ExtensionType("") + } +} + func FromFile(f *graphql.Upload) *file.File { if f == nil { return nil diff --git a/server/internal/adapter/gql/gqlmodel/convert_plugin.go b/server/internal/adapter/gql/gqlmodel/convert_plugin.go index c0a1c29914..632a40e4e3 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_plugin.go +++ b/server/internal/adapter/gql/gqlmodel/convert_plugin.go @@ -1,8 +1,11 @@ package gqlmodel import ( + "encoding/json" + "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearthx/util" + "github.com/samber/lo" ) func ToPlugin(p *plugin.Plugin) *Plugin { @@ -41,6 +44,24 @@ func ToPlugin(p *plugin.Plugin) *Plugin { } } +func ToPlugins(pl []*plugin.Plugin) []*Plugin { + return lo.Map(pl, func(s *plugin.Plugin, _ int) *Plugin { + return ToPlugin(s) + }) +} + +func ToPluginsFromJSON(data []interface{}) []*Plugin { + var plgs []*Plugin + bytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + return nil + } + if err := json.Unmarshal(bytes, &plgs); err != nil { + return nil + } + return plgs +} + func ToPluginExtensionType(t plugin.ExtensionType) PluginExtensionType { switch t { case plugin.ExtensionTypePrimitive: diff --git a/server/internal/adapter/gql/gqlmodel/convert_project.go b/server/internal/adapter/gql/gqlmodel/convert_project.go index 01500b4b70..79e1c4e9ed 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_project.go +++ b/server/internal/adapter/gql/gqlmodel/convert_project.go @@ -1,6 +1,7 @@ package gqlmodel import ( + "encoding/json" "time" "github.com/reearth/reearth/server/pkg/project" @@ -67,6 +68,18 @@ func ToProject(p *project.Project) *Project { } } +func ToProjectFromJSON(data map[string]any) *Project { + var p Project + bytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + return nil + } + if err := json.Unmarshal(bytes, &p); err != nil { + return nil + } + return &p +} + func ProjectSortTypeFrom(pst *ProjectSort) *project.SortType { if pst == nil { return nil diff --git a/server/internal/adapter/gql/gqlmodel/convert_scene.go b/server/internal/adapter/gql/gqlmodel/convert_scene.go index f78ffcc31c..ea19b0a7f2 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_scene.go +++ b/server/internal/adapter/gql/gqlmodel/convert_scene.go @@ -64,6 +64,8 @@ func ToStyle(v *scene.Style) *Style { ID: IDFrom(v.ID()), Name: v.Name(), Value: JSON(*v.Value()), + + SceneID: IDFrom(v.Scene()), } } diff --git a/server/internal/adapter/gql/gqlmodel/convert_storytelling.go b/server/internal/adapter/gql/gqlmodel/convert_storytelling.go index d2a1374b36..f019ab9c48 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_storytelling.go +++ b/server/internal/adapter/gql/gqlmodel/convert_storytelling.go @@ -20,6 +20,7 @@ func ToStory(s *storytelling.Story) *Story { CreatedAt: s.Id().Timestamp(), UpdatedAt: s.UpdatedAt(), PublishedAt: s.PublishedAt(), + SceneID: IDFrom(s.Scene()), PanelPosition: ToStoryPosition(s.PanelPosition()), BgColor: ToStoryBgColor(s.BgColor()), diff --git a/server/internal/adapter/gql/gqlmodel/models_gen.go b/server/internal/adapter/gql/gqlmodel/models_gen.go index 912e509544..84b30131b9 100644 --- a/server/internal/adapter/gql/gqlmodel/models_gen.go +++ b/server/internal/adapter/gql/gqlmodel/models_gen.go @@ -558,6 +558,14 @@ type DuplicateStylePayload struct { Style *Style `json:"style"` } +type ExportProjectInput struct { + ProjectID ID `json:"projectId"` +} + +type ExportProjectPayload struct { + ProjectData JSON `json:"projectData"` +} + type Feature struct { Type string `json:"type"` Geometry Geometry `json:"geometry"` @@ -606,6 +614,14 @@ type ImportLayerPayload struct { ParentLayer *LayerGroup `json:"parentLayer"` } +type ImportProjectInput struct { + File graphql.Upload `json:"file"` +} + +type ImportProjectPayload struct { + ProjectData JSON `json:"projectData"` +} + type Infobox struct { SceneID ID `json:"sceneId"` LayerID ID `json:"layerId"` diff --git a/server/internal/adapter/gql/resolver_mutation_project.go b/server/internal/adapter/gql/resolver_mutation_project.go index daa82eb5c0..bb82526629 100644 --- a/server/internal/adapter/gql/resolver_mutation_project.go +++ b/server/internal/adapter/gql/resolver_mutation_project.go @@ -2,6 +2,8 @@ package gql import ( "context" + "encoding/json" + "io" "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" "github.com/reearth/reearth/server/internal/usecase/interfaces" @@ -107,3 +109,88 @@ func (r *mutationResolver) DeleteProject(ctx context.Context, input gqlmodel.Del return &gqlmodel.DeleteProjectPayload{ProjectID: input.ProjectID}, nil } + +func (r *mutationResolver) ExportProject(ctx context.Context, input gqlmodel.ExportProjectInput) (*gqlmodel.ExportProjectPayload, error) { + + pid, err := gqlmodel.ToID[id.Project](input.ProjectID) + if err != nil { + return nil, err + } + + prj, res, plgs, err := usecases(ctx).Project.ExportProject(ctx, pid, getOperator(ctx)) + if err != nil { + return nil, err + } + res["project"] = gqlmodel.ToProject(prj) + res["plugins"] = gqlmodel.ToPlugins(plgs) + + return &gqlmodel.ExportProjectPayload{ + ProjectData: res, + }, nil + +} + +func (r *mutationResolver) ImportProject(ctx context.Context, input gqlmodel.ImportProjectInput) (*gqlmodel.ImportProjectPayload, error) { + + fileBytes, err := io.ReadAll(input.File.File) + if err != nil { + return nil, err + } + + var jsonData map[string]interface{} + if err := json.Unmarshal(fileBytes, &jsonData); err != nil { + return nil, err + } + + projectData, _ := jsonData["project"].(map[string]interface{}) + prj, tx, err := usecases(ctx).Project.ImportProject(ctx, projectData) + if err != nil { + return nil, err + } + defer func() { + if err2 := tx.End(ctx); err == nil && err2 != nil { + err = err2 + } + }() + + pluginsData, _ := jsonData["plugins"].([]interface{}) + _, err = usecases(ctx).Plugin.ImportPlugins(ctx, pluginsData) + if err != nil { + return nil, err + } + + sceneData, _ := jsonData["scene"].(map[string]interface{}) + _, err = usecases(ctx).Scene.ImportScene(ctx, prj, sceneData) + if err != nil { + return nil, err + } + + _, err = usecases(ctx).NLSLayer.ImportNLSLayers(ctx, sceneData) + if err != nil { + return nil, err + } + + _, err = usecases(ctx).Style.ImportStyles(ctx, sceneData) + if err != nil { + return nil, err + } + + _, err = usecases(ctx).StoryTelling.ImportStory(ctx, sceneData) + if err != nil { + return nil, err + } + + tx.Commit() + + prj, res, plgs, err := usecases(ctx).Project.ExportProject(ctx, prj.ID(), getOperator(ctx)) + if err != nil { + return nil, err + } + res["project"] = gqlmodel.ToProject(prj) + res["plugins"] = gqlmodel.ToPlugins(plgs) + + return &gqlmodel.ImportProjectPayload{ + ProjectData: res, + }, nil + +} diff --git a/server/internal/usecase/interactor/nlslayer.go b/server/internal/usecase/interactor/nlslayer.go index 5b0d7c43b6..d0ecdbdbd6 100644 --- a/server/internal/usecase/interactor/nlslayer.go +++ b/server/internal/usecase/interactor/nlslayer.go @@ -13,6 +13,7 @@ import ( "github.com/reearth/reearth/server/pkg/nlslayer/nlslayerops" "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearth/server/pkg/property" + "github.com/reearth/reearth/server/pkg/scene/builder" "github.com/reearth/reearthx/account/accountusecase/accountrepo" "github.com/reearth/reearthx/rerror" "github.com/reearth/reearthx/usecasex" @@ -830,3 +831,47 @@ func (i *NLSLayer) DeleteGeoJSONFeature(ctx context.Context, inp interfaces.Dele tx.Commit() return inp.FeatureID, nil } + +func (i *NLSLayer) ImportNLSLayers(ctx context.Context, sceneData map[string]interface{}) (nlslayer.NLSLayerList, error) { + sceneJSON, err := builder.ParseSceneJSON(ctx, sceneData) + if err != nil { + return nil, err + } + sceneID, err := id.SceneIDFrom(sceneJSON.ID) + if err != nil { + return nil, err + } + + nlayers := []nlslayer.NLSLayer{} + for _, nlsLayerJSON := range sceneJSON.NLSLayers { + nlsLayerID, err := id.NLSLayerIDFrom(nlsLayerJSON.ID) + if err != nil { + return nil, err + } + nlayer, err := nlslayer.New(). + ID(nlsLayerID). + Simple(). + Scene(sceneID). + Title(nlsLayerJSON.Title). + LayerType(nlslayer.LayerType(nlsLayerJSON.LayerType)). + Config((*nlslayer.Config)(nlsLayerJSON.Config)). + IsVisible(nlsLayerJSON.IsVisible). + IsSketch(nlsLayerJSON.IsSketch). + Build() + if err != nil { + return nil, err + } + nlayers = append(nlayers, nlayer) + } + + nlsLayerList := make(nlslayer.NLSLayerList, len(nlayers)) + for i, layer := range nlayers { + nlsLayerList[i] = &layer + } + + if err := i.nlslayerRepo.SaveAll(ctx, nlsLayerList); err != nil { + return nil, err + } + + return nlsLayerList, nil +} diff --git a/server/internal/usecase/interactor/nlslayer_test.go b/server/internal/usecase/interactor/nlslayer_test.go index ba7bfcd4e6..f620dc214f 100644 --- a/server/internal/usecase/interactor/nlslayer_test.go +++ b/server/internal/usecase/interactor/nlslayer_test.go @@ -2,16 +2,21 @@ package interactor import ( "context" + "encoding/json" + "fmt" "testing" + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" "github.com/reearth/reearth/server/internal/infrastructure/memory" "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/internal/usecase/interfaces" "github.com/reearth/reearth/server/pkg/id" "github.com/reearth/reearth/server/pkg/nlslayer" + "github.com/reearth/reearth/server/pkg/policy" "github.com/reearth/reearth/server/pkg/project" "github.com/reearth/reearth/server/pkg/scene" "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/workspace" "github.com/stretchr/testify/assert" ) @@ -255,3 +260,84 @@ func TestDeleteGeoJSONFeature(t *testing.T) { assert.NotNil(t, featureCollection) assert.Equal(t, 0, len(featureCollection.Features())) } + +func TestImportNLSLayers(t *testing.T) { + ctx := context.Background() + + db := memory.New() + ifl := NewNLSLayer(db) + + ws := workspace.New().NewID().Policy(policy.ID("policy").Ref()).MustBuild() + prj, _ := project.New().NewID().Workspace(ws.ID()).Build() + _ = db.Project.Save(ctx, prj) + scene, _ := scene.New().NewID().Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).RootLayer(id.NewLayerID()).Build() + _ = db.Scene.Save(ctx, scene) + + var sceneData map[string]interface{} + err := json.Unmarshal([]byte(fmt.Sprintf(`{ + "schemaVersion": 1, + "id": "%s", + "nlsLayers": [ + { + "id": "01j7g9gwj6qbv286pcwwmwq5ds", + "title": "japan_architecture (2).csv", + "layerType": "simple", + "config": { + "data": { + "csv": { + "latColumn": "lat", + "lngColumn": "lng" + }, + "type": "csv", + "url": "http://localhost:8080/assets/01j7g9gpba44e0nxwc727nax0q.csv" + } + }, + "isVisible": true, + "isSketch": false + } + ] + }`, scene.ID())), &sceneData) + assert.NoError(t, err) + + // invoke the target function + result, err := ifl.ImportNLSLayers(ctx, sceneData) + assert.NoError(t, err) + assert.NotNil(t, result) + + // actual + temp := gqlmodel.ToNLSLayers(result, nil) + resultJSON, err := json.Marshal(temp) + assert.NoError(t, err) + actual := string(resultJSON) + + // expected + var expectedMap []map[string]interface{} + err = json.Unmarshal([]byte(fmt.Sprintf(`[ + { + "id": "01j7g9gwj6qbv286pcwwmwq5ds", + "layerType": "simple", + "sceneId": "%s", + "config": { + "data": { + "csv": { + "latColumn": "lat", + "lngColumn": "lng" + }, + "type": "csv", + "url": "http://localhost:8080/assets/01j7g9gpba44e0nxwc727nax0q.csv" + } + }, + "title": "japan_architecture (2).csv", + "visible": true, + "isSketch": false + } +]`, scene.ID())), &expectedMap) + assert.NoError(t, err) + expectedJSON, err := json.Marshal(expectedMap) + assert.NoError(t, err) + expected := string(expectedJSON) + + // comparison check + assert.JSONEq(t, expected, actual) + +} diff --git a/server/internal/usecase/interactor/plugin.go b/server/internal/usecase/interactor/plugin.go index 14027eb695..d5cb74255a 100644 --- a/server/internal/usecase/interactor/plugin.go +++ b/server/internal/usecase/interactor/plugin.go @@ -3,10 +3,13 @@ package interactor import ( "context" + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + jsonmodel "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/internal/usecase/gateway" "github.com/reearth/reearth/server/internal/usecase/interfaces" "github.com/reearth/reearth/server/internal/usecase/repo" + "github.com/reearth/reearth/server/pkg/i18n" "github.com/reearth/reearth/server/pkg/id" "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearthx/usecasex" @@ -49,3 +52,53 @@ func (i *Plugin) pluginCommon() *pluginCommon { func (i *Plugin) Fetch(ctx context.Context, ids []id.PluginID, operator *usecase.Operator) ([]*plugin.Plugin, error) { return i.pluginRepo.FindByIDs(ctx, ids) } + +func (i *Plugin) ImportPlugins(ctx context.Context, pluginsData []interface{}) ([]*plugin.Plugin, error) { + var pluginsJSON = jsonmodel.ToPluginsFromJSON(pluginsData) + + importedPlugins := []*plugin.Plugin{} + for _, pluginJSON := range pluginsJSON { + pid, err := jsonmodel.ToPluginID(pluginJSON.ID) + if err != nil { + return nil, err + } + var extensions []*plugin.Extension + for _, pluginJSONextension := range pluginJSON.Extensions { + psid, err := jsonmodel.ToPropertySchemaID(pluginJSONextension.PropertySchemaID) + if err != nil { + return nil, err + } + extension, err := plugin.NewExtension(). + ID(id.PluginExtensionID(pluginJSONextension.ExtensionID)). + Type(gqlmodel.FromPluginExtension(pluginJSONextension.Type)). + Name(i18n.StringFrom(pluginJSONextension.Name)). + Description(i18n.StringFrom(pluginJSONextension.Description)). + Icon(pluginJSONextension.Icon). + SingleOnly(*pluginJSONextension.SingleOnly). + Schema(psid). + Build() + if err != nil { + return nil, err + } + extensions = append(extensions, extension) + } + p, err := plugin.New(). + ID(pid). + Name(i18n.StringFrom(pluginJSON.Name)). + Description(i18n.StringFrom(pluginJSON.Description)). + Author(pluginJSON.Author). + RepositoryURL(pluginJSON.RepositoryURL). + Extensions(extensions). + Build() + if err != nil { + return nil, err + } + if !p.ID().System() { + if err := i.pluginRepo.Save(ctx, p); err != nil { + return nil, err + } + importedPlugins = append(importedPlugins, p) + } + } + return importedPlugins, nil +} diff --git a/server/internal/usecase/interactor/plugin_test.go b/server/internal/usecase/interactor/plugin_test.go new file mode 100644 index 0000000000..ad94e56afb --- /dev/null +++ b/server/internal/usecase/interactor/plugin_test.go @@ -0,0 +1,175 @@ +package interactor + +import ( + "context" + "encoding/json" + "testing" + + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + "github.com/reearth/reearth/server/internal/infrastructure/fs" + "github.com/reearth/reearth/server/internal/infrastructure/memory" + "github.com/reearth/reearth/server/internal/usecase/gateway" + "github.com/samber/lo" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestImportPlugins(t *testing.T) { + ctx := context.Background() + + db := memory.New() + is := NewPlugin(db, &gateway.Container{ + File: lo.Must(fs.NewFile(afero.NewMemMapFs(), "https://example.com")), + }) + + var pluginsData []interface{} + err := json.Unmarshal([]byte(`[ + { + "id": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "sceneId": "01j7g9ddv4sbf8tgt5c6xxj5xc", + "name": "Plugin Communication Demo for Beta", + "version": "1.0.0", + "description": "", + "author": "", + "repositoryUrl": "", + "extensions": [ + { + "extensionId": "widgetcomm", + "pluginId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "type": "WIDGET", + "name": "Communication Demo Widget", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0/widgetcomm", + "allTranslatedName": { + "en": "Communication Demo Widget" + }, + "translatedName": "", + "translatedDescription": "" + }, + { + "extensionId": "storyblockcomm", + "pluginId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "type": "StoryBlock", + "name": "Communication Demo Story Block", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0/storyblockcomm", + "allTranslatedName": { + "en": "Communication Demo Story Block" + }, + "translatedName": "", + "translatedDescription": "" + }, + { + "extensionId": "infoboxblockcomm", + "pluginId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "type": "InfoboxBlock", + "name": "Communication Demo Infobox Block", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0/infoboxblockcomm", + "allTranslatedName": { + "en": "Communication Demo Infobox Block" + }, + "translatedName": "", + "translatedDescription": "" + } + ], + "allTranslatedName": { + "en": "Plugin Communication Demo for Beta" + }, + "translatedName": "", + "translatedDescription": "" + } + ]`), &pluginsData) + assert.NoError(t, err) + + // invoke the target function + result, err := is.ImportPlugins(ctx, pluginsData) + assert.NoError(t, err) + assert.NotNil(t, result) + + // actual + temp := gqlmodel.ToPlugins(result) + resultJSON, err := json.Marshal(temp) + assert.NoError(t, err) + actual := string(resultJSON) + + // expected + var expectedMap []map[string]interface{} + err = json.Unmarshal([]byte(`[ + { + "id": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "sceneId": "01j7g9ddv4sbf8tgt5c6xxj5xc", + "name": "Plugin Communication Demo for Beta", + "version": "1.0.0", + "description": "", + "author": "", + "repositoryUrl": "", + "extensions": [ + { + "extensionId": "widgetcomm", + "pluginId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "type": "WIDGET", + "name": "Communication Demo Widget", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0/widgetcomm", + "allTranslatedName": { + "en": "Communication Demo Widget" + }, + "translatedName": "", + "translatedDescription": "" + }, + { + "extensionId": "storyblockcomm", + "pluginId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "type": "StoryBlock", + "name": "Communication Demo Story Block", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0/storyblockcomm", + "allTranslatedName": { + "en": "Communication Demo Story Block" + }, + "translatedName": "", + "translatedDescription": "" + }, + { + "extensionId": "infoboxblockcomm", + "pluginId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0", + "type": "InfoboxBlock", + "name": "Communication Demo Infobox Block", + "description": "", + "icon": "", + "singleOnly": false, + "propertySchemaId": "01j7g9ddv4sbf8tgt5c6xxj5xc~reearth-plugin-communication-demo-beta~1.0.0/infoboxblockcomm", + "allTranslatedName": { + "en": "Communication Demo Infobox Block" + }, + "translatedName": "", + "translatedDescription": "" + } + ], + "allTranslatedName": { + "en": "Plugin Communication Demo for Beta" + }, + "translatedName": "", + "translatedDescription": "" + } + ]`), &expectedMap) + assert.NoError(t, err) + expectedJSON, err := json.Marshal(expectedMap) + assert.NoError(t, err) + expected := string(expectedJSON) + + // comparison check + assert.JSONEq(t, expected, actual) + +} diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go index d42c60a4cf..5f2aae2dd3 100644 --- a/server/internal/usecase/interactor/project.go +++ b/server/internal/usecase/interactor/project.go @@ -6,14 +6,18 @@ import ( "io" "time" + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + jsonmodel "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/internal/usecase/gateway" "github.com/reearth/reearth/server/internal/usecase/interfaces" "github.com/reearth/reearth/server/internal/usecase/repo" "github.com/reearth/reearth/server/pkg/id" + "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearth/server/pkg/project" "github.com/reearth/reearth/server/pkg/scene" "github.com/reearth/reearth/server/pkg/scene/builder" + "github.com/reearth/reearth/server/pkg/visualizer" "github.com/reearth/reearthx/account/accountdomain" "github.com/reearth/reearthx/account/accountusecase/accountrepo" "github.com/reearth/reearthx/rerror" @@ -25,6 +29,7 @@ type Project struct { commonSceneLock assetRepo repo.Asset projectRepo repo.Project + storytellingRepo repo.Storytelling userRepo accountrepo.User workspaceRepo accountrepo.Workspace sceneRepo repo.Scene @@ -38,6 +43,7 @@ type Project struct { file gateway.File nlsLayerRepo repo.NLSLayer layerStyles repo.Style + pluginRepo repo.Plugin } func NewProject(r *repo.Container, gr *gateway.Container) interfaces.Project { @@ -45,6 +51,7 @@ func NewProject(r *repo.Container, gr *gateway.Container) interfaces.Project { commonSceneLock: commonSceneLock{sceneLockRepo: r.SceneLock}, assetRepo: r.Asset, projectRepo: r.Project, + storytellingRepo: r.Storytelling, userRepo: r.User, workspaceRepo: r.Workspace, sceneRepo: r.Scene, @@ -58,6 +65,7 @@ func NewProject(r *repo.Container, gr *gateway.Container) interfaces.Project { file: gr.File, nlsLayerRepo: r.NLSLayer, layerStyles: r.Style, + pluginRepo: r.Plugin, } } @@ -415,6 +423,7 @@ func (i *Project) Publish(ctx context.Context, params interfaces.PublishProjectP repo.TagLoaderFrom(i.tagRepo), repo.TagSceneLoaderFrom(i.tagRepo, scenes), repo.NLSLayerLoaderFrom(i.nlsLayerRepo), + false, ).ForScene(s).WithNLSLayers(&nlsLayers).WithLayerStyle(layerStyles).Build(ctx, w, time.Now(), coreSupport, enableGa, trackingId) }() @@ -484,6 +493,127 @@ func (i *Project) Delete(ctx context.Context, projectID id.ProjectID, operator * return nil } +func (i *Project) ExportProject(ctx context.Context, projectID id.ProjectID, operator *usecase.Operator) (*project.Project, map[string]interface{}, []*plugin.Plugin, error) { + + prj, err := i.projectRepo.FindByID(ctx, projectID) + if err != nil { + return nil, nil, nil, err + } + sce, err := i.sceneRepo.FindByProject(ctx, prj.ID()) + if err != nil { + return nil, nil, nil, err + } + + pluginIDs := sce.PluginIds() + var filteredPluginIDs []id.PluginID + for _, pid := range pluginIDs { + if pid.String() != "reearth" { + filteredPluginIDs = append(filteredPluginIDs, pid) + } + } + plgs, err := i.pluginRepo.FindByIDs(ctx, filteredPluginIDs) + if err != nil { + return nil, nil, nil, err + } + + sceneID := sce.ID() + nlsLayers, err := i.nlsLayerRepo.FindByScene(ctx, sceneID) + if err != nil { + return nil, nil, nil, err + } + layerStyles, err := i.layerStyles.FindByScene(ctx, sceneID) + if err != nil { + return nil, nil, nil, err + } + storyList, err := i.storytellingRepo.FindByScene(ctx, sceneID) + if err != nil { + return nil, nil, nil, err + } + sceneJSON, err := builder.New( + repo.LayerLoaderFrom(i.layerRepo), + repo.PropertyLoaderFrom(i.propertyRepo), + repo.DatasetGraphLoaderFrom(i.datasetRepo), + repo.TagLoaderFrom(i.tagRepo), + repo.TagSceneLoaderFrom(i.tagRepo, []id.SceneID{sceneID}), + repo.NLSLayerLoaderFrom(i.nlsLayerRepo), + true, + ).ForScene(sce).WithNLSLayers(&nlsLayers).WithLayerStyle(layerStyles).WithStory((*storyList)[0]).BuildResult( + ctx, + time.Now(), + prj.CoreSupport(), + prj.EnableGA(), + prj.TrackingID(), + ) + if err != nil { + return nil, nil, nil, err + } + + res := make(map[string]interface{}) + res["scene"] = sceneJSON + return prj, res, plgs, nil +} + +func (i *Project) ImportProject(ctx context.Context, projectData map[string]interface{}) (*project.Project, usecasex.Tx, error) { + + tx, err := i.transaction.Begin(ctx) + if err != nil { + return nil, nil, err + } + + var p = jsonmodel.ToProjectFromJSON(projectData) + + projectID, err := id.ProjectIDFrom(string(p.ID)) + if err != nil { + return nil, nil, err + } + workspaceID, err := accountdomain.WorkspaceIDFrom(string(p.TeamID)) + if err != nil { + return nil, nil, err + } + + prjBuilder := project.New(). + ID(projectID). + Workspace(workspaceID). + IsArchived(p.IsArchived). + IsBasicAuthActive(p.IsBasicAuthActive). + BasicAuthUsername(p.BasicAuthUsername). + BasicAuthPassword(p.BasicAuthPassword). + Name(p.Name). + Description(p.Description). + Alias(p.Alias). + PublicTitle(p.PublicTitle). + PublicDescription(p.PublicDescription). + PublicImage(p.PublicImage). + PublicNoIndex(p.PublicNoIndex). + CoreSupport(p.CoreSupport). + EnableGA(p.EnableGa). + TrackingID(p.TrackingID). + Starred(p.Starred) + + if !p.CreatedAt.IsZero() { + prjBuilder = prjBuilder.UpdatedAt(p.CreatedAt) + } + if p.PublishedAt != nil { + prjBuilder = prjBuilder.PublishedAt(*p.PublishedAt) + } + + if p.ImageURL != nil { + prjBuilder = prjBuilder.ImageURL(p.ImageURL) + } + + prjBuilder = prjBuilder.Visualizer(visualizer.Visualizer(p.Visualizer)) + prjBuilder = prjBuilder.PublishmentStatus(gqlmodel.FromPublishmentStatus(p.PublishmentStatus)) + + prj, err := prjBuilder.Build() + if err != nil { + return nil, nil, err + } + if err := i.projectRepo.Save(ctx, prj); err != nil { + return nil, nil, err + } + return prj, tx, nil +} + func updateProjectUpdatedAt(ctx context.Context, prj *project.Project, r repo.Project) error { currentTime := time.Now().UTC() prj.SetUpdatedAt(currentTime) diff --git a/server/internal/usecase/interactor/project_test.go b/server/internal/usecase/interactor/project_test.go index 964ccdca66..29a5f3bf4b 100644 --- a/server/internal/usecase/interactor/project_test.go +++ b/server/internal/usecase/interactor/project_test.go @@ -2,11 +2,15 @@ package interactor import ( "context" + "encoding/json" "net/url" "testing" + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + "github.com/reearth/reearth/server/internal/infrastructure/fs" "github.com/reearth/reearth/server/internal/infrastructure/memory" "github.com/reearth/reearth/server/internal/usecase" + "github.com/reearth/reearth/server/internal/usecase/gateway" "github.com/reearth/reearth/server/internal/usecase/interfaces" "github.com/reearth/reearth/server/pkg/policy" "github.com/reearth/reearth/server/pkg/project" @@ -17,6 +21,7 @@ import ( "github.com/reearth/reearthx/rerror" "github.com/reearth/reearthx/usecasex" "github.com/samber/lo" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" ) @@ -137,3 +142,119 @@ func TestProject_Create(t *testing.T) { assert.Same(t, policy.ErrPolicyViolation, err) assert.Nil(t, got) } + +func TestImportProject(t *testing.T) { + + ctx := context.Background() + + db := memory.New() + ifp := NewProject(db, &gateway.Container{ + File: lo.Must(fs.NewFile(afero.NewMemMapFs(), "https://example.com")), + }) + + var projectData map[string]interface{} + err := json.Unmarshal([]byte(`{ + "id": "01j7g9ddttkpnt3esk8h4w7xhv", + "isArchived": false, + "isBasicAuthActive": false, + "basicAuthUsername": "", + "basicAuthPassword": "", + "createdAt": "2024-09-11T19:17:39.418+09:00", + "updatedAt": "2024-09-11T10:22:09.581Z", + "name": "ProjectName1", + "description": "ProjectOverview1", + "alias": "", + "publicTitle": "", + "publicDescription": "", + "publicImage": "", + "publicNoIndex": false, + "imageUrl": { + "Scheme": "http", + "Opaque": "", + "User": null, + "Host": "localhost:8080", + "Path": "/assets/01j7g9d988ct8hajjxfsb6e1n6.jpeg", + "RawPath": "", + "OmitHost": false, + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "teamId": "01j7g99pb1q1vf684af39bajw5", + "visualizer": "cesium", + "publishmentStatus": "PRIVATE", + "coreSupport": true, + "enableGa": false, + "trackingId": "", + "starred": false + }`), &projectData) + assert.NoError(t, err) + + // invoke the target function + result, _, err := ifp.ImportProject(ctx, projectData) + assert.NoError(t, err) + assert.NotNil(t, result) + + // actual + temp := gqlmodel.ToProject(result) + resultByte, err := json.Marshal(temp) + assert.NoError(t, err) + var resultMap map[string]interface{} + err = json.Unmarshal(resultByte, &resultMap) + assert.NoError(t, err) + + // Exclude items that are updated upon creation. + delete(resultMap, "updatedAt") + delete(resultMap, "createdAt") + + actualByte, err := json.Marshal(resultMap) + assert.NoError(t, err) + actual := string(actualByte) + + // expected + var expectedMap map[string]interface{} + err = json.Unmarshal([]byte(`{ + "id": "01j7g9ddttkpnt3esk8h4w7xhv", + "isArchived": false, + "isBasicAuthActive": false, + "basicAuthUsername": "", + "basicAuthPassword": "", + "name": "ProjectName1", + "description": "ProjectOverview1", + "alias": "", + "publicTitle": "", + "publicDescription": "", + "publicImage": "", + "publicNoIndex": false, + "imageUrl": { + "Scheme": "http", + "Opaque": "", + "User": null, + "Host": "localhost:8080", + "Path": "/assets/01j7g9d988ct8hajjxfsb6e1n6.jpeg", + "RawPath": "", + "OmitHost": false, + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "teamId": "01j7g99pb1q1vf684af39bajw5", + "visualizer": "cesium", + "publishmentStatus": "PRIVATE", + "coreSupport": true, + "enableGa": false, + "trackingId": "", + "starred": false + }`), &expectedMap) + + assert.NoError(t, err) + expectedJSON, err := json.Marshal(expectedMap) + assert.NoError(t, err) + expected := string(expectedJSON) + + // comparison check + assert.JSONEq(t, expected, actual) + +} diff --git a/server/internal/usecase/interactor/scene.go b/server/internal/usecase/interactor/scene.go index 9730d305b6..b453784fbd 100644 --- a/server/internal/usecase/interactor/scene.go +++ b/server/internal/usecase/interactor/scene.go @@ -3,6 +3,7 @@ package interactor import ( "context" "errors" + "time" "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/internal/usecase/gateway" @@ -12,8 +13,10 @@ import ( "github.com/reearth/reearth/server/pkg/id" "github.com/reearth/reearth/server/pkg/layer" "github.com/reearth/reearth/server/pkg/plugin" + "github.com/reearth/reearth/server/pkg/project" "github.com/reearth/reearth/server/pkg/property" "github.com/reearth/reearth/server/pkg/scene" + "github.com/reearth/reearth/server/pkg/scene/builder" "github.com/reearth/reearth/server/pkg/visualizer" "github.com/reearth/reearthx/rerror" "github.com/reearth/reearthx/usecasex" @@ -192,23 +195,11 @@ func (i *Scene) AddWidget(ctx context.Context, sid id.SceneID, pid id.PluginID, return nil, nil, err } - pr, err := i.pluginRepo.FindByID(ctx, pid) + extension, err := i.getWidgePlugin(ctx, pid, eid) if err != nil { - if errors.Is(err, rerror.ErrNotFound) { - return nil, nil, interfaces.ErrPluginNotFound - } return nil, nil, err } - extension := pr.Extension(eid) - if extension == nil { - return nil, nil, interfaces.ErrExtensionNotFound - } - - if extension.Type() != plugin.ExtensionTypeWidget { - return nil, nil, interfaces.ErrExtensionTypeMustBeWidget - } - property, err := property.New().NewID().Schema(extension.Schema()).Scene(sid).Build() if err != nil { return nil, nil, err @@ -301,23 +292,11 @@ func (i *Scene) UpdateWidget(ctx context.Context, param interfaces.UpdateWidgetP } _, location := scene.Widgets().Alignment().Find(param.WidgetID) - pr, err := i.pluginRepo.FindByID(ctx, widget.Plugin()) + extension, err := i.getWidgePlugin(ctx, widget.Plugin(), widget.Extension()) if err != nil { - if errors.Is(err, rerror.ErrNotFound) { - return nil, nil, interfaces.ErrPluginNotFound - } return nil, nil, err } - extension := pr.Extension(widget.Extension()) - if extension == nil { - return nil, nil, interfaces.ErrExtensionNotFound - } - - if extension.Type() != plugin.ExtensionTypeWidget { - return nil, nil, interfaces.ErrExtensionTypeMustBeWidget - } - if param.Enabled != nil { widget.SetEnabled(*param.Enabled) } @@ -602,8 +581,137 @@ func (i *Scene) RemoveCluster(ctx context.Context, sceneID id.SceneID, clusterID return s, nil } +func (i *Scene) ImportScene(ctx context.Context, prj *project.Project, sceneData map[string]interface{}) (*scene.Scene, error) { + sceneJSON, err := builder.ParseSceneJSON(ctx, sceneData) + if err != nil { + return nil, err + } + sceneID, err := id.SceneIDFrom(sceneJSON.ID) + if err != nil { + return nil, err + } + widgets := []*scene.Widget{} + for _, widgetJSON := range sceneJSON.Widgets { + widgetID, err := id.WidgetIDFrom(widgetJSON.ID) + if err != nil { + return nil, err + } + pluginID, err := id.PluginIDFrom(widgetJSON.PluginID) + if err != nil { + return nil, err + } + extensionID := id.PluginExtensionID(widgetJSON.ExtensionID) + extension, err := i.getWidgePlugin(ctx, pluginID, extensionID) + if err != nil { + return nil, err + } + prop, err := property.New().NewID().Schema(extension.Schema()).Scene(sceneID).Build() + if err != nil { + return nil, err + } + ps, err := i.propertySchemaRepo.FindByID(ctx, extension.Schema()) + if err != nil { + return nil, err + } + prop, err = builder.AddItemFromPropertyJSON(prop, ps, widgetJSON.Property) + if err != nil { + return nil, err + } + // Save property + if err = i.propertyRepo.Save(ctx, prop); err != nil { + return nil, err + } + widget, err := scene.NewWidget(widgetID, pluginID, extensionID, prop.ID(), widgetJSON.Enabled, widgetJSON.Extended) + if err != nil { + return nil, err + } + widgets = append(widgets, widget) + } + clusters := []*scene.Cluster{} + for _, clusterJson := range sceneJSON.Clusters { + clusterID, err := id.ClusterIDFrom(clusterJson.ID) + if err != nil { + return nil, err + } + property, err := property.New().NewID().Schema(id.MustPropertySchemaID("reearth/cluster")).Scene(sceneID).Build() + if err != nil { + return nil, err + } + if err = i.propertyRepo.Save(ctx, property); err != nil { + return nil, err + } + cluster, err := scene.NewCluster(clusterID, clusterJson.Name, property.ID()) + if err != nil { + return nil, err + } + clusters = append(clusters, cluster) + } + clusterList := scene.NewClusterListFrom(clusters) + var viz = visualizer.VisualizerCesium + if prj.CoreSupport() { + viz = visualizer.VisualizerCesiumBeta + } + schema := builtin.GetPropertySchemaByVisualizer(viz) + prop, err := property.New().NewID().Schema(schema.ID()).Scene(sceneID).Build() + if err != nil { + return nil, err + } + tiles := id.PropertySchemaGroupID("tiles") + g := prop.GetOrCreateGroupList(schema, property.PointItemBySchema(tiles)) + g.Add(property.NewGroup().NewID().SchemaGroup(tiles).MustBuild(), -1) + rootLayer, err := layer.NewGroup().NewID().Scene(sceneID).Root(true).Build() + if err != nil { + return nil, err + } + if err = i.propertyRepo.Filtered(repo.SceneFilter{Writable: scene.IDList{sceneID}}).Save(ctx, prop); err != nil { + return nil, err + } + if err = i.layerRepo.Filtered(repo.SceneFilter{Writable: scene.IDList{sceneID}}).Save(ctx, rootLayer); err != nil { + return nil, err + } + scene, err := scene.New(). + ID(sceneID). + Project(prj.ID()). + Workspace(prj.Workspace()). + RootLayer(rootLayer.ID()). + Widgets(scene.NewWidgets(widgets, builder.ParserWidgetAlignSystem(sceneJSON.WidgetAlignSystem))). + UpdatedAt(time.Now()). + Property(prop.ID()). + Clusters(clusterList). + Build() + if err != nil { + return nil, err + } + if err := i.sceneRepo.Save(ctx, scene); err != nil { + return nil, err + } + if err := updateProjectUpdatedAt(ctx, prj, i.projectRepo); err != nil { + return nil, err + } + // operator.AddNewScene(prj.Workspace(), sceneID) + return scene, nil +} + func injectExtensionsToScene(s *scene.Scene, ext []plugin.ID) { lo.ForEach(ext, func(p plugin.ID, _ int) { s.Plugins().Add(scene.NewPlugin(p, nil)) }) } + +func (i *Scene) getWidgePlugin(ctx context.Context, pid id.PluginID, eid id.PluginExtensionID) (*plugin.Extension, error) { + pr, err := i.pluginRepo.FindByID(ctx, pid) + if err != nil { + if errors.Is(err, rerror.ErrNotFound) { + return nil, interfaces.ErrPluginNotFound + } + return nil, err + } + extension := pr.Extension(eid) + if extension == nil { + return nil, interfaces.ErrExtensionNotFound + } + if extension.Type() != plugin.ExtensionTypeWidget { + return nil, interfaces.ErrExtensionTypeMustBeWidget + } + return extension, nil +} diff --git a/server/internal/usecase/interactor/scene_test.go b/server/internal/usecase/interactor/scene_test.go new file mode 100644 index 0000000000..7bb8fee4aa --- /dev/null +++ b/server/internal/usecase/interactor/scene_test.go @@ -0,0 +1,335 @@ +package interactor + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + "github.com/reearth/reearth/server/internal/infrastructure/fs" + "github.com/reearth/reearth/server/internal/infrastructure/memory" + "github.com/reearth/reearth/server/internal/usecase/gateway" + "github.com/reearth/reearth/server/pkg/policy" + "github.com/reearth/reearth/server/pkg/project" + "github.com/reearth/reearthx/account/accountdomain/workspace" + "github.com/samber/lo" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestImportScene(t *testing.T) { + ctx := context.Background() + + db := memory.New() + ifs := NewScene(db, &gateway.Container{ + File: lo.Must(fs.NewFile(afero.NewMemMapFs(), "https://example.com")), + PluginRegistry: &mockPluginRegistry{}, + }) + + ws := workspace.New().NewID().Policy(policy.ID("policy").Ref()).MustBuild() + prj, _ := project.New().NewID().Workspace(ws.ID()).Build() + _ = db.Project.Save(ctx, prj) + + var sceneData map[string]interface{} + err := json.Unmarshal([]byte(`{ + "schemaVersion": 1, + "id": "01j7g9ddv4sbf8tgt5c6xxj5xc", + "publishedAt": "2024-09-11T19:23:44.223046+09:00", + "property": { + "tiles": [ + { + "id": "01j7g9ddv4sbf8tgt5cbjxrksh" + } + ] + }, + "plugins": {}, + "layers": null, + "widgets": [ + { + "id": "01j7g9h4f1k93vspn3gdtz67az", + "pluginId": "reearth", + "extensionId": "button", + "property": { + "default": { + "buttonBgcolor": { + "type": "string", + "value": "#79b4beff" + }, + "buttonColor": { + "type": "string", + "value": "#171289ff" + }, + "buttonTitle": { + "type": "string", + "value": "TestButton1" + } + } + }, + "enabled": false, + "extended": false + }, + { + "id": "01j7g9jckefd0zxyy34bbygmhy", + "pluginId": "reearth", + "extensionId": "navigator", + "property": { + "default": { + "visible": { + "type": "string", + "value": "desktop" + } + } + }, + "enabled": false, + "extended": false + } + ], + "widgetAlignSystem": { + "inner": null, + "outer": { + "left": { + "top": { + "widgetIds": [ + "01j7g9h4f1k93vspn3gdtz67az", + "01j7g9jr89rjq1egrb1hhcd8jy" + ], + "align": "start", + "padding": { + "top": 0, + "bottom": 0, + "left": 0, + "right": 0 + }, + "gap": null, + "centered": false, + "background": null + }, + "middle": null, + "bottom": null + }, + "center": null, + "right": { + "top": { + "widgetIds": [ + "01j7g9jckefd0zxyy34bbygmhy" + ], + "align": "start", + "padding": { + "top": 0, + "bottom": 0, + "left": 0, + "right": 0 + }, + "gap": null, + "centered": false, + "background": null + }, + "middle": null, + "bottom": null + } + } + }, + "tags": [], + "clusters": [], + "layerStyles": null, + "coreSupport": true, + "enableGa": false, + "trackingId": "" + }`), &sceneData) + assert.NoError(t, err) + + // invoke the target function + result, err := ifs.ImportScene(ctx, prj, sceneData) + assert.NoError(t, err) + assert.NotNil(t, result) + + // actual + temp := gqlmodel.ToScene(result) + resultJSON, err := json.Marshal(temp) + assert.NoError(t, err) + + // Exclude items that are updated upon creation. + var resultMap map[string]interface{} + err = json.Unmarshal(resultJSON, &resultMap) + assert.NoError(t, err) + delete(resultMap, "rootLayerId") + delete(resultMap, "propertyId") + delete(resultMap, "updatedAt") + delete(resultMap, "createdAt") + if widgets, ok := resultMap["widgets"].([]interface{}); ok { + for _, widget := range widgets { + if widgetMap, ok := widget.(map[string]interface{}); ok { + delete(widgetMap, "propertyId") + } + } + } + + resultJSON, err = json.Marshal(resultMap) + assert.NoError(t, err) + actual := string(resultJSON) + + // expected + var expectedMap map[string]interface{} + err = json.Unmarshal([]byte(fmt.Sprintf(`{ + "clusters": [], + "datasetSchemas": null, + "id": "01j7g9ddv4sbf8tgt5c6xxj5xc", + "newLayers": null, + "plugins": [], + "projectId": "%s", + "stories": null, + "styles": null, + "tagIds": null, + "tags": null, + "teamId": "%s", + "widgetAlignSystem": { + "inner": { + "center": { + "bottom": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "middle": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "top": { + "align": "START", + "centered": false, + "widgetIds": [] + } + }, + "left": { + "bottom": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "middle": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "top": { + "align": "START", + "centered": false, + "widgetIds": [] + } + }, + "right": { + "bottom": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "middle": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "top": { + "align": "START", + "centered": false, + "widgetIds": [] + } + } + }, + "outer": { + "center": { + "bottom": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "middle": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "top": { + "align": "START", + "centered": false, + "widgetIds": [] + } + }, + "left": { + "bottom": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "middle": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "top": { + "align": "START", + "centered": false, + "padding": { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0 + }, + "widgetIds": [ + "01j7g9h4f1k93vspn3gdtz67az", + "01j7g9jr89rjq1egrb1hhcd8jy" + ] + } + }, + "right": { + "bottom": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "middle": { + "align": "START", + "centered": false, + "widgetIds": [] + }, + "top": { + "align": "START", + "centered": false, + "padding": { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0 + }, + "widgetIds": [ + "01j7g9jckefd0zxyy34bbygmhy" + ] + } + } + } + }, + "widgets": [ + { + "enabled": false, + "extended": false, + "extensionId": "button", + "id": "01j7g9h4f1k93vspn3gdtz67az", + "pluginId": "reearth" + }, + { + "enabled": false, + "extended": false, + "extensionId": "navigator", + "id": "01j7g9jckefd0zxyy34bbygmhy", + "pluginId": "reearth" + } + ] +}`, prj.ID(), prj.Workspace())), &expectedMap) + assert.NoError(t, err) + expectedJSON, err := json.Marshal(expectedMap) + assert.NoError(t, err) + expected := string(expectedJSON) + + // Comparison check + assert.JSONEq(t, expected, actual) +} diff --git a/server/internal/usecase/interactor/storytelling.go b/server/internal/usecase/interactor/storytelling.go index 4424644b0f..2ac541f9f4 100644 --- a/server/internal/usecase/interactor/storytelling.go +++ b/server/internal/usecase/interactor/storytelling.go @@ -40,6 +40,8 @@ type Storytelling struct { transaction usecasex.Transaction nlsLayerRepo repo.NLSLayer layerStyles repo.Style + + propertySchemaRepo repo.PropertySchema } func NewStorytelling(r *repo.Container, gr *gateway.Container) interfaces.Storytelling { @@ -59,6 +61,8 @@ func NewStorytelling(r *repo.Container, gr *gateway.Container) interfaces.Storyt transaction: r.Transaction, nlsLayerRepo: r.NLSLayer, layerStyles: r.Style, + + propertySchemaRepo: r.PropertySchema, } } @@ -374,6 +378,7 @@ func (i *Storytelling) Publish(ctx context.Context, inp interfaces.PublishStoryI repo.TagLoaderFrom(i.tagRepo), repo.TagSceneLoaderFrom(i.tagRepo, scenes), repo.NLSLayerLoaderFrom(i.nlsLayerRepo), + false, ).ForScene(scene).WithNLSLayers(&nlsLayers).WithLayerStyle(layerStyles).WithStory(story).Build(ctx, w, time.Now(), true, false, "") }() @@ -992,6 +997,137 @@ func (i *Storytelling) MoveBlock(ctx context.Context, inp interfaces.MoveBlockPa return story, page, &inp.BlockID, inp.Index, nil } +func (i *Storytelling) ImportStory(ctx context.Context, sceneData map[string]interface{}) (*storytelling.Story, error) { + sceneJSON, err := builder.ParseSceneJSON(ctx, sceneData) + if err != nil { + return nil, err + } + sceneID, err := id.SceneIDFrom(sceneJSON.ID) + if err != nil { + return nil, err + } + storyJSON := sceneJSON.Story + + pages := []*storytelling.Page{} + for _, pageJSON := range storyJSON.Pages { + + blocks := []*storytelling.Block{} + for _, blockJSON := range pageJSON.Blocks { + blockID, err := id.BlockIDFrom(blockJSON.ID) + if err != nil { + return nil, err + } + pluginID, err := id.PluginIDFrom(blockJSON.PluginId) + if err != nil { + return nil, err + } + extensionID := id.PluginExtensionID(blockJSON.ExtensionId) + _, extension, err := i.getPlugin(ctx, &pluginID, &extensionID) + if err != nil { + return nil, err + } + if extension.Type() != plugin.ExtensionTypeStoryBlock { + return nil, interfaces.ErrExtensionTypeMustBeStoryBlock + } + prop, err := property.New().NewID().Schema(extension.Schema()).Scene(sceneID).Build() + if err != nil { + return nil, err + } + ps, err := i.propertySchemaRepo.FindByID(ctx, extension.Schema()) + if err != nil { + return nil, err + } + prop, err = builder.AddItemFromPropertyJSON(prop, ps, blockJSON.Property) + if err != nil { + return nil, err + } + // Save property + if err = i.propertyRepo.Save(ctx, prop); err != nil { + return nil, err + } + block, err := storytelling.NewBlock(). + ID(blockID). + Property(prop.ID()). + Plugin(pluginID). + Extension(extensionID). + Build() + if err != nil { + return nil, err + } + blocks = append(blocks, block) + } + + pageID, err := id.PageIDFrom(pageJSON.ID) + if err != nil { + return nil, err + } + schema := builtin.GetPropertySchema(builtin.PropertySchemaIDStoryPage) + prop, err := property.New().NewID().Schema(schema.ID()).Scene(sceneID).Build() + if err != nil { + return nil, err + } + ps, err := i.propertySchemaRepo.FindByID(ctx, schema.ID()) + if err != nil { + return nil, err + } + prop, err = builder.AddItemFromPropertyJSON(prop, ps, pageJSON.Property) + if err != nil { + return nil, err + } + // Save property + if err = i.propertyRepo.Save(ctx, prop); err != nil { + return nil, err + } + page, err := storytelling.NewPage(). + ID(pageID). + Property(prop.ID()). + Title(pageJSON.Title). + Swipeable(pageJSON.Swipeable). + Blocks(blocks). + Build() + if err != nil { + return nil, err + } + pages = append(pages, page) + } + + storyID, _ := id.StoryIDFrom(storyJSON.ID) + + schema := builtin.GetPropertySchema(builtin.PropertySchemaIDStory) + prop, err := property.New().NewID().Schema(schema.ID()).Scene(sceneID).Build() + if err != nil { + return nil, err + } + ps, err := i.propertySchemaRepo.FindByID(ctx, schema.ID()) + if err != nil { + return nil, err + } + prop, err = builder.AddItemFromPropertyJSON(prop, ps, storyJSON.Property) + if err != nil { + return nil, err + } + // Save property + if err = i.propertyRepo.Save(ctx, prop); err != nil { + return nil, err + } + story, err := storytelling.NewStory(). + ID(storyID). + Property(prop.ID()). + Scene(sceneID). + PanelPosition(storytelling.Position(storyJSON.PanelPosition)). + BgColor(storyJSON.BgColor). + Pages(storytelling.NewPageList(pages)). + Build() + if err != nil { + return nil, err + } + if err := i.storytellingRepo.Save(ctx, *story); err != nil { + return nil, err + } + + return story, nil +} + func (i *Storytelling) getPlugin(ctx context.Context, pId *id.PluginID, eId *id.PluginExtensionID) (*plugin.Plugin, *plugin.Extension, error) { if pId == nil { return nil, nil, nil diff --git a/server/internal/usecase/interactor/storytelling_test.go b/server/internal/usecase/interactor/storytelling_test.go new file mode 100644 index 0000000000..e498f0a1ed --- /dev/null +++ b/server/internal/usecase/interactor/storytelling_test.go @@ -0,0 +1,202 @@ +package interactor + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + "github.com/reearth/reearth/server/internal/infrastructure/fs" + "github.com/reearth/reearth/server/internal/infrastructure/memory" + "github.com/reearth/reearth/server/internal/usecase/gateway" + "github.com/reearth/reearth/server/pkg/id" + "github.com/reearth/reearth/server/pkg/project" + "github.com/reearth/reearth/server/pkg/scene" + "github.com/reearth/reearthx/account/accountdomain" + "github.com/samber/lo" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestImportStory(t *testing.T) { + ctx := context.Background() + + db := memory.New() + ifs := NewStorytelling(db, &gateway.Container{ + File: lo.Must(fs.NewFile(afero.NewMemMapFs(), "https://example.com")), + }) + + prj, _ := project.New().NewID().Build() + _ = db.Project.Save(ctx, prj) + scene, _ := scene.New().NewID().Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).RootLayer(id.NewLayerID()).Build() + _ = db.Scene.Save(ctx, scene) + + var sceneData map[string]interface{} + err := json.Unmarshal([]byte(fmt.Sprintf(`{ + "schemaVersion": 1, + "id": "%s", + "story": { + "id": "01j7g9ddvkarms2gmc59ysw66r", + "property": {}, + "pages": [ + { + "id": "01j7g9ddwk4a12x1t8wm865s6h", + "property": { + "title": { + "color": { + "type": "string", + "value": "#9a19bfff" + }, + "title": { + "type": "string", + "value": "Title1" + } + } + }, + "title": "Untitled", + "blocks": [ + { + "id": "01j7g9mdnjk1jafw592btqx6t7", + "property": { + "default": { + "text": { + "type": "string", + "value": "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Block1\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}" + } + } + }, + "plugins": null, + "extensionId": "textStoryBlock", + "pluginId": "reearth" + }, + { + "id": "01j7g9n3x4yqae71crdjcpeyc0", + "property": { + "default": { + "text": { + "type": "string", + "value": "## MarkDown1" + } + } + }, + "plugins": null, + "extensionId": "mdTextStoryBlock", + "pluginId": "reearth" + }, + { + "id": "01j7g9nnnap0cwa1farwd841xc", + "property": { + "default": { + "src": { + "type": "url", + "value": "http://localhost:8080/assets/01j7g9nwtq1zqc7ex5gfvd1mbe.jpeg" + } + } + }, + "plugins": null, + "extensionId": "imageStoryBlock", + "pluginId": "reearth" + } + ], + "swipeable": false, + "swipeableLayers": null, + "layers": [] + } + ], + "position": "right", + "bgColor": "#b2efd8ff" + } + }`, scene.ID())), &sceneData) + assert.NoError(t, err) + + // invoke the target function + result, err := ifs.ImportStory(ctx, sceneData) + assert.NoError(t, err) + assert.NotNil(t, result) + + // actual + temp := gqlmodel.ToStory(result) + resultByte, err := json.Marshal(temp) + assert.NoError(t, err) + var resultMap map[string]interface{} + err = json.Unmarshal(resultByte, &resultMap) + assert.NoError(t, err) + + // Exclude items that are updated upon creation. + delete(resultMap, "propertyId") + delete(resultMap, "updatedAt") + delete(resultMap, "createdAt") + if pages, ok := resultMap["pages"].([]interface{}); ok { + for _, page := range pages { + if pageMap, ok := page.(map[string]interface{}); ok { + delete(pageMap, "propertyId") + delete(pageMap, "updatedAt") + delete(pageMap, "createdAt") + delete(pageMap, "sceneId") + if blocks, ok := pageMap["blocks"].([]interface{}); ok { + for _, block := range blocks { + if blockMap, ok := block.(map[string]interface{}); ok { + delete(blockMap, "propertyId") + } + } + } + } + } + } + actualByte, err := json.Marshal(resultMap) + assert.NoError(t, err) + actual := string(actualByte) + + // expected + var expectedMap map[string]interface{} + err = json.Unmarshal([]byte(fmt.Sprintf(`{ + "id": "01j7g9ddvkarms2gmc59ysw66r", + "title": "", + "alias": "", + "pages": [ + { + "id": "01j7g9ddwk4a12x1t8wm865s6h", + "title": "Untitled", + "blocks": [ + { + "id": "01j7g9mdnjk1jafw592btqx6t7", + "pluginId": "reearth", + "extensionId": "textStoryBlock" + }, + { + "id": "01j7g9n3x4yqae71crdjcpeyc0", + "pluginId": "reearth", + "extensionId": "mdTextStoryBlock" + }, + { + "id": "01j7g9nnnap0cwa1farwd841xc", + "pluginId": "reearth", + "extensionId": "imageStoryBlock" + } + ], + "swipeable": false, + "layersIds": null, + "layers": null + } + ], + "publishmentStatus": "", + "sceneId": "%s", + "panelPosition": "RIGHT", + "bgColor": "#b2efd8ff", + "isBasicAuthActive": false, + "basicAuthUsername": "", + "basicAuthPassword": "", + "publicTitle": "", + "publicDescription": "", + "publicImage": "", + "publicNoIndex": false +}`, scene.ID())), &expectedMap) + assert.NoError(t, err) + expectedJSON, err := json.Marshal(expectedMap) + assert.NoError(t, err) + expected := string(expectedJSON) + + // comparison check + assert.JSONEq(t, expected, actual) +} diff --git a/server/internal/usecase/interactor/style.go b/server/internal/usecase/interactor/style.go index 37b515cade..51b402db9c 100644 --- a/server/internal/usecase/interactor/style.go +++ b/server/internal/usecase/interactor/style.go @@ -8,6 +8,7 @@ import ( "github.com/reearth/reearth/server/internal/usecase/repo" "github.com/reearth/reearth/server/pkg/id" "github.com/reearth/reearth/server/pkg/scene" + "github.com/reearth/reearth/server/pkg/scene/builder" "github.com/reearth/reearth/server/pkg/scene/sceneops" "github.com/reearth/reearthx/usecasex" ) @@ -198,3 +199,39 @@ func (i *Style) DuplicateStyle(ctx context.Context, styleID id.StyleID, operator tx.Commit() return duplicatedStyle, nil } + +func (i *Style) ImportStyles(ctx context.Context, sceneData map[string]interface{}) (scene.StyleList, error) { + sceneJSON, err := builder.ParseSceneJSON(ctx, sceneData) + if err != nil { + return nil, err + } + sceneID, err := id.SceneIDFrom(sceneJSON.ID) + if err != nil { + return nil, err + } + + styles := []*scene.Style{} + for _, layerStyleJson := range sceneJSON.LayerStyles { + styleID, err := id.StyleIDFrom(layerStyleJson.ID) + if err != nil { + return nil, err + } + style, err := scene.NewStyle(). + ID(styleID). + Name(layerStyleJson.Name). + Value((*scene.StyleValue)(layerStyleJson.Value)). + Scene(sceneID). + Build() + if err != nil { + return nil, err + } + styles = append(styles, style) + } + + styleList := scene.StyleList(styles) + if err := i.styleRepo.SaveAll(ctx, styleList); err != nil { + return nil, err + } + + return styleList, nil +} diff --git a/server/internal/usecase/interactor/style_test.go b/server/internal/usecase/interactor/style_test.go new file mode 100644 index 0000000000..4b1487540e --- /dev/null +++ b/server/internal/usecase/interactor/style_test.go @@ -0,0 +1,93 @@ +package interactor + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + "github.com/reearth/reearth/server/internal/infrastructure/memory" + "github.com/reearth/reearth/server/pkg/id" + "github.com/reearth/reearth/server/pkg/policy" + "github.com/reearth/reearth/server/pkg/project" + "github.com/reearth/reearth/server/pkg/scene" + "github.com/reearth/reearthx/account/accountdomain" + "github.com/reearth/reearthx/account/accountdomain/workspace" + "github.com/stretchr/testify/assert" +) + +func TestImportStyles(t *testing.T) { + ctx := context.Background() + + db := memory.New() + ifs := NewStyle(db) + + ws := workspace.New().NewID().Policy(policy.ID("policy").Ref()).MustBuild() + prj, _ := project.New().NewID().Workspace(ws.ID()).Build() + _ = db.Project.Save(ctx, prj) + scene, _ := scene.New().NewID().Workspace(accountdomain.NewWorkspaceID()).Project(prj.ID()).RootLayer(id.NewLayerID()).Build() + _ = db.Scene.Save(ctx, scene) + + var sceneData map[string]interface{} + err := json.Unmarshal([]byte(fmt.Sprintf(`{ + "schemaVersion": 1, + "id": "%s", + "layerStyles": [ + { + "id": "01j7hzqgycv76hxsygmcrb47m6", + "name": "スタイル_0", + "value": { + "color": "red" + } + }, + { + "id": "01j7hzrgc3ag8m1ftzye05csgx", + "name": "スタイル_1", + "value": { + "font": "bold" + } + } + ] + }`, scene.ID())), &sceneData) + assert.NoError(t, err) + + // invoke the target function + result, err := ifs.ImportStyles(ctx, sceneData) + assert.NoError(t, err) + assert.NotNil(t, result) + + // actual + temp := gqlmodel.ToStyles(result) + resultJSON, err := json.Marshal(temp) + assert.NoError(t, err) + actual := string(resultJSON) + + // expected + var expectedMap []map[string]interface{} + err = json.Unmarshal([]byte(fmt.Sprintf(`[ + { + "id": "01j7hzqgycv76hxsygmcrb47m6", + "sceneId": "%s", + "name": "スタイル_0", + "value": { + "color": "red" + } + }, + { + "id": "01j7hzrgc3ag8m1ftzye05csgx", + "sceneId": "%s", + "name": "スタイル_1", + "value": { + "font": "bold" + } + } + ]`, scene.ID(), scene.ID())), &expectedMap) + assert.NoError(t, err) + expectedJSON, err := json.Marshal(expectedMap) + assert.NoError(t, err) + expected := string(expectedJSON) + + // comparison check + assert.JSONEq(t, expected, actual) +} diff --git a/server/internal/usecase/interfaces/nlslayer.go b/server/internal/usecase/interfaces/nlslayer.go index 1a7700da79..2fda2a7bcb 100644 --- a/server/internal/usecase/interfaces/nlslayer.go +++ b/server/internal/usecase/interfaces/nlslayer.go @@ -86,4 +86,5 @@ type NLSLayer interface { AddGeoJSONFeature(context.Context, AddNLSLayerGeoJSONFeatureParams, *usecase.Operator) (nlslayer.Feature, error) UpdateGeoJSONFeature(context.Context, UpdateNLSLayerGeoJSONFeatureParams, *usecase.Operator) (nlslayer.Feature, error) DeleteGeoJSONFeature(context.Context, DeleteNLSLayerGeoJSONFeatureParams, *usecase.Operator) (id.FeatureID, error) + ImportNLSLayers(context.Context, map[string]interface{}) (nlslayer.NLSLayerList, error) } diff --git a/server/internal/usecase/interfaces/plugin.go b/server/internal/usecase/interfaces/plugin.go index f3533ba1a8..6a7690fbbd 100644 --- a/server/internal/usecase/interfaces/plugin.go +++ b/server/internal/usecase/interfaces/plugin.go @@ -21,4 +21,5 @@ type Plugin interface { Fetch(context.Context, []id.PluginID, *usecase.Operator) ([]*plugin.Plugin, error) Upload(context.Context, io.Reader, id.SceneID, *usecase.Operator) (*plugin.Plugin, *scene.Scene, error) UploadFromRemote(context.Context, *url.URL, id.SceneID, *usecase.Operator) (*plugin.Plugin, *scene.Scene, error) + ImportPlugins(context.Context, []interface{}) ([]*plugin.Plugin, error) } diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go index 7bfc578f3a..1a529b89fb 100644 --- a/server/internal/usecase/interfaces/project.go +++ b/server/internal/usecase/interfaces/project.go @@ -7,6 +7,7 @@ import ( "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/pkg/id" + "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearth/server/pkg/project" "github.com/reearth/reearth/server/pkg/visualizer" "github.com/reearth/reearthx/account/accountdomain" @@ -66,4 +67,6 @@ type Project interface { Publish(context.Context, PublishProjectParam, *usecase.Operator) (*project.Project, error) CheckAlias(context.Context, string) (bool, error) Delete(context.Context, id.ProjectID, *usecase.Operator) error + ExportProject(context.Context, id.ProjectID, *usecase.Operator) (*project.Project, map[string]interface{}, []*plugin.Plugin, error) + ImportProject(context.Context, map[string]interface{}) (*project.Project, usecasex.Tx, error) } diff --git a/server/internal/usecase/interfaces/scene.go b/server/internal/usecase/interfaces/scene.go index ec5060f4a4..9570407216 100644 --- a/server/internal/usecase/interfaces/scene.go +++ b/server/internal/usecase/interfaces/scene.go @@ -6,6 +6,7 @@ import ( "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/pkg/id" + "github.com/reearth/reearth/server/pkg/project" "github.com/reearth/reearth/server/pkg/scene" ) @@ -30,6 +31,7 @@ type Scene interface { AddCluster(context.Context, id.SceneID, string, *usecase.Operator) (*scene.Scene, *scene.Cluster, error) UpdateCluster(context.Context, UpdateClusterParam, *usecase.Operator) (*scene.Scene, *scene.Cluster, error) RemoveCluster(context.Context, id.SceneID, id.ClusterID, *usecase.Operator) (*scene.Scene, error) + ImportScene(context.Context, *project.Project, map[string]interface{}) (*scene.Scene, error) } type UpdateWidgetParam struct { diff --git a/server/internal/usecase/interfaces/story.go b/server/internal/usecase/interfaces/story.go index a6529bb8e9..55db62fb0e 100644 --- a/server/internal/usecase/interfaces/story.go +++ b/server/internal/usecase/interfaces/story.go @@ -149,4 +149,6 @@ type Storytelling interface { CreateBlock(context.Context, CreateBlockParam, *usecase.Operator) (*storytelling.Story, *storytelling.Page, *storytelling.Block, int, error) RemoveBlock(context.Context, RemoveBlockParam, *usecase.Operator) (*storytelling.Story, *storytelling.Page, *id.BlockID, error) MoveBlock(context.Context, MoveBlockParam, *usecase.Operator) (*storytelling.Story, *storytelling.Page, *id.BlockID, int, error) + + ImportStory(context.Context, map[string]interface{}) (*storytelling.Story, error) } diff --git a/server/internal/usecase/interfaces/style.go b/server/internal/usecase/interfaces/style.go index 9e37769d54..75e945dd4f 100644 --- a/server/internal/usecase/interfaces/style.go +++ b/server/internal/usecase/interfaces/style.go @@ -27,4 +27,5 @@ type Style interface { UpdateStyle(context.Context, UpdateStyleInput, *usecase.Operator) (*scene.Style, error) RemoveStyle(context.Context, id.StyleID, *usecase.Operator) (id.StyleID, error) DuplicateStyle(context.Context, id.StyleID, *usecase.Operator) (*scene.Style, error) + ImportStyles(context.Context, map[string]interface{}) (scene.StyleList, error) } diff --git a/server/pkg/property/sealed.go b/server/pkg/property/sealed.go index c76f2bcf6a..0d60915309 100644 --- a/server/pkg/property/sealed.go +++ b/server/pkg/property/sealed.go @@ -117,14 +117,14 @@ func sealedGroup(ctx context.Context, fields []*MergedField, d dataset.GraphLoad return res, nil } -func (s *Sealed) Interface() map[string]interface{} { +func (s *Sealed) Interface(exportType bool) map[string]interface{} { if s == nil { return nil } res := map[string]interface{}{} for _, item := range s.Items { - i := item.Interface() + i := item.Interface(exportType) if i != nil { res[item.SchemaGroup.String()] = i } @@ -133,7 +133,7 @@ func (s *Sealed) Interface() map[string]interface{} { return res } -func (s *SealedItem) Interface() interface{} { +func (s *SealedItem) Interface(exportType bool) interface{} { if s == nil { return nil } @@ -141,7 +141,7 @@ func (s *SealedItem) Interface() interface{} { if len(s.Groups) > 0 { items := make([]map[string]interface{}, 0, len(s.Groups)) for _, g := range s.Groups { - i := sealedFieldsInterface(g.Fields) + i := sealedFieldsInterface(g.Fields, exportType) if g.Original != nil { i["id"] = g.Original.String() } @@ -150,14 +150,18 @@ func (s *SealedItem) Interface() interface{} { return items } - return sealedFieldsInterface(s.Fields) + return sealedFieldsInterface(s.Fields, exportType) } -func sealedFieldsInterface(fields []*SealedField) map[string]interface{} { +func sealedFieldsInterface(fields []*SealedField, exportType bool) map[string]interface{} { item := map[string]interface{}{} for _, f := range fields { - item[f.ID.String()] = f.Val.Value().Interface() + if exportType { + item[f.ID.String()] = f.Val.Value().InterfaceWithType() + } else { + item[f.ID.String()] = f.Val.Value().Interface() + } } return item diff --git a/server/pkg/property/sealed_test.go b/server/pkg/property/sealed_test.go index 19dd8725a5..2b2b1eb9da 100644 --- a/server/pkg/property/sealed_test.go +++ b/server/pkg/property/sealed_test.go @@ -463,7 +463,7 @@ func TestSealed_Interface(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - res := tc.S.Interface() + res := tc.S.Interface(false) assert.Equal(t, tc.Expected, res) }) } diff --git a/server/pkg/property/value.go b/server/pkg/property/value.go index 1f33d9da57..844d1f77b5 100644 --- a/server/pkg/property/value.go +++ b/server/pkg/property/value.go @@ -113,6 +113,16 @@ func (v *Value) Interface() interface{} { return v.v.Interface() } +func (v *Value) InterfaceWithType() interface{} { + if v == nil { + return nil + } + return map[string]interface{}{ + "value": v.v.Interface(), + "type": v.v.Type(), + } +} + func (v *Value) Cast(vt ValueType) *Value { if v == nil { return nil diff --git a/server/pkg/scene/builder/builder.go b/server/pkg/scene/builder/builder.go index c940d2ea5e..91dc6607c5 100644 --- a/server/pkg/scene/builder/builder.go +++ b/server/pkg/scene/builder/builder.go @@ -3,6 +3,7 @@ package builder import ( "context" "encoding/json" + "errors" "io" "time" @@ -33,15 +34,18 @@ type Builder struct { nlsLayer *nlslayer.NLSLayerList layerStyles *scene.StyleList story *storytelling.Story + + exportType bool } -func New(ll layer.Loader, pl property.Loader, dl dataset.GraphLoader, tl tag.Loader, tsl tag.SceneLoader, nlsl nlslayer.Loader) *Builder { +func New(ll layer.Loader, pl property.Loader, dl dataset.GraphLoader, tl tag.Loader, tsl tag.SceneLoader, nlsl nlslayer.Loader, exp bool) *Builder { e := &encoder{} return &Builder{ - ploader: pl, - tloader: tsl, - nlsloader: nlsl, - encoder: e, + ploader: pl, + tloader: tsl, + nlsloader: nlsl, + encoder: e, + exportType: exp, exporter: &encoding.Exporter{ Merger: &merging.Merger{ LayerLoader: ll, @@ -125,6 +129,43 @@ func (b *Builder) Build(ctx context.Context, w io.Writer, publishedAt time.Time, return json.NewEncoder(w).Encode(res) } +func (b *Builder) BuildResult(ctx context.Context, publishedAt time.Time, coreSupport bool, enableGa bool, trackingId string) (*sceneJSON, error) { + if b == nil || b.scene == nil { + return nil, errors.New("invalid builder state") + } + + sceneData, err := b.buildScene(ctx, publishedAt, coreSupport, enableGa, trackingId) + if err != nil { + return nil, err + } + + if b.story != nil { + story, err := b.buildStory(ctx) + if err != nil { + return nil, err + } + sceneData.Story = story + } + + if b.nlsLayer != nil { + nlsLayers, err := b.buildNLSLayers(ctx) + if err != nil { + return nil, err + } + sceneData.NLSLayers = nlsLayers + } + + if b.layerStyles != nil { + layerStyles, err := b.buildLayerStyles(ctx) + if err != nil { + return nil, err + } + sceneData.LayerStyles = layerStyles + } + + return sceneData, nil +} + func (b *Builder) buildScene(ctx context.Context, publishedAt time.Time, coreSupport bool, enableGa bool, trackingId string) (*sceneJSON, error) { if b == nil { return nil, nil diff --git a/server/pkg/scene/builder/builder_test.go b/server/pkg/scene/builder/builder_test.go index c343970ab3..e6bbf8d801 100644 --- a/server/pkg/scene/builder/builder_test.go +++ b/server/pkg/scene/builder/builder_test.go @@ -11,6 +11,7 @@ import ( "github.com/reearth/reearth/server/pkg/property" "github.com/reearth/reearth/server/pkg/scene" "github.com/reearth/reearth/server/pkg/tag" + "github.com/reearth/reearth/server/pkg/value" "github.com/reearth/reearthx/account/accountdomain" "github.com/stretchr/testify/assert" ) @@ -768,10 +769,51 @@ func TestSceneBuilder(t *testing.T) { Clusters: []*clusterJSON{}, } + exportType := false + // exec - sb := New(lloader, ploader, dloader, tloader, tsloader, nlsloader).ForScene(scene) + sb := New(lloader, ploader, dloader, tloader, tsloader, nlsloader, exportType).ForScene(scene) result, err := sb.buildScene(context.Background(), publishedAt, false, false, "") assert.NoError(t, err) assert.Equal(t, expected, result) + + // export mode + exportType = true + + sb = New(lloader, ploader, dloader, tloader, tsloader, nlsloader, exportType).ForScene(scene) + result, err = sb.buildScene(context.Background(), publishedAt, false, false, "") + + expected.Property = map[string]interface{}{ + "A": map[string]interface{}{ + "a": map[string]interface{}{ + "type": value.TypeString, + "value": "hogehoge", + }, + }, + } + + expected.Plugins[pluginID.String()] = map[string]interface{}{ + "A": map[string]interface{}{ + "a": map[string]interface{}{ + "type": value.TypeString, + "value": "hogehoge", + }, + }, + } + + // result.Widgets = result.Widgets[:1] + result.Widgets = result.Widgets[1:] + expected.Widgets[0].Property = map[string]interface{}{ + "A": map[string]interface{}{ + "a": map[string]interface{}{ + "type": value.TypeString, + "value": "hogehoge", + }, + }, + } + + assert.NoError(t, err) + assert.Equal(t, expected, result) + } diff --git a/server/pkg/scene/builder/decoder.go b/server/pkg/scene/builder/decoder.go new file mode 100644 index 0000000000..d6adb61efc --- /dev/null +++ b/server/pkg/scene/builder/decoder.go @@ -0,0 +1,140 @@ +package builder + +import ( + "context" + "encoding/json" + + "github.com/reearth/reearth/server/internal/adapter/gql/gqlmodel" + "github.com/reearth/reearth/server/pkg/id" + "github.com/reearth/reearth/server/pkg/property" + "github.com/reearth/reearth/server/pkg/scene" + "github.com/reearth/reearthx/idx" +) + +func ParseSceneJSON(ctx context.Context, sceneJSONData map[string]interface{}) (*sceneJSON, error) { + sceneBytes, err := json.MarshalIndent(sceneJSONData, "", " ") + if err != nil { + return nil, err + } + var result sceneJSON + if err := json.Unmarshal(sceneBytes, &result); err != nil { + return nil, err + } + return &result, nil +} + +func ParserWidgetAlignSystem(widgetAlignSystemJSON *widgetAlignSystemJSON) *scene.WidgetAlignSystem { + if widgetAlignSystemJSON == nil { + return nil + } + was := scene.NewWidgetAlignSystem() + if widgetAlignSystemJSON.Inner != nil { + parseWidgetZone(was.Zone(scene.WidgetZoneInner), widgetAlignSystemJSON.Inner) + } + if widgetAlignSystemJSON.Outer != nil { + parseWidgetZone(was.Zone(scene.WidgetZoneOuter), widgetAlignSystemJSON.Outer) + } + return was +} + +func parseWidgetZone(zone *scene.WidgetZone, widgetZoneJSON *widgetZoneJSON) { + if zone == nil || widgetZoneJSON == nil { + return + } + if widgetZoneJSON.Left != nil { + setWidgetSection(zone.Section(scene.WidgetSectionLeft), widgetZoneJSON.Left) + } + if widgetZoneJSON.Center != nil { + setWidgetSection(zone.Section(scene.WidgetSectionCenter), widgetZoneJSON.Center) + } + if widgetZoneJSON.Right != nil { + setWidgetSection(zone.Section(scene.WidgetSectionRight), widgetZoneJSON.Right) + } +} + +func setWidgetSection(section *scene.WidgetSection, widgetSectionJSON *widgetSectionJSON) { + if section == nil || widgetSectionJSON == nil { + return + } + section.SetArea(scene.WidgetAreaTop, parseWidgetArea(widgetSectionJSON.Top)) + section.SetArea(scene.WidgetAreaMiddle, parseWidgetArea(widgetSectionJSON.Middle)) + section.SetArea(scene.WidgetAreaBottom, parseWidgetArea(widgetSectionJSON.Bottom)) +} + +func parseWidgetArea(widgetAreaJSON *widgetAreaJSON) *scene.WidgetArea { + if widgetAreaJSON == nil { + return nil + } + var widgetIDs []idx.ID[id.Widget] + for _, widgetID := range widgetAreaJSON.WidgetIDs { + id, err := gqlmodel.ToID[id.Widget](gqlmodel.ID(widgetID)) + if err != nil { + continue + } + widgetIDs = append(widgetIDs, id) + } + return scene.NewWidgetArea( + widgetIDs, + parseWidgetAlign(widgetAreaJSON.Align), + parseWidgetAreaPadding(widgetAreaJSON.Padding), + widgetAreaJSON.Gap, + widgetAreaJSON.Centered, + widgetAreaJSON.Background, + ) +} + +func parseWidgetAlign(align string) scene.WidgetAlignType { + switch align { + case "start": + return scene.WidgetAlignStart + case "centered": + return scene.WidgetAlignCentered + case "end": + return scene.WidgetAlignEnd + default: + return scene.WidgetAlignStart + } +} + +func parseWidgetAreaPadding(paddingJSON *widgetAreaPaddingJSON) *scene.WidgetAreaPadding { + if paddingJSON == nil { + return nil + } + return scene.NewWidgetAreaPadding( + paddingJSON.Left, + paddingJSON.Right, + paddingJSON.Top, + paddingJSON.Bottom, + ) +} + +func AddItemFromPropertyJSON(prop *property.Property, ps *property.Schema, pj propertyJSON) (*property.Property, error) { + for sgKey, value1 := range pj { + schemaGroupID := id.PropertySchemaGroupIDFromRef(&sgKey) + if iVal, ok := value1.(map[string]interface{}); ok { + for fKey, value2 := range iVal { + fieldID := id.PropertyFieldIDFromRef(&fKey) + ptr := property.NewPointer(schemaGroupID, nil, fieldID) + v, ok := parsePropertyValue(value2) + if ok { + _, _, _, err := prop.UpdateValue(ps, ptr, v) + if err != nil { + return nil, err + } + } + } + } + } + return prop, nil +} + +func parsePropertyValue(value interface{}) (*property.Value, bool) { + if fieldObj, ok := value.(map[string]interface{}); ok { + fieldType, ok1 := fieldObj["type"].(string) + fieldVal, ok2 := fieldObj["value"] + if ok1 && ok2 { + return property.ValueType(fieldType).ValueFrom(fieldVal), ok + } + } + return nil, false +} diff --git a/server/pkg/scene/builder/encoder.go b/server/pkg/scene/builder/encoder.go index dbee9b3a6e..74417251f9 100644 --- a/server/pkg/scene/builder/encoder.go +++ b/server/pkg/scene/builder/encoder.go @@ -109,7 +109,7 @@ func (e *encoder) infobox(i *merging.SealedInfobox) *infoboxJSON { } func (e *encoder) property(p *property.Sealed) propertyJSON { - return p.Interface() + return p.Interface(false) } type layerJSON struct { @@ -183,6 +183,7 @@ type widgetJSON struct { PluginID string `json:"pluginId"` ExtensionID string `json:"extensionId"` Property propertyJSON `json:"property"` + Enabled bool `json:"enabled"` Extended bool `json:"extended"` } diff --git a/server/pkg/scene/builder/scene.go b/server/pkg/scene/builder/scene.go index 0f92e7245b..cf3fa9c2c9 100644 --- a/server/pkg/scene/builder/scene.go +++ b/server/pkg/scene/builder/scene.go @@ -69,7 +69,7 @@ func (b *Builder) widgets(ctx context.Context, p []*property.Property) []*widget sceneWidgets := b.scene.Widgets().Widgets() res := make([]*widgetJSON, 0, len(sceneWidgets)) for _, w := range sceneWidgets { - if !w.Enabled() { + if !b.exportType && !w.Enabled() { continue } @@ -137,12 +137,12 @@ func toTag(t tag.Tag, m tag.Map) tagJSON { } func (b *Builder) property(ctx context.Context, p *property.Property) propertyJSON { - return property.SealProperty(ctx, p).Interface() + return property.SealProperty(ctx, p).Interface(b.exportType) } func findProperty(pp []*property.Property, i property.ID) *property.Property { for _, p := range pp { - if p.ID() == i { + if p != nil && p.ID() == i { return p } } diff --git a/server/pkg/scene/scene.go b/server/pkg/scene/scene.go index 1af9ad901e..1249d1d64a 100644 --- a/server/pkg/scene/scene.go +++ b/server/pkg/scene/scene.go @@ -3,6 +3,8 @@ package scene import ( "errors" "time" + + "github.com/reearth/reearth/server/pkg/id" ) var ErrSceneIsLocked error = errors.New("scene is locked") @@ -76,6 +78,17 @@ func (s *Scene) Plugins() *Plugins { return s.plugins } +func (s *Scene) PluginIds() []id.PluginID { + if s == nil { + return nil + } + var pluginIDs []id.PluginID + for _, plugin := range s.plugins.Plugins() { + pluginIDs = append(pluginIDs, plugin.Plugin()) + } + return pluginIDs +} + func (s *Scene) UpdatedAt() time.Time { if s == nil { return time.Time{} diff --git a/server/pkg/scene/widget.go b/server/pkg/scene/widget.go index 3a1cb7bc42..fa2f0a9c8c 100644 --- a/server/pkg/scene/widget.go +++ b/server/pkg/scene/widget.go @@ -9,7 +9,7 @@ type Widget struct { extended bool } -func NewWidget(wid WidgetID, plugin PluginID, extension PluginExtensionID, property PropertyID, enabled, extended bool) (*Widget, error) { +func NewWidget(wid WidgetID, plugin PluginID, extension PluginExtensionID, property PropertyID, enabled bool, extended bool) (*Widget, error) { if !plugin.Validate() || string(extension) == "" || property.IsNil() { return nil, ErrInvalidID }