├── internal ├── usecase │ ├── cursor.go │ ├── interactor │ │ ├── emails │ │ │ └── auth_text.tmpl │ │ ├── team_test.go │ │ ├── published_test.go │ │ └── layer_test.go │ ├── gateway │ │ ├── google.go │ │ ├── mailer.go │ │ ├── container.go │ │ ├── authenticator.go │ │ ├── plugin_registry.go │ │ ├── datasouce.go │ │ └── file.go │ ├── repo │ │ ├── config.go │ │ ├── lock.go │ │ ├── scene_lock.go │ │ ├── auth_request.go │ │ ├── transaction.go │ │ ├── team.go │ │ ├── scene.go │ │ ├── plugin.go │ │ ├── asset.go │ │ ├── user.go │ │ ├── property_schema.go │ │ ├── dataset_schema.go │ │ ├── property.go │ │ ├── project.go │ │ └── tag.go │ ├── pagination.go │ ├── interfaces │ │ ├── common.go │ │ ├── plugin.go │ │ ├── asset.go │ │ ├── team.go │ │ └── published.go │ └── pageinfo.go ├── infrastructure │ ├── marketplace │ │ └── testdata │ │ │ └── test.zip │ ├── fs │ │ ├── common.go │ │ └── plugin_test.go │ ├── memory │ │ ├── config_test.go │ │ ├── lock.go │ │ ├── container.go │ │ ├── layer_test.go │ │ ├── transaction_test.go │ │ ├── config.go │ │ └── transaction.go │ ├── google │ │ ├── google.go │ │ └── fetch.go │ ├── mailer │ │ ├── logger.go │ │ ├── mock.go │ │ ├── sendgrid.go │ │ └── smtp.go │ ├── mongo │ │ ├── transaction.go │ │ ├── migration │ │ │ ├── migrations.go │ │ │ ├── 210310145844_remove_preview_token.go │ │ │ ├── 220309174648_add_scene_field_to_property_schema.go │ │ │ ├── 201217132559_add_scene_widget_id.go │ │ │ └── 220214180713_split_schema_of_properties.go │ │ ├── mongodoc │ │ │ ├── config.go │ │ │ ├── consumer.go │ │ │ ├── pagination.go │ │ │ └── consumer_test.go │ │ └── mongo_test.go │ └── gcs │ │ └── file_test.go ├── adapter │ ├── gql │ │ ├── resolver_mutation.go │ │ ├── gqlmodel │ │ │ ├── convert_value_test.go │ │ │ ├── convert_asset.go │ │ │ ├── convert_property_test.go │ │ │ └── convert_tag.go │ │ ├── resolver.go │ │ ├── resolver_asset.go │ │ ├── resolver_user.go │ │ ├── resolver_project.go │ │ ├── resolver_property_test.go │ │ ├── resolver_mutation_asset.go │ │ ├── resolver_team.go │ │ └── context.go │ └── http │ │ └── published.go └── app │ ├── profiler.go │ ├── private.go │ ├── web.go │ └── usecase.go ├── main.go ├── pkg ├── plugin │ ├── pluginpack │ │ ├── testdata │ │ │ ├── test │ │ │ │ ├── test │ │ │ │ │ └── foo.bar │ │ │ │ ├── index.js │ │ │ │ ├── reearth_ja.yml │ │ │ │ ├── reearth_zh-CN.yml │ │ │ │ └── reearth.yml │ │ │ └── test.zip │ │ └── package_test.go │ ├── manifest │ │ ├── testdata │ │ │ ├── minimum.yml │ │ │ ├── test.yml │ │ │ └── translation.yml │ │ ├── manifest.go │ │ └── parser_translation.go │ ├── loader.go │ ├── metadata.go │ ├── id.go │ ├── list.go │ └── list_test.go ├── layer │ ├── item_test.go │ ├── layer_test.go │ ├── decoding │ │ ├── shapetest │ │ │ ├── point.shp │ │ │ ├── shapes.zip │ │ │ ├── polygon.shp │ │ │ └── polyline.shp │ │ ├── format.go │ │ └── decoder.go │ ├── item_builder_test.go │ ├── group_builder_test.go │ ├── encoding │ │ ├── common_test.go │ │ ├── encoder.go │ │ ├── common.go │ │ └── exporter.go │ ├── layerops │ │ └── processor.go │ ├── infobox_field_builder.go │ ├── infobox_field.go │ └── builder.go ├── builtin │ └── migration.go ├── file │ ├── testdata │ │ ├── test.zip │ │ └── test.tar.gz │ ├── targz.go │ └── zip_test.go ├── shp │ ├── test_files │ │ ├── empty.zip │ │ ├── multi.zip │ │ ├── point.shp │ │ ├── pointm.shp │ │ ├── pointz.shp │ │ ├── polygon.shp │ │ ├── polygonm.shp │ │ ├── polygonz.shp │ │ ├── polyline.shp │ │ ├── polylinem.shp │ │ ├── polylinez.shp │ │ ├── multipatch.shp │ │ ├── multipoint.shp │ │ ├── multipointm.shp │ │ ├── multipointz.shp │ │ └── ne_110m_admin_0_countries.zip │ ├── errreader.go │ ├── sequentialreader_test.go │ └── shapetype_string.go ├── dataset │ ├── diff.go │ ├── loader.go │ ├── schema_field_diff.go │ ├── graph_loader.go │ ├── field.go │ ├── schema_field.go │ ├── schema_list_test.go │ ├── schema_list.go │ ├── id.go │ └── schema_field_builder.go ├── id │ ├── common.go │ └── idx │ │ ├── id_test.go │ │ ├── string.go │ │ ├── string_test.go │ │ ├── ulid.go │ │ └── set.go ├── visualizer │ └── visualizer.go ├── util │ ├── now.go │ ├── util.go │ └── util_test.go ├── property │ ├── id_test.go │ ├── migrator.go │ ├── condition.go │ ├── schema_pointer.go │ ├── schema_field_ui_test.go │ ├── validator.go │ ├── item_builder.go │ ├── schema_pointer_test.go │ ├── condition_test.go │ ├── value_camera_test.go │ ├── field_builder.go │ ├── loader.go │ ├── builder.go │ ├── schema_builder.go │ ├── value_dataset.go │ └── item.go ├── user │ ├── theme.go │ ├── team.go │ ├── id.go │ ├── auth.go │ ├── team_test.go │ ├── password_reset.go │ ├── role.go │ ├── team_builder.go │ ├── initializer.go │ ├── team_list.go │ └── password_test.go ├── project │ ├── publishment_status.go │ └── id.go ├── config │ ├── config_test.go │ └── config.go ├── tag │ ├── group.go │ ├── tag_test.go │ ├── map.go │ ├── tag.go │ ├── item.go │ ├── loader.go │ ├── id.go │ └── group_builder.go ├── asset │ ├── id.go │ ├── sort_type.go │ ├── asset.go │ └── builder.go ├── scene │ ├── plugin_test.go │ ├── plugin.go │ ├── list.go │ ├── cluster.go │ ├── lock.go │ └── list_test.go ├── value │ ├── latlng_test.go │ ├── latlngheight_test.go │ ├── ref.go │ ├── bool.go │ ├── url.go │ ├── rect.go │ ├── string.go │ ├── string_test.go │ ├── polygon.go │ ├── latlng.go │ ├── type.go │ ├── bool_test.go │ ├── latlngheight.go │ └── optional.go ├── cache │ └── cache.go ├── i18n │ └── string.go ├── writer │ └── seeker_closer.go └── czml │ └── czml.go ├── .graphqlconfig ├── cmd └── reearth │ ├── debug.go │ ├── release.go │ └── main.go ├── .github ├── CODEOWNERS ├── pull_request_template.md ├── changelog.yml ├── workflows │ ├── renovate.yml │ ├── stage.yml │ └── pr_title.yml └── renovate.json ├── tools.go ├── .golangci.yml ├── codecov.yml ├── Makefile ├── .vscode ├── launch.json ├── extensions.json └── settings.json ├── README.md ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── .goreleaser.yml ├── tools └── cmd │ └── shapefiletest │ └── main.go └── gqlgen.yml /internal/usecase/cursor.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() {} 4 | -------------------------------------------------------------------------------- /pkg/plugin/pluginpack/testdata/test/test/foo.bar: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "schema.graphql" 3 | } 4 | -------------------------------------------------------------------------------- /pkg/layer/item_test.go: -------------------------------------------------------------------------------- 1 | package layer 2 | 3 | var _ Layer = &Item{} 4 | -------------------------------------------------------------------------------- /pkg/plugin/manifest/testdata/minimum.yml: -------------------------------------------------------------------------------- 1 | id: aaa 2 | version: 1.1.1 3 | -------------------------------------------------------------------------------- /pkg/builtin/migration.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | // TODO: migration code 4 | -------------------------------------------------------------------------------- /pkg/plugin/pluginpack/testdata/test/index.js: -------------------------------------------------------------------------------- 1 | console.log("hello world"); 2 | -------------------------------------------------------------------------------- /pkg/plugin/pluginpack/testdata/test/reearth_ja.yml: -------------------------------------------------------------------------------- 1 | { "name": "テストプラグイン" } 2 | -------------------------------------------------------------------------------- /pkg/plugin/pluginpack/testdata/test/reearth_zh-CN.yml: -------------------------------------------------------------------------------- 1 | { "name": "测试插件" } 2 | -------------------------------------------------------------------------------- /cmd/reearth/debug.go: -------------------------------------------------------------------------------- 1 | //go:build !release 2 | 3 | package main 4 | 5 | const debug = true 6 | -------------------------------------------------------------------------------- /cmd/reearth/release.go: -------------------------------------------------------------------------------- 1 | //go:build release 2 | 3 | package main 4 | 5 | const debug = false 6 | -------------------------------------------------------------------------------- /pkg/layer/layer_test.go: -------------------------------------------------------------------------------- 1 | package layer 2 | 3 | var _ Layer = &Item{} 4 | var _ Layer = &Group{} 5 | -------------------------------------------------------------------------------- /pkg/file/testdata/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/file/testdata/test.zip -------------------------------------------------------------------------------- /pkg/file/testdata/test.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/file/testdata/test.tar.gz -------------------------------------------------------------------------------- /pkg/plugin/pluginpack/testdata/test/reearth.yml: -------------------------------------------------------------------------------- 1 | { "id": "testplugin", "version": "1.0.1", "name": "testplugin" } 2 | -------------------------------------------------------------------------------- /pkg/shp/test_files/empty.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/empty.zip -------------------------------------------------------------------------------- /pkg/shp/test_files/multi.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/multi.zip -------------------------------------------------------------------------------- /pkg/shp/test_files/point.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/point.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/pointm.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/pointm.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/pointz.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/pointz.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/polygon.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/polygon.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/polygonm.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/polygonm.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/polygonz.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/polygonz.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/polyline.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/polyline.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/polylinem.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/polylinem.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/polylinez.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/polylinez.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/multipatch.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/multipatch.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/multipoint.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/multipoint.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/multipointm.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/multipointm.shp -------------------------------------------------------------------------------- /pkg/shp/test_files/multipointz.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/multipointz.shp -------------------------------------------------------------------------------- /pkg/dataset/diff.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | type Diff struct { 4 | Added List 5 | Removed List 6 | Others map[ID]*Dataset 7 | } 8 | -------------------------------------------------------------------------------- /pkg/layer/decoding/shapetest/point.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/layer/decoding/shapetest/point.shp -------------------------------------------------------------------------------- /pkg/layer/decoding/shapetest/shapes.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/layer/decoding/shapetest/shapes.zip -------------------------------------------------------------------------------- /pkg/plugin/pluginpack/testdata/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/plugin/pluginpack/testdata/test.zip -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rot1024 2 | /pkg/builtin/manifest.yml @HideBa 3 | /pkg/builtin/manifest_ja.yml @HideBa 4 | -------------------------------------------------------------------------------- /pkg/id/common.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import "github.com/reearth/reearth-backend/pkg/id/idx" 4 | 5 | var ErrInvalidID = idx.ErrInvalidID 6 | -------------------------------------------------------------------------------- /pkg/layer/decoding/shapetest/polygon.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/layer/decoding/shapetest/polygon.shp -------------------------------------------------------------------------------- /pkg/layer/decoding/shapetest/polyline.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/layer/decoding/shapetest/polyline.shp -------------------------------------------------------------------------------- /pkg/plugin/loader.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Loader func(context.Context, []ID) ([]*Plugin, error) 8 | -------------------------------------------------------------------------------- /pkg/visualizer/visualizer.go: -------------------------------------------------------------------------------- 1 | package visualizer 2 | 3 | type Visualizer string 4 | 5 | const ( 6 | VisualizerCesium Visualizer = "cesium" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/shp/test_files/ne_110m_admin_0_countries.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/pkg/shp/test_files/ne_110m_admin_0_countries.zip -------------------------------------------------------------------------------- /internal/usecase/interactor/emails/auth_text.tmpl: -------------------------------------------------------------------------------- 1 | Hi {{ .UserName }}: 2 | {{ .Message }} 3 | 4 | To {{ .ActionLabel }}: 5 | {{ .ActionURL }} 6 | 7 | {{ .Suffix }} -------------------------------------------------------------------------------- /internal/infrastructure/marketplace/testdata/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reearth/reearth-backend/HEAD/internal/infrastructure/marketplace/testdata/test.zip -------------------------------------------------------------------------------- /cmd/reearth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/reearth/reearth-backend/internal/app" 4 | 5 | var version = "" 6 | 7 | func main() { 8 | app.Start(debug, version) 9 | } 10 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package main 4 | 5 | import ( 6 | _ "github.com/99designs/gqlgen" 7 | _ "github.com/idubinskiy/schematyper" 8 | _ "github.com/vektah/dataloaden" 9 | ) 10 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver_mutation.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | func (r *Resolver) Mutation() MutationResolver { 4 | return &mutationResolver{r} 5 | } 6 | 7 | type mutationResolver struct{ *Resolver } 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-use-default: false 3 | 4 | linters: 5 | enable: 6 | - gofmt 7 | - goimports 8 | 9 | goimports: 10 | local-prefixes: github.com/reearth/reearth-backend 11 | -------------------------------------------------------------------------------- /internal/infrastructure/fs/common.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | const ( 4 | assetDir = "assets" 5 | pluginDir = "plugins" 6 | publishedDir = "published" 7 | manifestFilePath = "reearth.yml" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/usecase/gateway/google.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Google interface { 8 | FetchCSV(token string, fileId string, sheetName string) (*io.ReadCloser, error) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/util/now.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "time" 4 | 5 | var Now = time.Now 6 | 7 | func MockNow(t time.Time) func() { 8 | Now = func() time.Time { return t } 9 | return func() { Now = time.Now } 10 | } 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ## What I've done 4 | 5 | 6 | ## What I haven't done 7 | 8 | ## How I tested 9 | 10 | ## Which point I want you to review particularly 11 | 12 | ## Memo 13 | -------------------------------------------------------------------------------- /pkg/property/id_test.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | func mockNewItemID(id ItemID) func() { 4 | original := NewItemID 5 | NewItemID = func() ItemID { return id } 6 | return func() { 7 | NewItemID = original 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /internal/usecase/gateway/mailer.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | type Contact struct { 4 | Email string 5 | Name string 6 | } 7 | 8 | type Mailer interface { 9 | SendMail(toContacts []Contact, subject, plainContent, htmlContent string) error 10 | } 11 | -------------------------------------------------------------------------------- /pkg/user/theme.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | type Theme string 4 | 5 | const ( 6 | ThemeDefault Theme = "default" 7 | ThemeLight Theme = "light" 8 | ThemeDark Theme = "dark" 9 | ) 10 | 11 | func (t Theme) Ref() *Theme { 12 | return &t 13 | } 14 | -------------------------------------------------------------------------------- /internal/usecase/gateway/container.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | type Container struct { 4 | Authenticator Authenticator 5 | Mailer Mailer 6 | DataSource DataSource 7 | PluginRegistry PluginRegistry 8 | File File 9 | Google Google 10 | } 11 | -------------------------------------------------------------------------------- /pkg/layer/item_builder_test.go: -------------------------------------------------------------------------------- 1 | package layer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestItemBuilder_Tags(t *testing.T) { 10 | l := NewTagList(nil) 11 | b := NewItem().NewID().Tags(l).MustBuild() 12 | assert.Same(t, l, b.Tags()) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/layer/group_builder_test.go: -------------------------------------------------------------------------------- 1 | package layer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGroupBuilder_Tags(t *testing.T) { 10 | l := NewTagList(nil) 11 | b := NewGroup().NewID().Tags(l).MustBuild() 12 | assert.Same(t, l, b.Tags()) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/project/publishment_status.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | type PublishmentStatus string 4 | 5 | const ( 6 | PublishmentStatusPublic PublishmentStatus = "public" 7 | 8 | PublishmentStatusLimited PublishmentStatus = "limited" 9 | 10 | PublishmentStatusPrivate PublishmentStatus = "private" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/property/migrator.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | type Migrator struct { 4 | NewSchema *Schema 5 | Plans []MigrationPlan 6 | } 7 | 8 | type MigrationPlan struct { 9 | From *Pointer 10 | To *Pointer 11 | } 12 | 13 | // func (m Migrator) Migrate(from *Property) *Property { 14 | 15 | // } 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: 'reach, diff, flags, files' 3 | behavior: default 4 | require_changes: false 5 | ignore: 6 | - "**/*_gen.go" 7 | - "**/*_test.go" 8 | - "**/doc.go" 9 | - "**/testdata" 10 | - internal/adapter/gql/generated.go 11 | - tools 12 | - main.go 13 | - tools.go 14 | -------------------------------------------------------------------------------- /internal/infrastructure/memory/config_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConfig(t *testing.T) { 11 | ctx := context.Background() 12 | c := NewConfig() 13 | assert.NoError(t, c.Unlock(ctx)) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/property/condition.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | type Condition struct { 4 | Field FieldID 5 | Value *Value 6 | } 7 | 8 | func (c *Condition) Clone() *Condition { 9 | if c == nil { 10 | return nil 11 | } 12 | return &Condition{ 13 | Field: c.Field, 14 | Value: c.Value.Clone(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestConfigNextMigrations(t *testing.T) { 10 | c := &Config{ 11 | Migration: 100, 12 | } 13 | assert.Equal(t, []int64{200, 500}, c.NextMigrations([]int64{1, 100, 500, 200, 2})) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/plugin/metadata.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "time" 4 | 5 | type Metadata struct { 6 | Name string `json:"name"` 7 | Description string `json:"description"` 8 | ThumbnailUrl string `json:"thumbnailUrl"` 9 | Author string `json:"author"` 10 | CreatedAt time.Time `json:"createdAt"` 11 | } 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | golangci-lint run --fix 3 | 4 | test: 5 | go test -race -v ./... 6 | 7 | build: 8 | go build ./cmd/reearth 9 | 10 | run-app: 11 | go run ./cmd/reearth 12 | 13 | run-db: 14 | docker compose up -d reearth-mongo 15 | 16 | gql: 17 | go generate ./internal/adapter/gql 18 | 19 | .PHONY: lint test build run-app run-db gql 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "cwd": "${workspaceRoot}", 10 | "program": "${workspaceRoot}/cmd/reearth", 11 | "env": {}, 12 | "args": [] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /internal/usecase/repo/config.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/config" 7 | ) 8 | 9 | type Config interface { 10 | LockAndLoad(context.Context) (*config.Config, error) 11 | Save(context.Context, *config.Config) error 12 | SaveAndUnlock(context.Context, *config.Config) error 13 | Unlock(context.Context) error 14 | } 15 | -------------------------------------------------------------------------------- /internal/usecase/repo/lock.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrFailedToLock = errors.New("failed to lock") 10 | ErrAlreadyLocked = errors.New("already locked") 11 | ErrNotLocked = errors.New("not locked") 12 | ) 13 | 14 | type Lock interface { 15 | Lock(context.Context, string) error 16 | Unlock(context.Context, string) error 17 | } 18 | -------------------------------------------------------------------------------- /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | prefixes: 2 | feat: 🚀 Features 3 | fix: 🔧 Bug Fixes 4 | docs: 📖 Documentation 5 | doc: 📖 Documentation 6 | perf: ⚡️ Performance 7 | refactor: ✨ Refactor 8 | style: 🎨 Styling 9 | test: 🧪 Testing 10 | chore: Miscellaneous Tasks 11 | build: Miscellaneous Tasks 12 | deps: Miscellaneous Tasks 13 | ci: false 14 | revert: false 15 | titleVersionPrefix: remove 16 | -------------------------------------------------------------------------------- /internal/adapter/gql/gqlmodel/convert_value_test.go: -------------------------------------------------------------------------------- 1 | package gqlmodel 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/reearth/reearth-backend/pkg/value" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_FromValueType(t *testing.T) { 11 | assert.Equal(t, value.TypeString, FromValueType(ValueTypeString)) 12 | assert.Equal(t, value.TypeNumber, FromValueType(ValueTypeNumber)) 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reearth-backend (DEPRECATED AND MIGRATED TO [reearth/reearth](https://github.com/reearth/reearth/blob/main/server)) 2 | 3 | **⚠️NOTE: This repository has been migrated to [reearth/reearth](https://github.com/reearth/reearth/blob/main/server) repository and this repo will be no longer used.** 4 | 5 | ## License 6 | 7 | Distributed under the Apache-2.0 License. See [Apache License 2.0](LICENSE) for more information. 8 | -------------------------------------------------------------------------------- /pkg/layer/decoding/format.go: -------------------------------------------------------------------------------- 1 | package decoding 2 | 3 | type LayerEncodingFormat string 4 | 5 | const ( 6 | LayerEncodingFormatKML LayerEncodingFormat = "kml" 7 | LayerEncodingFormatCZML LayerEncodingFormat = "czml" 8 | LayerEncodingFormatGEOJSON LayerEncodingFormat = "geojson" 9 | LayerEncodingFormatSHAPE LayerEncodingFormat = "shape" 10 | LayerEncodingFormatREEARTH LayerEncodingFormat = "reearth" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/property/schema_pointer.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | type SchemaFieldPointer struct { 4 | SchemaGroup SchemaGroupID 5 | Field FieldID 6 | } 7 | 8 | func (p SchemaFieldPointer) Pointer() *Pointer { 9 | return PointFieldBySchemaGroup(p.SchemaGroup, p.Field) 10 | } 11 | 12 | func (p *SchemaFieldPointer) Clone() *SchemaFieldPointer { 13 | if p == nil { 14 | return p 15 | } 16 | p2 := *p 17 | return &p2 18 | } 19 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func Must[T any](v T, err error) T { 4 | if err != nil { 5 | panic(err) 6 | } 7 | return v 8 | } 9 | 10 | func IsZero[T comparable](v T) bool { 11 | var z T 12 | return v == z 13 | } 14 | 15 | func IsNotZero[T comparable](v T) bool { 16 | return !IsZero(v) 17 | } 18 | 19 | func Deref[T any](r *T) T { 20 | if r == nil { 21 | var z T 22 | return z 23 | } 24 | return *r 25 | } 26 | -------------------------------------------------------------------------------- /internal/infrastructure/google/google.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase/gateway" 7 | ) 8 | 9 | type google struct { 10 | } 11 | 12 | func NewGoogle() gateway.Google { 13 | return &google{} 14 | } 15 | 16 | func (g google) FetchCSV(token string, fileId string, sheetName string) (*io.ReadCloser, error) { 17 | return fetchCSV(token, fileId, sheetName) 18 | } 19 | -------------------------------------------------------------------------------- /internal/infrastructure/memory/lock.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase/repo" 7 | ) 8 | 9 | type Lock struct{} 10 | 11 | func NewLock() repo.Lock { 12 | return &Lock{} 13 | } 14 | 15 | func (r *Lock) Lock(_ context.Context, _ string) error { 16 | return nil 17 | } 18 | 19 | func (r *Lock) Unlock(_ context.Context, _ string) error { 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/99designs/gqlgen 2 | 3 | package gql 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. 10 | 11 | var ErrNotImplemented = errors.New("not impleneted yet") 12 | var ErrUnauthorized = errors.New("unauthorized") 13 | 14 | type Resolver struct { 15 | } 16 | 17 | func NewResolver() ResolverRoot { 18 | return &Resolver{} 19 | } 20 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver_asset.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel" 7 | ) 8 | 9 | func (r *Resolver) Asset() AssetResolver { 10 | return &assetResolver{r} 11 | } 12 | 13 | type assetResolver struct{ *Resolver } 14 | 15 | func (r *assetResolver) Team(ctx context.Context, obj *gqlmodel.Asset) (*gqlmodel.Team, error) { 16 | return dataloaders(ctx).Team.Load(obj.TeamID) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/tag/group.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | type Group struct { 4 | tag 5 | tags IDList 6 | } 7 | 8 | func (g *Group) Tags() IDList { 9 | if g == nil { 10 | return nil 11 | } 12 | return g.tags.Clone() 13 | } 14 | 15 | func (g *Group) RemoveTag(ids ...ID) { 16 | if g == nil { 17 | return 18 | } 19 | g.tags = g.tags.Delete(ids...) 20 | } 21 | 22 | func (g *Group) AddTag(ids ...ID) { 23 | if g == nil { 24 | return 25 | } 26 | g.tags = g.tags.Add(ids...) 27 | } 28 | -------------------------------------------------------------------------------- /internal/usecase/gateway/authenticator.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | type AuthenticatorUpdateUserParam struct { 4 | ID string 5 | Name *string 6 | Email *string 7 | Password *string 8 | } 9 | 10 | type AuthenticatorUser struct { 11 | ID string 12 | Name string 13 | Email string 14 | EmailVerified bool 15 | } 16 | 17 | type Authenticator interface { 18 | UpdateUser(AuthenticatorUpdateUserParam) (AuthenticatorUser, error) 19 | } 20 | -------------------------------------------------------------------------------- /internal/usecase/gateway/plugin_registry.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/reearth/reearth-backend/pkg/id" 8 | "github.com/reearth/reearth-backend/pkg/plugin/pluginpack" 9 | ) 10 | 11 | var ErrFailedToFetchDataFromPluginRegistry = errors.New("failed to fetch data from the plugin registry") 12 | 13 | type PluginRegistry interface { 14 | FetchPluginPackage(context.Context, id.PluginID) (*pluginpack.Package, error) 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .DS_Store 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Editor 16 | .idea 17 | 18 | # reearth 19 | /dist 20 | /reearth 21 | /reearth-backend 22 | __debug_bin 23 | /data 24 | /bin 25 | /debug 26 | /mongo 27 | /.env* 28 | !/.env.example 29 | /coverage.txt 30 | /web 31 | -------------------------------------------------------------------------------- /internal/usecase/gateway/datasouce.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/reearth/reearth-backend/pkg/dataset" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | ) 10 | 11 | var ( 12 | ErrDataSourceInvalidURL error = errors.New("invalid url") 13 | ) 14 | 15 | type DataSource interface { 16 | Fetch(context.Context, string, id.SceneID) ([]*dataset.Schema, []*dataset.Dataset, error) 17 | IsURLValid(context.Context, string) bool 18 | } 19 | -------------------------------------------------------------------------------- /internal/usecase/repo/scene_lock.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/scene" 8 | ) 9 | 10 | type SceneLock interface { 11 | GetLock(context.Context, id.SceneID) (scene.LockMode, error) 12 | GetAllLock(context.Context, id.SceneIDList) ([]scene.LockMode, error) 13 | SaveLock(context.Context, id.SceneID, scene.LockMode) error 14 | ReleaseAllLock(context.Context) error 15 | } 16 | -------------------------------------------------------------------------------- /pkg/user/team.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | type Team struct { 4 | id TeamID 5 | name string 6 | members *Members 7 | } 8 | 9 | func (t *Team) ID() TeamID { 10 | return t.id 11 | } 12 | 13 | func (t *Team) Name() string { 14 | return t.name 15 | } 16 | 17 | func (t *Team) Members() *Members { 18 | return t.members 19 | } 20 | 21 | func (t *Team) IsPersonal() bool { 22 | return t.members.Fixed() 23 | } 24 | 25 | func (t *Team) Rename(name string) { 26 | t.name = name 27 | } 28 | -------------------------------------------------------------------------------- /pkg/asset/id.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/pkg/id" 5 | ) 6 | 7 | type ID = id.AssetID 8 | type TeamID = id.TeamID 9 | 10 | var NewID = id.NewAssetID 11 | var NewTeamID = id.NewTeamID 12 | 13 | var MustID = id.MustAssetID 14 | var MustTeamID = id.MustTeamID 15 | 16 | var IDFrom = id.AssetIDFrom 17 | var TeamIDFrom = id.TeamIDFrom 18 | 19 | var IDFromRef = id.AssetIDFromRef 20 | var TeamIDFromRef = id.TeamIDFromRef 21 | 22 | var ErrInvalidID = id.ErrInvalidID 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "golang.go", 7 | "redhat.vscode-yaml" 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [] 11 | } 12 | -------------------------------------------------------------------------------- /pkg/project/id.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/pkg/id" 5 | ) 6 | 7 | type ID = id.ProjectID 8 | type TeamID = id.TeamID 9 | 10 | var NewID = id.NewProjectID 11 | var NewTeamID = id.NewTeamID 12 | 13 | var MustID = id.MustProjectID 14 | var MustTeamID = id.MustTeamID 15 | 16 | var IDFrom = id.ProjectIDFrom 17 | var TeamIDFrom = id.TeamIDFrom 18 | 19 | var IDFromRef = id.ProjectIDFromRef 20 | var TeamIDFromRef = id.TeamIDFromRef 21 | 22 | var ErrInvalidID = id.ErrInvalidID 23 | -------------------------------------------------------------------------------- /pkg/user/id.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "github.com/reearth/reearth-backend/pkg/id" 4 | 5 | type ID = id.UserID 6 | type TeamID = id.TeamID 7 | 8 | var NewID = id.NewUserID 9 | var NewTeamID = id.NewTeamID 10 | 11 | var MustID = id.MustUserID 12 | var MustTeamID = id.MustTeamID 13 | 14 | var IDFrom = id.UserIDFrom 15 | var TeamIDFrom = id.TeamIDFrom 16 | 17 | var IDFromRef = id.UserIDFromRef 18 | var TeamIDFromRef = id.TeamIDFromRef 19 | 20 | var ErrInvalidID = id.ErrInvalidID 21 | 22 | type TeamIDList = id.TeamIDList 23 | -------------------------------------------------------------------------------- /internal/infrastructure/mailer/logger.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase/gateway" 7 | ) 8 | 9 | const loggerSep = "=======================" 10 | 11 | type logger struct{} 12 | 13 | func NewLogger() gateway.Mailer { 14 | return &logger{} 15 | } 16 | 17 | func (m *logger) SendMail(to []gateway.Contact, subject, plainContent, _ string) error { 18 | logMail(to, subject) 19 | fmt.Printf("%s\n%s\n%s\n", loggerSep, plainContent, loggerSep) 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/transaction.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/internal/infrastructure/mongo/mongodoc" 5 | "github.com/reearth/reearth-backend/internal/usecase/repo" 6 | ) 7 | 8 | type Transaction struct { 9 | client *mongodoc.Client 10 | } 11 | 12 | func NewTransaction(client *mongodoc.Client) repo.Transaction { 13 | return &Transaction{ 14 | client: client, 15 | } 16 | } 17 | 18 | func (t *Transaction) Begin() (repo.Tx, error) { 19 | return t.client.BeginTransaction() 20 | } 21 | -------------------------------------------------------------------------------- /internal/usecase/repo/auth_request.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/auth" 7 | "github.com/reearth/reearth-backend/pkg/id" 8 | ) 9 | 10 | type AuthRequest interface { 11 | FindByID(context.Context, id.AuthRequestID) (*auth.Request, error) 12 | FindByCode(context.Context, string) (*auth.Request, error) 13 | FindBySubject(context.Context, string) (*auth.Request, error) 14 | Save(context.Context, *auth.Request) error 15 | Remove(context.Context, id.AuthRequestID) error 16 | } 17 | -------------------------------------------------------------------------------- /pkg/layer/encoding/common_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetColor(t *testing.T) { 11 | assert.Equal(t, &color.RGBA{R: 255, G: 255, B: 255, A: 255}, getColor("#ffffff")) 12 | assert.Equal(t, &color.RGBA{R: 255, G: 255, B: 255, A: 255}, getColor("#fff")) 13 | assert.Equal(t, &color.RGBA{R: 255, G: 255, B: 255, A: 170}, getColor("#fffa")) 14 | assert.Equal(t, &color.RGBA{R: 255, G: 0, B: 0, A: 170}, getColor("#ff0000aa")) 15 | } 16 | -------------------------------------------------------------------------------- /internal/usecase/pagination.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | type Cursor string 4 | 5 | type Pagination struct { 6 | Before *Cursor 7 | After *Cursor 8 | First *int 9 | Last *int 10 | } 11 | 12 | func NewPagination(first *int, last *int, before *Cursor, after *Cursor) *Pagination { 13 | // Relay-Style Cursor Pagination 14 | // ref: https://www.apollographql.com/docs/react/features/pagination/#relay-style-cursor-pagination 15 | return &Pagination{ 16 | Before: before, 17 | After: after, 18 | First: first, 19 | Last: last, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/plugin/manifest/testdata/test.yml: -------------------------------------------------------------------------------- 1 | id: aaa 2 | name: bbb 3 | version: 1.1.1 4 | extensions: 5 | - id: hoge 6 | type: primitive 7 | visualizer: cesium 8 | schema: 9 | groups: 10 | - id: default 11 | representativeField: a 12 | fields: 13 | - id: a 14 | type: bool 15 | defaultValue: true 16 | availableIf: 17 | field: b 18 | type: number 19 | value: 1 20 | - id: b 21 | type: number 22 | -------------------------------------------------------------------------------- /internal/usecase/repo/transaction.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "context" 4 | 5 | type Transaction interface { 6 | Begin() (Tx, error) 7 | } 8 | 9 | type Tx interface { 10 | // Commit informs Tx to commit when End() is called. 11 | // If this was not called once, rollback is done when End() is called. 12 | Commit() 13 | // End finishes the transaction and do commit if Commit() was called once, or else do rollback. 14 | // This method is supposed to be called in the uscase layer using defer. 15 | End(context.Context) error 16 | IsCommitted() bool 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | reearth-backend: 4 | build: 5 | context: . 6 | image: reearth/reearth-backend:nightly 7 | environment: 8 | REEARTH_DB: mongodb://reearth-mongo 9 | ports: 10 | - '8080:8080' 11 | # env_file: 12 | # - ./.env 13 | links: 14 | - reearth-mongo 15 | depends_on: 16 | - reearth-mongo 17 | volumes: 18 | - ./data:/reearth/data 19 | reearth-mongo: 20 | image: mongo:4.4-focal 21 | ports: 22 | - 27017:27017 23 | volumes: 24 | - ./mongo:/data/db 25 | -------------------------------------------------------------------------------- /internal/app/profiler.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "cloud.google.com/go/profiler" 5 | "github.com/reearth/reearth-backend/pkg/log" 6 | ) 7 | 8 | func initProfiler(kind string, version string) { 9 | if kind == "" { 10 | return 11 | } 12 | 13 | if kind == "gcp" { 14 | initGCPProfiler(version) 15 | } 16 | 17 | log.Infof("profiler: %s initialized\n", kind) 18 | } 19 | 20 | func initGCPProfiler(version string) { 21 | if err := profiler.Start(profiler.Config{ 22 | Service: "reearth-backend", 23 | ServiceVersion: version, 24 | }); err != nil { 25 | log.Fatalln(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver_user.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel" 7 | ) 8 | 9 | func (r *Resolver) Me() MeResolver { 10 | return &meResolver{r} 11 | } 12 | 13 | type meResolver struct{ *Resolver } 14 | 15 | func (r *meResolver) MyTeam(ctx context.Context, obj *gqlmodel.Me) (*gqlmodel.Team, error) { 16 | return dataloaders(ctx).Team.Load(obj.MyTeamID) 17 | } 18 | 19 | func (r *meResolver) Teams(ctx context.Context, obj *gqlmodel.Me) ([]*gqlmodel.Team, error) { 20 | return loaders(ctx).Team.FindByUser(ctx, obj.ID) 21 | } 22 | -------------------------------------------------------------------------------- /internal/usecase/repo/team.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/user" 8 | ) 9 | 10 | type Team interface { 11 | FindByUser(context.Context, id.UserID) (user.TeamList, error) 12 | FindByIDs(context.Context, id.TeamIDList) (user.TeamList, error) 13 | FindByID(context.Context, id.TeamID) (*user.Team, error) 14 | Save(context.Context, *user.Team) error 15 | SaveAll(context.Context, []*user.Team) error 16 | Remove(context.Context, id.TeamID) error 17 | RemoveAll(context.Context, id.TeamIDList) error 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/renovate.yml: -------------------------------------------------------------------------------- 1 | name: renovate 2 | on: 3 | push: 4 | branches: 5 | - renovate/* 6 | jobs: 7 | renovate-go-sum-fix: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 2 14 | - name: fix 15 | uses: at-wat/go-sum-fix-action@v0 16 | with: 17 | git_user: ${{ github.actor }} 18 | git_email: ${{ github.actor }}@users.noreply.github.com 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | commit_style: squash 21 | push: force 22 | -------------------------------------------------------------------------------- /internal/usecase/repo/scene.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/scene" 8 | ) 9 | 10 | type Scene interface { 11 | Filtered(TeamFilter) Scene 12 | FindByID(context.Context, id.SceneID) (*scene.Scene, error) 13 | FindByIDs(context.Context, id.SceneIDList) (scene.List, error) 14 | FindByTeam(context.Context, ...id.TeamID) (scene.List, error) 15 | FindByProject(context.Context, id.ProjectID) (*scene.Scene, error) 16 | Save(context.Context, *scene.Scene) error 17 | Remove(context.Context, id.SceneID) error 18 | } 19 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "sort" 4 | 5 | type Config struct { 6 | Migration int64 7 | Auth *Auth 8 | } 9 | 10 | type Auth struct { 11 | Cert string 12 | Key string 13 | } 14 | 15 | func (c *Config) NextMigrations(migrations []int64) []int64 { 16 | migrations2 := append([]int64{}, migrations...) 17 | sort.SliceStable(migrations2, func(i, j int) bool { return migrations2[i] < migrations2[j] }) 18 | 19 | for i, m := range migrations2 { 20 | if len(migrations2) <= i { 21 | return nil 22 | } 23 | if c.Migration < m { 24 | return migrations2[i:] 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/scene/plugin_test.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin(t *testing.T) { 10 | pid := MustPluginID("xxx~1.1.1") 11 | pr := NewPropertyID().Ref() 12 | 13 | res := NewPlugin(pid, pr) 14 | assert.Equal(t, &Plugin{ 15 | plugin: pid, 16 | property: pr, 17 | }, res) 18 | assert.Equal(t, pid, res.Plugin()) 19 | assert.Equal(t, &pid, res.PluginRef()) 20 | assert.Equal(t, pr, res.Property()) 21 | 22 | cl := res.Clone() 23 | assert.Equal(t, res, cl) 24 | assert.NotSame(t, res, cl) 25 | 26 | assert.Nil(t, (*Plugin)(nil).PluginRef()) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/property/schema_field_ui_test.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSchemaFieldUI(t *testing.T) { 10 | var uir *SchemaFieldUI 11 | assert.Equal(t, SchemaFieldUI(""), SchemaFieldUIFrom("")) 12 | assert.Equal(t, uir, SchemaFieldUIFromRef(nil)) 13 | ui := SchemaFieldUILayer 14 | assert.Equal(t, SchemaFieldUILayer, SchemaFieldUIFrom("layer")) 15 | assert.Equal(t, "layer", SchemaFieldUIFrom("layer").String()) 16 | str := "layer" 17 | assert.Equal(t, &ui, SchemaFieldUIFromRef(&str)) 18 | assert.Equal(t, &str, SchemaFieldUIFromRef(&str).StringRef()) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/user/auth.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Auth struct { 8 | Provider string 9 | Sub string 10 | } 11 | 12 | func AuthFromAuth0Sub(sub string) Auth { 13 | s := strings.SplitN(sub, "|", 2) 14 | if len(s) != 2 { 15 | return Auth{Provider: "", Sub: sub} 16 | } 17 | return Auth{Provider: s[0], Sub: sub} 18 | } 19 | 20 | func (a Auth) IsAuth0() bool { 21 | return a.Provider == "auth0" 22 | } 23 | 24 | func (a Auth) Ref() *Auth { 25 | a2 := a 26 | return &a2 27 | } 28 | 29 | func GenReearthSub(userID string) *Auth { 30 | return &Auth{ 31 | Provider: "reearth", 32 | Sub: "reearth|" + userID, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/id/idx/id_test.go: -------------------------------------------------------------------------------- 1 | package idx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type TID = ID[T] 10 | 11 | // T is a dummy ID type for unit tests 12 | type T struct{} 13 | 14 | func (T) Type() string { return "_" } 15 | 16 | var idstr = mustParseID("01fzxycwmq7n84q8kessktvb8z") 17 | 18 | func TestID_String(t *testing.T) { 19 | assert.Equal(t, "01fzxycwmq7n84q8kessktvb8z", TID{id: idstr}.String()) 20 | assert.Equal(t, "", ID[T]{}.String()) 21 | } 22 | 23 | func TestID_GoString(t *testing.T) { 24 | assert.Equal(t, "_ID(01fzxycwmq7n84q8kessktvb8z)", TID{id: idstr}.GoString()) 25 | assert.Equal(t, "_ID()", TID{}.GoString()) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/property/validator.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type Validator struct { 9 | SchemaLoader SchemaLoader 10 | } 11 | 12 | func (v Validator) Validate(ctx context.Context, properties List) error { 13 | schemaIDs := properties.Schemas() 14 | schemas, err := v.SchemaLoader(ctx, schemaIDs...) 15 | if err != nil { 16 | return err 17 | } 18 | schemaMap := schemas.Map() 19 | 20 | for _, p := range properties { 21 | schema := schemaMap[p.Schema()] 22 | if err := p.ValidateSchema(schema); err != nil { 23 | return fmt.Errorf("invalid property: %s (%s): %w", p.ID(), p.Schema(), err) 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/usecase/repo/plugin.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/plugin" 8 | ) 9 | 10 | type Plugin interface { 11 | Filtered(SceneFilter) Plugin 12 | FindByID(context.Context, id.PluginID) (*plugin.Plugin, error) 13 | FindByIDs(context.Context, []id.PluginID) ([]*plugin.Plugin, error) 14 | Save(context.Context, *plugin.Plugin) error 15 | Remove(context.Context, id.PluginID) error 16 | } 17 | 18 | func PluginLoaderFrom(r Plugin) plugin.Loader { 19 | return func(ctx context.Context, ids []id.PluginID) ([]*plugin.Plugin, error) { 20 | return r.FindByIDs(ctx, ids) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/shp/errreader.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // errReader is a helper to perform multiple successive read from another reader 9 | // and do the error checking only once afterwards. It will not perform any new 10 | // reads in case there was an error encountered earlier. 11 | type errReader struct { 12 | io.Reader 13 | e error 14 | n int64 15 | } 16 | 17 | func (er *errReader) Read(p []byte) (n int, err error) { 18 | if er.e != nil { 19 | return 0, fmt.Errorf("unable to read after previous error: %v", er.e) 20 | } 21 | n, err = er.Reader.Read(p) 22 | if n < len(p) && err != nil { 23 | er.e = err 24 | } 25 | er.n += int64(n) 26 | return n, er.e 27 | } 28 | -------------------------------------------------------------------------------- /pkg/tag/tag_test.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestToTagGroup(t *testing.T) { 10 | tag := Item{} 11 | group := ToTagGroup(&tag) 12 | assert.Nil(t, group) 13 | tag2 := Group{} 14 | group2 := ToTagGroup(&tag2) 15 | assert.NotNil(t, group2) 16 | } 17 | 18 | func TestToTagItem(t *testing.T) { 19 | tag := Group{} 20 | item := ToTagItem(&tag) 21 | assert.Nil(t, item) 22 | tag2 := Item{} 23 | item2 := ToTagItem(&tag2) 24 | assert.NotNil(t, item2) 25 | } 26 | 27 | func TestTag_Rename(t *testing.T) { 28 | tt := tag{ 29 | label: "xxx", 30 | } 31 | tt.Rename("changed") 32 | assert.Equal(t, "changed", tt.Label()) 33 | } 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine AS build 2 | ARG TAG=release 3 | ARG REV 4 | ARG VERSION 5 | 6 | RUN apk add --update --no-cache git ca-certificates build-base 7 | 8 | COPY go.mod go.sum main.go /reearth/ 9 | WORKDIR /reearth 10 | RUN go mod download 11 | 12 | COPY cmd/ /reearth/cmd/ 13 | COPY pkg/ /reearth/pkg/ 14 | COPY internal/ /reearth/internal/ 15 | 16 | RUN CGO_ENABLED=0 go build -tags "${TAG}" "-ldflags=-X main.version=${VERSION} -s -w -buildid=" -trimpath ./cmd/reearth 17 | 18 | FROM scratch 19 | 20 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 21 | COPY --from=build /reearth/reearth /reearth/reearth 22 | 23 | WORKDIR /reearth 24 | 25 | CMD [ "./reearth" ] 26 | -------------------------------------------------------------------------------- /internal/infrastructure/memory/container.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/internal/usecase/repo" 5 | ) 6 | 7 | func New() *repo.Container { 8 | c := &repo.Container{} 9 | c.Asset = NewAsset() 10 | c.Config = NewConfig() 11 | c.DatasetSchema = NewDatasetSchema() 12 | c.Dataset = NewDataset() 13 | c.Layer = NewLayer() 14 | c.Plugin = NewPlugin() 15 | c.Project = NewProject() 16 | c.PropertySchema = NewPropertySchema() 17 | c.Property = NewProperty() 18 | c.Scene = NewScene() 19 | c.Tag = NewTag() 20 | c.Team = NewTeam() 21 | c.User = NewUser() 22 | c.SceneLock = NewSceneLock() 23 | c.Transaction = NewTransaction() 24 | c.Lock = NewLock() 25 | return c 26 | } 27 | -------------------------------------------------------------------------------- /pkg/property/item_builder.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | type ItemBuilder struct { 4 | base itemBase 5 | } 6 | 7 | func NewItem() *ItemBuilder { 8 | return &ItemBuilder{} 9 | } 10 | 11 | func (b *ItemBuilder) Group() *GroupBuilder { 12 | return NewGroup().base(b.base) 13 | } 14 | 15 | func (b *ItemBuilder) GroupList() *GroupListBuilder { 16 | return NewGroupList().base(b.base) 17 | } 18 | 19 | func (b *ItemBuilder) ID(id ItemID) *ItemBuilder { 20 | b.base.ID = id 21 | return b 22 | } 23 | 24 | func (b *ItemBuilder) NewID() *ItemBuilder { 25 | b.base.ID = NewItemID() 26 | return b 27 | } 28 | 29 | func (b *ItemBuilder) SchemaGroup(g SchemaGroupID) *ItemBuilder { 30 | b.base.SchemaGroup = g 31 | return b 32 | } 33 | -------------------------------------------------------------------------------- /pkg/id/idx/string.go: -------------------------------------------------------------------------------- 1 | package idx 2 | 3 | type StringID[T Type] string 4 | 5 | func StringIDFromRef[T Type](id *string) *StringID[T] { 6 | if id == nil { 7 | return nil 8 | } 9 | id2 := StringID[T](*id) 10 | return &id2 11 | } 12 | 13 | func (id StringID[T]) Ref() *StringID[T] { 14 | if id == "" { 15 | return nil 16 | } 17 | return &id 18 | } 19 | 20 | func (id *StringID[T]) CloneRef() *StringID[T] { 21 | if id == nil { 22 | return nil 23 | } 24 | id2 := *id 25 | return &id2 26 | } 27 | 28 | func (id StringID[_]) String() string { 29 | return string(id) 30 | } 31 | 32 | func (id *StringID[_]) StringRef() *string { 33 | if id == nil { 34 | return nil 35 | } 36 | id2 := string(*id) 37 | return &id2 38 | } 39 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/migration/migrations.go: -------------------------------------------------------------------------------- 1 | // Code generated by migrategen, DO NOT EDIT. 2 | 3 | package migration 4 | 5 | // To add a new migration, run go run ./tools/cmd/migrategen migration_name 6 | 7 | // WARNING: 8 | // If the migration takes too long, the deployment may fail in a serverless environment. 9 | // Set the batch size to as large a value as possible without using up the RAM of the deployment destination. 10 | var migrations = map[int64]MigrationFunc{ 11 | 201217132559: AddSceneWidgetId, 12 | 201217193948: AddSceneDefaultTile, 13 | 210310145844: RemovePreviewToken, 14 | 210730175108: AddSceneAlignSystem, 15 | 220214180713: SplitSchemaOfProperties, 16 | 220309174648: AddSceneFieldToPropertySchema, 17 | } 18 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: reearth-backend 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - main: ./cmd/reearth 7 | flags: 8 | - -tags=release 9 | - -trimpath 10 | ldflags: 11 | - -s -w 12 | - -X main.version={{.Version}} 13 | - -buildid= 14 | env: 15 | - CGO_ENABLED=0 16 | archives: 17 | - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 18 | replacements: 19 | darwin: darwin 20 | linux: linux 21 | windows: windows 22 | 386: i386 23 | amd64: x86_64 24 | format_overrides: 25 | - goos: windows 26 | format: zip 27 | changelog: 28 | skip: true 29 | release: 30 | disable: true 31 | -------------------------------------------------------------------------------- /pkg/asset/sort_type.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | SortTypeID = SortType("id") 10 | SortTypeName = SortType("name") 11 | SortTypeSize = SortType("size") 12 | 13 | ErrInvalidSortType = errors.New("invalid sort type") 14 | ) 15 | 16 | type SortType string 17 | 18 | func check(role SortType) bool { 19 | switch role { 20 | case SortTypeID: 21 | return true 22 | case SortTypeName: 23 | return true 24 | case SortTypeSize: 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | func SortTypeFromString(r string) (SortType, error) { 31 | role := SortType(strings.ToLower(r)) 32 | 33 | if check(role) { 34 | return role, nil 35 | } 36 | return role, ErrInvalidSortType 37 | } 38 | -------------------------------------------------------------------------------- /pkg/layer/encoding/encoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/reearth/reearth-backend/pkg/layer/merging" 7 | ) 8 | 9 | var encoders = map[string]func(w io.Writer) Encoder{ 10 | "kml": func(w io.Writer) Encoder { return NewKMLEncoder(w) }, 11 | "geojson": func(w io.Writer) Encoder { return NewGeoJSONEncoder(w) }, 12 | "czml": func(w io.Writer) Encoder { return NewCZMLEncoder(w) }, 13 | "shp": func(w io.Writer) Encoder { return NewSHPEncoder(w) }, 14 | } 15 | 16 | type Encoder interface { 17 | Encode(merging.SealedLayer) error 18 | MimeType() string 19 | } 20 | 21 | func EncoderFromExt(ext string, w io.Writer) Encoder { 22 | e := encoders[ext] 23 | if e == nil { 24 | return nil 25 | } 26 | return e(w) 27 | } 28 | -------------------------------------------------------------------------------- /internal/usecase/interfaces/common.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "errors" 4 | 5 | type ListOperation string 6 | 7 | const ( 8 | ListOperationAdd ListOperation = "add" 9 | ListOperationMove ListOperation = "move" 10 | ListOperationRemove ListOperation = "remove" 11 | ) 12 | 13 | var ( 14 | ErrSceneIsLocked error = errors.New("scene is locked") 15 | ErrOperationDenied error = errors.New("operation denied") 16 | ErrFileNotIncluded error = errors.New("file not included") 17 | ) 18 | 19 | type Container struct { 20 | Asset Asset 21 | Dataset Dataset 22 | Layer Layer 23 | Plugin Plugin 24 | Project Project 25 | Property Property 26 | Published Published 27 | Scene Scene 28 | Tag Tag 29 | Team Team 30 | User User 31 | } 32 | -------------------------------------------------------------------------------- /internal/usecase/repo/asset.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase" 7 | "github.com/reearth/reearth-backend/pkg/asset" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | ) 10 | 11 | type AssetFilter struct { 12 | Sort *asset.SortType 13 | Keyword *string 14 | Pagination *usecase.Pagination 15 | } 16 | 17 | type Asset interface { 18 | Filtered(TeamFilter) Asset 19 | FindByTeam(context.Context, id.TeamID, AssetFilter) ([]*asset.Asset, *usecase.PageInfo, error) 20 | FindByID(context.Context, id.AssetID) (*asset.Asset, error) 21 | FindByIDs(context.Context, id.AssetIDList) ([]*asset.Asset, error) 22 | Save(context.Context, *asset.Asset) error 23 | Remove(context.Context, id.AssetID) error 24 | } 25 | -------------------------------------------------------------------------------- /pkg/value/latlng_test.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLatLng_Clone(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | LL, Expected *LatLng 13 | }{ 14 | { 15 | Name: "nil latlng", 16 | }, 17 | { 18 | Name: "cloned", 19 | LL: &LatLng{ 20 | Lat: 10, 21 | Lng: 11, 22 | }, 23 | Expected: &LatLng{ 24 | Lat: 10, 25 | Lng: 11, 26 | }, 27 | }, 28 | } 29 | 30 | for _, tc := range tests { 31 | tc := tc 32 | t.Run(tc.Name, func(t *testing.T) { 33 | t.Parallel() 34 | res := tc.LL.Clone() 35 | assert.Equal(t, tc.Expected, res) 36 | if tc.Expected != nil { 37 | assert.NotSame(t, tc.Expected, res) 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/stage.yml: -------------------------------------------------------------------------------- 1 | name: Stage 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | stage: 6 | runs-on: ubuntu-latest 7 | if: github.ref == 'refs/heads/main' 8 | steps: 9 | - name: git config 10 | env: 11 | GPT_USER: ${{ secrets.GPT_USER }} 12 | run: | 13 | git config --global user.name $GPT_USER 14 | git config --global pull.rebase false 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | token: ${{ secrets.GPT }} 19 | - name: Checkout release branch 20 | run: git switch release || git switch -c release 21 | - name: Merge main branch to release branch 22 | run: git merge -X theirs main 23 | - name: Git push 24 | run: git push origin release 25 | -------------------------------------------------------------------------------- /.github/workflows/pr_title.yml: -------------------------------------------------------------------------------- 1 | name: PR Title Checker 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | - labeled 9 | - unlabeled 10 | jobs: 11 | pr_title: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: amannn/action-semantic-pull-request@v4 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | ignoreLabels: meta 18 | scopes: | 19 | web 20 | server 21 | subjectPattern: ^(?![A-Z]).+$ 22 | subjectPatternError: | 23 | The subject "{subject}" found in the pull request title "{title}" 24 | didn't match the configured pattern. Please ensure that the subject 25 | doesn't start with an uppercase character. 26 | -------------------------------------------------------------------------------- /pkg/id/idx/string_test.go: -------------------------------------------------------------------------------- 1 | package idx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/samber/lo" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStringID_Ref(t *testing.T) { 11 | assert.Equal(t, lo.ToPtr(StringID[T]("a")), StringID[T]("a").Ref()) 12 | } 13 | 14 | func TestStringID_CloneRef(t *testing.T) { 15 | s := lo.ToPtr(StringID[T]("a")) 16 | res := s.CloneRef() 17 | assert.Equal(t, s, res) 18 | assert.NotSame(t, s, res) 19 | assert.Nil(t, (*StringID[T])(nil).CloneRef()) 20 | } 21 | 22 | func TestStringID_String(t *testing.T) { 23 | assert.Equal(t, "a", StringID[T]("a").String()) 24 | } 25 | 26 | func TestStringID_StringRef(t *testing.T) { 27 | assert.Equal(t, lo.ToPtr("a"), lo.ToPtr(StringID[T]("a")).StringRef()) 28 | assert.Nil(t, (*StringID[T])(nil).StringRef()) 29 | } 30 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver_project.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel" 7 | "github.com/reearth/reearth-backend/pkg/rerror" 8 | ) 9 | 10 | func (r *Resolver) Project() ProjectResolver { 11 | return &projectResolver{r} 12 | } 13 | 14 | type projectResolver struct{ *Resolver } 15 | 16 | func (r *projectResolver) Team(ctx context.Context, obj *gqlmodel.Project) (*gqlmodel.Team, error) { 17 | return dataloaders(ctx).Team.Load(obj.TeamID) 18 | } 19 | 20 | func (r *projectResolver) Scene(ctx context.Context, obj *gqlmodel.Project) (*gqlmodel.Scene, error) { 21 | s, err := loaders(ctx).Scene.FindByProject(ctx, obj.ID) 22 | if err != nil && err != rerror.ErrNotFound { 23 | return nil, err 24 | } 25 | return s, nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/plugin/manifest/manifest.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/pkg/plugin" 5 | "github.com/reearth/reearth-backend/pkg/property" 6 | ) 7 | 8 | type Manifest struct { 9 | Plugin *plugin.Plugin 10 | ExtensionSchema property.SchemaList 11 | Schema *property.Schema 12 | } 13 | 14 | func (m Manifest) PropertySchemas() property.SchemaList { 15 | sl := append(property.SchemaList{}, m.ExtensionSchema...) 16 | if m.Schema != nil { 17 | sl = append(sl, m.Schema) 18 | } 19 | return sl 20 | } 21 | 22 | func (m Manifest) PropertySchema(psid property.SchemaID) *property.Schema { 23 | if psid.IsNil() { 24 | return nil 25 | } 26 | if m.Schema != nil && psid.Equal(m.Schema.ID()) { 27 | return m.Schema 28 | } 29 | return m.ExtensionSchema.Find(psid) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/property/schema_pointer_test.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSchemaFieldPointer_Pointer(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | target *SchemaFieldPointer 13 | want *Pointer 14 | }{ 15 | { 16 | name: "ok", 17 | target: &SchemaFieldPointer{ 18 | SchemaGroup: SchemaGroupID("a"), 19 | Field: FieldID("b"), 20 | }, 21 | want: &Pointer{ 22 | schemaGroup: SchemaGroupID("a").Ref(), 23 | item: nil, 24 | field: FieldID("b").Ref(), 25 | }, 26 | }, 27 | } 28 | 29 | for _, tt := range tests { 30 | tt := tt 31 | t.Run(tt.name, func(t *testing.T) { 32 | t.Parallel() 33 | assert.Equal(t, tt.want, tt.target.Pointer()) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/infrastructure/gcs/file_test.go: -------------------------------------------------------------------------------- 1 | package gcs 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetGCSObjectURL(t *testing.T) { 11 | e, _ := url.Parse("https://hoge.com/assets/xxx.yyy") 12 | b, _ := url.Parse("https://hoge.com/assets") 13 | assert.Equal(t, e, getGCSObjectURL(b, "xxx.yyy")) 14 | } 15 | 16 | func TestGetGCSObjectNameFromURL(t *testing.T) { 17 | u, _ := url.Parse("https://hoge.com/assets/xxx.yyy") 18 | b, _ := url.Parse("https://hoge.com") 19 | b2, _ := url.Parse("https://hoge2.com") 20 | assert.Equal(t, "assets/xxx.yyy", getGCSObjectNameFromURL(b, u)) 21 | assert.Equal(t, "", getGCSObjectNameFromURL(b2, u)) 22 | assert.Equal(t, "", getGCSObjectNameFromURL(nil, u)) 23 | assert.Equal(t, "", getGCSObjectNameFromURL(b, nil)) 24 | } 25 | -------------------------------------------------------------------------------- /internal/usecase/repo/user.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/user" 8 | ) 9 | 10 | type User interface { 11 | FindByIDs(context.Context, id.UserIDList) ([]*user.User, error) 12 | FindByID(context.Context, id.UserID) (*user.User, error) 13 | FindByAuth0Sub(context.Context, string) (*user.User, error) 14 | FindByEmail(context.Context, string) (*user.User, error) 15 | FindByName(context.Context, string) (*user.User, error) 16 | FindByNameOrEmail(context.Context, string) (*user.User, error) 17 | FindByVerification(context.Context, string) (*user.User, error) 18 | FindByPasswordResetRequest(context.Context, string) (*user.User, error) 19 | Save(context.Context, *user.User) error 20 | Remove(context.Context, id.UserID) error 21 | } 22 | -------------------------------------------------------------------------------- /pkg/property/condition_test.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCondition_Clone(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | Con, Expected *Condition 13 | }{ 14 | { 15 | Name: "nil condition", 16 | Con: nil, 17 | Expected: nil, 18 | }, 19 | { 20 | Name: "nil condition", 21 | Con: &Condition{ 22 | Field: "a", 23 | Value: ValueTypeBool.ValueFrom(true), 24 | }, 25 | Expected: &Condition{ 26 | Field: "a", 27 | Value: ValueTypeBool.ValueFrom(true), 28 | }, 29 | }, 30 | } 31 | 32 | for _, tt := range tests { 33 | tt := tt 34 | t.Run(tt.Name, func(t *testing.T) { 35 | t.Parallel() 36 | res := tt.Con.Clone() 37 | assert.Equal(t, tt.Expected, res) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/adapter/gql/gqlmodel/convert_asset.go: -------------------------------------------------------------------------------- 1 | package gqlmodel 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/pkg/asset" 5 | ) 6 | 7 | func ToAsset(a *asset.Asset) *Asset { 8 | if a == nil { 9 | return nil 10 | } 11 | 12 | return &Asset{ 13 | ID: IDFrom(a.ID()), 14 | CreatedAt: a.CreatedAt(), 15 | TeamID: IDFrom(a.Team()), 16 | Name: a.Name(), 17 | Size: a.Size(), 18 | URL: a.URL(), 19 | ContentType: a.ContentType(), 20 | } 21 | } 22 | 23 | func AssetSortTypeFrom(ast *AssetSortType) *asset.SortType { 24 | if ast == nil { 25 | return nil 26 | } 27 | 28 | switch *ast { 29 | case AssetSortTypeDate: 30 | return &asset.SortTypeID 31 | case AssetSortTypeName: 32 | return &asset.SortTypeName 33 | case AssetSortTypeSize: 34 | return &asset.SortTypeSize 35 | } 36 | return &asset.SortTypeID 37 | } 38 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/mongodoc/config.go: -------------------------------------------------------------------------------- 1 | package mongodoc 2 | 3 | import "github.com/reearth/reearth-backend/pkg/config" 4 | 5 | type ConfigDocument struct { 6 | Migration int64 7 | Auth *Auth 8 | } 9 | 10 | type Auth struct { 11 | Cert string 12 | Key string 13 | } 14 | 15 | func NewConfig(c config.Config) ConfigDocument { 16 | d := ConfigDocument{ 17 | Migration: c.Migration, 18 | } 19 | if c.Auth != nil { 20 | d.Auth = &Auth{ 21 | Cert: c.Auth.Cert, 22 | Key: c.Auth.Key, 23 | } 24 | } 25 | return d 26 | } 27 | 28 | func (c *ConfigDocument) Model() *config.Config { 29 | if c == nil { 30 | return &config.Config{} 31 | } 32 | m := &config.Config{ 33 | Migration: c.Migration, 34 | } 35 | if c.Auth != nil { 36 | m.Auth = &config.Auth{ 37 | Cert: c.Auth.Cert, 38 | Key: c.Auth.Key, 39 | } 40 | } 41 | return m 42 | } 43 | -------------------------------------------------------------------------------- /internal/usecase/interfaces/plugin.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net/url" 8 | 9 | "github.com/reearth/reearth-backend/internal/usecase" 10 | "github.com/reearth/reearth-backend/pkg/id" 11 | "github.com/reearth/reearth-backend/pkg/plugin" 12 | "github.com/reearth/reearth-backend/pkg/scene" 13 | ) 14 | 15 | var ( 16 | ErrPluginAlreadyRegistered = errors.New("plugin already registered") 17 | ErrInvalidPluginPackage = errors.New("invalid plugin package") 18 | ) 19 | 20 | type Plugin interface { 21 | Fetch(context.Context, []id.PluginID, *usecase.Operator) ([]*plugin.Plugin, error) 22 | Upload(context.Context, io.Reader, id.SceneID, *usecase.Operator) (*plugin.Plugin, *scene.Scene, error) 23 | UploadFromRemote(context.Context, *url.URL, id.SceneID, *usecase.Operator) (*plugin.Plugin, *scene.Scene, error) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/plugin/id.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "github.com/reearth/reearth-backend/pkg/id" 4 | 5 | type ID = id.PluginID 6 | type ExtensionID = id.PluginExtensionID 7 | type PropertySchemaID = id.PropertySchemaID 8 | type SceneID = id.SceneID 9 | 10 | var NewID = id.NewPluginID 11 | var NewSceneID = id.NewSceneID 12 | var NewPropertySchemaID = id.NewPropertySchemaID 13 | 14 | var MustID = id.MustPluginID 15 | var MustSceneID = id.MustSceneID 16 | var MustPropertySchemaID = id.MustPropertySchemaID 17 | 18 | var IDFrom = id.PluginIDFrom 19 | var SceneIDFrom = id.SceneIDFrom 20 | var PropertySchemaIDFrom = id.PropertySchemaIDFrom 21 | 22 | var IDFromRef = id.PluginIDFromRef 23 | var SceneIDFromRef = id.SceneIDFromRef 24 | var PropertySchemaIDFromRef = id.PropertySchemaIDFromRef 25 | 26 | var OfficialPluginID = id.OfficialPluginID 27 | var ErrInvalidID = id.ErrInvalidID 28 | -------------------------------------------------------------------------------- /pkg/value/latlngheight_test.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLatLngHeight_Clone(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | LL, Expected *LatLngHeight 13 | }{ 14 | { 15 | Name: "nil LatLngHeight", 16 | }, 17 | { 18 | Name: "cloned", 19 | LL: &LatLngHeight{ 20 | Lat: 10, 21 | Lng: 11, 22 | Height: 12, 23 | }, 24 | Expected: &LatLngHeight{ 25 | Lat: 10, 26 | Lng: 11, 27 | Height: 12, 28 | }, 29 | }, 30 | } 31 | 32 | for _, tc := range tests { 33 | tc := tc 34 | t.Run(tc.Name, func(t *testing.T) { 35 | t.Parallel() 36 | res := tc.LL.Clone() 37 | assert.Equal(t, tc.Expected, res) 38 | if tc.Expected != nil { 39 | assert.NotSame(t, tc.Expected, res) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/value/ref.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "fmt" 4 | 5 | var TypeRef Type = "ref" 6 | 7 | type propertyRef struct{} 8 | 9 | func (*propertyRef) I2V(i interface{}) (interface{}, bool) { 10 | if v, ok := i.(string); ok { 11 | return v, true 12 | } 13 | if v, ok := i.(*string); ok { 14 | return *v, true 15 | } 16 | if v, ok := i.(fmt.Stringer); ok { 17 | return v.String(), true 18 | } 19 | if v, ok := i.(*fmt.Stringer); ok && v != nil { 20 | return (*v).String(), true 21 | } 22 | return nil, false 23 | } 24 | 25 | func (*propertyRef) V2I(v interface{}) (interface{}, bool) { 26 | return v, true 27 | } 28 | 29 | func (*propertyRef) Validate(i interface{}) bool { 30 | _, ok := i.(string) 31 | return ok 32 | } 33 | 34 | func (v *Value) ValueRef() (vv string, ok bool) { 35 | if v == nil { 36 | return 37 | } 38 | vv, ok = v.v.(string) 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /internal/infrastructure/mailer/mock.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase/gateway" 7 | ) 8 | 9 | type Mock struct { 10 | lock sync.Mutex 11 | mails []Mail 12 | } 13 | 14 | type Mail struct { 15 | To []gateway.Contact 16 | Subject string 17 | PlainContent string 18 | HTMLContent string 19 | } 20 | 21 | func NewMock() *Mock { 22 | return &Mock{} 23 | } 24 | 25 | func (m *Mock) SendMail(to []gateway.Contact, subject, text, html string) error { 26 | m.lock.Lock() 27 | defer m.lock.Unlock() 28 | m.mails = append(m.mails, Mail{ 29 | To: append([]gateway.Contact{}, to...), 30 | Subject: subject, 31 | PlainContent: text, 32 | HTMLContent: html, 33 | }) 34 | return nil 35 | } 36 | 37 | func (m *Mock) Mails() []Mail { 38 | return append([]Mail{}, m.mails...) 39 | } 40 | -------------------------------------------------------------------------------- /internal/infrastructure/memory/layer_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/reearth/reearth-backend/pkg/id" 8 | "github.com/reearth/reearth-backend/pkg/layer" 9 | "github.com/reearth/reearth-backend/pkg/tag" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestLayer_FindByTag(t *testing.T) { 14 | ctx := context.Background() 15 | sid := id.NewSceneID() 16 | t1, _ := tag.NewItem().NewID().Scene(sid).Label("item").Build() 17 | tl := layer.NewTagList([]layer.Tag{layer.NewTagGroup(t1.ID(), nil)}) 18 | lg := layer.New().NewID().Tags(tl).Scene(sid).Group().MustBuild() 19 | 20 | repo := Layer{ 21 | data: map[id.LayerID]layer.Layer{ 22 | lg.ID(): lg, 23 | }, 24 | } 25 | 26 | out, err := repo.FindByTag(ctx, t1.ID()) 27 | assert.NoError(t, err) 28 | assert.Equal(t, layer.List{lg.LayerRef()}, out) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/scene/plugin.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | type Plugin struct { 4 | plugin PluginID 5 | property *PropertyID 6 | } 7 | 8 | func NewPlugin(plugin PluginID, property *PropertyID) *Plugin { 9 | return &Plugin{ 10 | plugin: plugin, 11 | property: property.CopyRef(), 12 | } 13 | } 14 | 15 | func (s *Plugin) Plugin() PluginID { 16 | if s == nil { 17 | return PluginID{} 18 | } 19 | return s.plugin 20 | } 21 | 22 | func (s *Plugin) PluginRef() *PluginID { 23 | if s == nil { 24 | return nil 25 | } 26 | return s.plugin.Ref() 27 | } 28 | 29 | func (s *Plugin) Property() *PropertyID { 30 | if s == nil { 31 | return nil 32 | } 33 | return s.property.CopyRef() 34 | } 35 | 36 | func (s *Plugin) Clone() *Plugin { 37 | if s == nil { 38 | return nil 39 | } 40 | return &Plugin{ 41 | plugin: s.plugin.Clone(), 42 | property: s.property.CopyRef(), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/tag/map.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | import "sort" 4 | 5 | type Map map[ID]Tag 6 | 7 | func (m Map) All() List { 8 | if m == nil || len(m) == 0 { 9 | return nil 10 | } 11 | res := make(List, 0, len(m)) 12 | for _, t := range m { 13 | res = append(res, t) 14 | } 15 | sort.SliceStable(res, func(i, j int) bool { 16 | return res[i].ID().Compare(res[j].ID()) < 0 17 | }) 18 | return res 19 | } 20 | 21 | func MapFromList(tags []Tag) Map { 22 | res := make(Map) 23 | for _, t := range tags { 24 | if t == nil { 25 | continue 26 | } 27 | 28 | res[t.ID()] = t 29 | } 30 | return res 31 | } 32 | 33 | func MapFromRefList(tags []*Tag) Map { 34 | res := make(Map) 35 | for _, t := range tags { 36 | if t == nil { 37 | continue 38 | } 39 | 40 | t2 := *t 41 | if t2 == nil { 42 | continue 43 | } 44 | 45 | res[t2.ID()] = t2 46 | } 47 | return res 48 | } 49 | -------------------------------------------------------------------------------- /pkg/layer/encoding/common.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "image/color" 5 | "strconv" 6 | "strings" 7 | 8 | "gopkg.in/go-playground/colors.v1" 9 | ) 10 | 11 | func getColor(str string) *color.RGBA { 12 | if len(str) == 0 { 13 | return nil 14 | } 15 | 16 | cs := str 17 | a := "" 18 | 19 | if str[0] == '#' { 20 | if len(str) == 5 { 21 | cs = str[:len(str)-1] 22 | a = strings.Repeat(str[len(str)-1:], 2) 23 | } else if len(str) == 9 { 24 | cs = str[:len(str)-2] 25 | a = str[len(str)-2:] 26 | } 27 | } 28 | 29 | b, err := colors.Parse(cs) 30 | if err != nil || b == nil { 31 | return nil 32 | } 33 | 34 | c := b.ToRGBA() 35 | var alpha uint8 36 | if a != "" { 37 | a2, _ := strconv.ParseUint(a, 16, 8) 38 | alpha = uint8(a2) 39 | } else { 40 | alpha = uint8(c.A * 255) 41 | } 42 | 43 | return &color.RGBA{R: c.R, G: c.G, B: c.B, A: alpha} 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "go.lintTool": "golangci-lint", 4 | "yaml.format.enable": true, 5 | "yaml.completion": true, 6 | "yaml.validate": true, 7 | "yaml.hover": true, 8 | "yaml.schemas": { 9 | "./schemas/plugin_manifest.json": [ 10 | "/pkg/builtin/manifest.yml" 11 | ], 12 | "./schemas/plugin_manifest_translation.json": [ 13 | "/pkg/builtin/manifest_*.yml" 14 | ], 15 | "https://json.schemastore.org/github-workflow.json": ".github/workflows/build.yml" 16 | }, 17 | "json.schemas": [ 18 | { 19 | "fileMatch": [ 20 | "/pkg/builtin/manifest.json" 21 | ], 22 | "url": "./schemas/plugin_manifest.json" 23 | }, 24 | { 25 | "fileMatch": [ 26 | "/pkg/builtin/manifest_*.json" 27 | ], 28 | "url": "./schemas/plugin_manifest_translation.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /internal/adapter/http/published.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/url" 7 | 8 | "github.com/reearth/reearth-backend/internal/usecase/interfaces" 9 | ) 10 | 11 | type PublishedController struct { 12 | usecase interfaces.Published 13 | } 14 | 15 | func NewPublishedController(usecase interfaces.Published) *PublishedController { 16 | return &PublishedController{usecase: usecase} 17 | } 18 | 19 | func (c *PublishedController) Metadata(ctx context.Context, name string) (interfaces.ProjectPublishedMetadata, error) { 20 | return c.usecase.Metadata(ctx, name) 21 | } 22 | 23 | func (c *PublishedController) Data(ctx context.Context, name string) (io.Reader, error) { 24 | return c.usecase.Data(ctx, name) 25 | } 26 | 27 | func (c *PublishedController) Index(ctx context.Context, name string, url *url.URL) (string, error) { 28 | return c.usecase.Index(ctx, name, url) 29 | } 30 | -------------------------------------------------------------------------------- /internal/app/private.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/reearth/reearth-backend/internal/adapter" 9 | "github.com/reearth/reearth-backend/pkg/id" 10 | "github.com/reearth/reearth-backend/pkg/rerror" 11 | ) 12 | 13 | func ExportLayer() echo.HandlerFunc { 14 | return func(c echo.Context) error { 15 | ctx := c.Request().Context() 16 | u := adapter.Usecases(ctx) 17 | 18 | param := c.Param("param") 19 | params := strings.Split(param, ".") 20 | if len(params) != 2 { 21 | return rerror.ErrNotFound 22 | } 23 | 24 | lid, err := id.LayerIDFrom(params[0]) 25 | if err != nil { 26 | return rerror.ErrNotFound 27 | } 28 | 29 | reader, mime, err := u.Layer.Export(ctx, lid, params[1]) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return c.Stream(http.StatusOK, mime, reader) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/usecase/repo/property_schema.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/property" 8 | ) 9 | 10 | type PropertySchema interface { 11 | Filtered(SceneFilter) PropertySchema 12 | FindByID(context.Context, id.PropertySchemaID) (*property.Schema, error) 13 | FindByIDs(context.Context, []id.PropertySchemaID) (property.SchemaList, error) 14 | Save(context.Context, *property.Schema) error 15 | SaveAll(context.Context, property.SchemaList) error 16 | Remove(context.Context, id.PropertySchemaID) error 17 | RemoveAll(context.Context, []id.PropertySchemaID) error 18 | } 19 | 20 | func PropertySchemaLoaderFrom(r PropertySchema) property.SchemaLoader { 21 | return func(ctx context.Context, ids ...id.PropertySchemaID) (property.SchemaList, error) { 22 | return r.FindByIDs(ctx, ids) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/value/bool.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "strconv" 4 | 5 | var TypeBool Type = "bool" 6 | 7 | type propertyBool struct{} 8 | 9 | func (p *propertyBool) I2V(i interface{}) (interface{}, bool) { 10 | switch v := i.(type) { 11 | case bool: 12 | return v, true 13 | case string: 14 | if b, err := strconv.ParseBool(v); err == nil { 15 | return b, true 16 | } 17 | case *bool: 18 | if v != nil { 19 | return p.I2V(*v) 20 | } 21 | case *string: 22 | if v != nil { 23 | return p.I2V(*v) 24 | } 25 | } 26 | return nil, false 27 | } 28 | 29 | func (*propertyBool) V2I(v interface{}) (interface{}, bool) { 30 | return v, true 31 | } 32 | 33 | func (*propertyBool) Validate(i interface{}) bool { 34 | _, ok := i.(bool) 35 | return ok 36 | } 37 | 38 | func (v *Value) ValueBool() (vv bool, ok bool) { 39 | if v == nil { 40 | return 41 | } 42 | vv, ok = v.v.(bool) 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /pkg/tag/tag.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrEmptyLabel = errors.New("tag label can't be empty") 9 | ErrInvalidSceneID = errors.New("invalid scene ID") 10 | ) 11 | 12 | type tag struct { 13 | id ID 14 | label string 15 | sceneId SceneID 16 | } 17 | 18 | type Tag interface { 19 | ID() ID 20 | Scene() SceneID 21 | Label() string 22 | Rename(string) 23 | } 24 | 25 | func (t *tag) ID() ID { 26 | return t.id 27 | } 28 | 29 | func (t *tag) Scene() SceneID { 30 | return t.sceneId 31 | } 32 | 33 | func (t *tag) Label() string { 34 | return t.label 35 | } 36 | 37 | func (t *tag) Rename(s string) { 38 | t.label = s 39 | } 40 | 41 | func ToTagGroup(t Tag) *Group { 42 | if tg, ok := t.(*Group); ok { 43 | return tg 44 | } 45 | return nil 46 | } 47 | 48 | func ToTagItem(t Tag) *Item { 49 | if ti, ok := t.(*Item); ok { 50 | return ti 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/file/targz.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "errors" 7 | "io" 8 | ) 9 | 10 | type TarReader struct { 11 | tr *tar.Reader 12 | } 13 | 14 | func NewTarReader(tr *tar.Reader) *TarReader { 15 | return &TarReader{tr: tr} 16 | } 17 | 18 | func TarReaderFromTarGz(r io.Reader) (*TarReader, error) { 19 | gzipReader, err := gzip.NewReader(r) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &TarReader{tr: tar.NewReader(gzipReader)}, nil 24 | } 25 | 26 | func (r *TarReader) Next() (*File, error) { 27 | if r == nil || r.tr == nil { 28 | return nil, nil 29 | } 30 | 31 | h, err := r.tr.Next() 32 | if errors.Is(err, io.EOF) { 33 | return nil, nil 34 | } 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | fi := h.FileInfo() 40 | if fi.IsDir() { 41 | return r.Next() 42 | } 43 | 44 | return &File{Content: io.NopCloser(r.tr), Path: h.Name, Size: fi.Size()}, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/tag/item.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | type Item struct { 4 | tag 5 | parent *ID 6 | linkedDatasetFieldID *DatasetFieldID 7 | linkedDatasetID *DatasetID 8 | linkedDatasetSchemaID *DatasetSchemaID 9 | } 10 | 11 | func (i *Item) Parent() *ID { 12 | if i == nil { 13 | return nil 14 | } 15 | return i.parent.CopyRef() 16 | } 17 | 18 | func (i *Item) LinkedDatasetFieldID() *DatasetFieldID { 19 | if i == nil { 20 | return nil 21 | } 22 | return i.linkedDatasetFieldID.CopyRef() 23 | } 24 | 25 | func (i *Item) LinkedDatasetID() *DatasetID { 26 | if i == nil { 27 | return nil 28 | } 29 | return i.linkedDatasetID.CopyRef() 30 | } 31 | 32 | func (i *Item) LinkedDatasetSchemaID() *DatasetSchemaID { 33 | if i == nil { 34 | return nil 35 | } 36 | return i.linkedDatasetSchemaID.CopyRef() 37 | } 38 | 39 | func (i *Item) SetParent(p *ID) { 40 | if i == nil { 41 | return 42 | } 43 | i.parent = p.CopyRef() 44 | } 45 | -------------------------------------------------------------------------------- /pkg/layer/layerops/processor.go: -------------------------------------------------------------------------------- 1 | package layerops 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/layer" 7 | ) 8 | 9 | type Processor struct { 10 | RootLayerID layer.ID 11 | LayerLoader layer.Loader 12 | } 13 | 14 | type UninstallPluginResult struct { 15 | ModifiedLayers layer.List 16 | RemovedProperties []layer.PropertyID 17 | } 18 | 19 | func (p Processor) UninstallPlugin(ctx context.Context, pluginID layer.PluginID) (res UninstallPluginResult, err error) { 20 | err = p.LayerLoader.Walk(ctx, func(l layer.Layer, parents layer.GroupList) error { 21 | // delete infobox fields 22 | if removedProperties := l.Infobox().RemoveAllByPlugin(pluginID, nil); len(removedProperties) > 0 { 23 | res.RemovedProperties = append(res.RemovedProperties, removedProperties...) 24 | res.ModifiedLayers = append(res.ModifiedLayers, &l) 25 | } 26 | return nil 27 | }, []layer.ID{p.RootLayerID}) 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /pkg/scene/list.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | type List []*Scene 4 | 5 | func (l List) IDs() []ID { 6 | if l == nil { 7 | return nil 8 | } 9 | 10 | res := make([]ID, 0, len(l)) 11 | for _, s := range l { 12 | res = append(res, s.ID()) 13 | } 14 | return res 15 | } 16 | 17 | func (l List) FilterByID(ids ...ID) List { 18 | if l == nil { 19 | return nil 20 | } 21 | 22 | res := make(List, 0, len(l)) 23 | for _, s := range l { 24 | sid2 := s.ID() 25 | for _, sid := range ids { 26 | if sid == sid2 { 27 | res = append(res, s) 28 | break 29 | } 30 | } 31 | } 32 | return res 33 | } 34 | 35 | func (l List) FilterByTeam(teams ...TeamID) List { 36 | if l == nil { 37 | return nil 38 | } 39 | 40 | res := make(List, 0, len(l)) 41 | for _, s := range l { 42 | st := s.Team() 43 | for _, t := range teams { 44 | if t == st { 45 | res = append(res, s) 46 | break 47 | } 48 | } 49 | } 50 | return res 51 | } 52 | -------------------------------------------------------------------------------- /pkg/user/team_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTeam_ID(t *testing.T) { 10 | tid := NewTeamID() 11 | tm := NewTeam().ID(tid).MustBuild() 12 | assert.Equal(t, tid, tm.ID()) 13 | } 14 | 15 | func TestTeam_Name(t *testing.T) { 16 | tm := NewTeam().NewID().Name("ttt").MustBuild() 17 | assert.Equal(t, "ttt", tm.Name()) 18 | } 19 | 20 | func TestTeam_Members(t *testing.T) { 21 | m := map[ID]Role{ 22 | NewID(): RoleOwner, 23 | } 24 | tm := NewTeam().NewID().Members(m).MustBuild() 25 | assert.Equal(t, m, tm.Members().Members()) 26 | } 27 | 28 | func TestTeam_IsPersonal(t *testing.T) { 29 | tm := NewTeam().NewID().Personal(true).MustBuild() 30 | assert.Equal(t, true, tm.IsPersonal()) 31 | } 32 | 33 | func TestTeam_Rename(t *testing.T) { 34 | tm := NewTeam().NewID().Name("ttt").MustBuild() 35 | tm.Rename("ccc") 36 | assert.Equal(t, "ccc", tm.Name()) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/dataset/loader.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Loader func(context.Context, ...ID) (List, error) 8 | 9 | func LoaderFrom(data []*Dataset) Loader { 10 | return func(ctx context.Context, ids ...ID) (List, error) { 11 | res := make(List, 0, len(ids)) 12 | for _, i := range ids { 13 | found := false 14 | for _, d := range data { 15 | if i == d.ID() { 16 | res = append(res, d) 17 | found = true 18 | break 19 | } 20 | } 21 | if !found { 22 | res = append(res, nil) 23 | } 24 | } 25 | return res, nil 26 | } 27 | } 28 | 29 | func LoaderFromMap(data map[ID]*Dataset) Loader { 30 | return func(ctx context.Context, ids ...ID) (List, error) { 31 | res := make(List, 0, len(ids)) 32 | for _, i := range ids { 33 | if d, ok := data[i]; ok { 34 | res = append(res, d) 35 | } else { 36 | res = append(res, nil) 37 | } 38 | } 39 | return res, nil 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/mongodoc/consumer.go: -------------------------------------------------------------------------------- 1 | package mongodoc 2 | 3 | import "go.mongodb.org/mongo-driver/bson" 4 | 5 | type Consumer interface { 6 | // Consume で渡されたrawの参照をフィールドに持ってはいけません 7 | // MUST NOT HAVE A ROW REFERENCE PASSED BY Consume METHOD IN THE FIELD 8 | Consume(raw bson.Raw) error 9 | } 10 | 11 | type FuncConsumer func(raw bson.Raw) error 12 | 13 | func (c FuncConsumer) Consume(raw bson.Raw) error { 14 | return c(raw) 15 | } 16 | 17 | type BatchConsumer struct { 18 | Size int 19 | Rows []bson.Raw 20 | Callback func([]bson.Raw) error 21 | } 22 | 23 | func (c *BatchConsumer) Consume(raw bson.Raw) error { 24 | size := c.Size 25 | if size == 0 { 26 | size = 10 27 | } 28 | 29 | if raw != nil { 30 | c.Rows = append(c.Rows, raw) 31 | } 32 | 33 | if raw == nil || len(c.Rows) >= size { 34 | err := c.Callback(c.Rows) 35 | c.Rows = []bson.Raw{} 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/plugin/manifest/testdata/translation.yml: -------------------------------------------------------------------------------- 1 | { 2 | "description": "test plugin desc", 3 | "name": "test plugin name", 4 | "extensions": 5 | { 6 | "test_ext": 7 | { 8 | "name": "test ext name", 9 | "propertySchema": 10 | { 11 | "test_ps": 12 | { 13 | "description": "test ps desc", 14 | "title": "test ps title", 15 | "fields": 16 | { 17 | "test_field": 18 | { 19 | "title": "test field name", 20 | "description": "test field desc", 21 | "choices": { "test_key": "test choice value" }, 22 | "prefix": "P", 23 | "suffix": "S", 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /pkg/plugin/list.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "sort" 4 | 5 | type List []*Plugin 6 | 7 | func (l List) Find(p ID) *Plugin { 8 | for _, q := range l { 9 | if q.ID().Equal(p) { 10 | return q 11 | } 12 | } 13 | return nil 14 | } 15 | 16 | func (l List) Concat(m List) List { 17 | return append(l, m...) 18 | } 19 | 20 | func (l List) MapToIDs(ids []ID) List { 21 | res := make(List, 0, len(ids)) 22 | for _, id := range ids { 23 | res = append(res, l.Find(id)) 24 | } 25 | return res 26 | } 27 | 28 | func (l List) Map() Map { 29 | m := make(Map, len(l)) 30 | for _, p := range l { 31 | m[p.ID()] = p 32 | } 33 | return m 34 | } 35 | 36 | type Map map[ID]*Plugin 37 | 38 | func (m Map) List() List { 39 | if m == nil { 40 | return nil 41 | } 42 | res := make(List, 0, len(m)) 43 | for _, p := range m { 44 | res = append(res, p) 45 | } 46 | sort.SliceStable(res, func(i, j int) bool { 47 | return res[i].ID().String() > res[j].ID().String() 48 | }) 49 | return res 50 | } 51 | -------------------------------------------------------------------------------- /pkg/user/password_reset.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | var timeNow = time.Now 10 | 11 | type PasswordReset struct { 12 | Token string 13 | CreatedAt time.Time 14 | } 15 | 16 | func NewPasswordReset() *PasswordReset { 17 | return &PasswordReset{ 18 | Token: generateToken(), 19 | CreatedAt: timeNow(), 20 | } 21 | } 22 | 23 | func PasswordResetFrom(token string, createdAt time.Time) *PasswordReset { 24 | return &PasswordReset{ 25 | Token: token, 26 | CreatedAt: createdAt, 27 | } 28 | } 29 | 30 | func generateToken() string { 31 | return uuid.New().String() 32 | } 33 | 34 | func (pr *PasswordReset) Validate(token string) bool { 35 | return pr != nil && pr.Token == token && pr.CreatedAt.Add(24*time.Hour).After(time.Now()) 36 | } 37 | 38 | func (pr *PasswordReset) Clone() *PasswordReset { 39 | if pr == nil { 40 | return nil 41 | } 42 | pr2 := PasswordResetFrom(pr.Token, pr.CreatedAt) 43 | return pr2 44 | } 45 | -------------------------------------------------------------------------------- /pkg/dataset/schema_field_diff.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | type SchemaFieldDiff struct { 4 | Added []*SchemaField 5 | Removed []*SchemaField 6 | Replaced map[FieldID]*SchemaField 7 | } 8 | 9 | func (d *Schema) FieldDiffBySource(d2 *Schema) SchemaFieldDiff { 10 | added := []*SchemaField{} 11 | removed := []*SchemaField{} 12 | // others := map[string]DatasetDiffTouple{} 13 | others2 := map[FieldID]*SchemaField{} 14 | 15 | s1 := map[string]*SchemaField{} 16 | for _, d1 := range d.fields { 17 | s1[d1.Source()] = d1 18 | } 19 | 20 | for _, d2 := range d2.fields { 21 | if d1, ok := s1[d2.Source()]; ok { 22 | others2[d1.ID()] = d2 23 | } else { 24 | // added 25 | added = append(added, d2) 26 | } 27 | } 28 | 29 | for _, d1 := range d.fields { 30 | if _, ok := others2[d1.ID()]; !ok { 31 | // removed 32 | removed = append(removed, d1) 33 | } 34 | } 35 | 36 | return SchemaFieldDiff{ 37 | Added: added, 38 | Removed: removed, 39 | Replaced: others2, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/infrastructure/memory/transaction_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTransaction_Committed(t *testing.T) { 12 | tr := NewTransaction() 13 | tx, err := tr.Begin() 14 | assert.NoError(t, err) 15 | assert.Equal(t, 0, tr.Committed()) 16 | tx.Commit() 17 | assert.Equal(t, 0, tr.Committed()) 18 | assert.NoError(t, tx.End(context.Background())) 19 | assert.Equal(t, 1, tr.Committed()) 20 | } 21 | 22 | func TestTransaction_SetBeginError(t *testing.T) { 23 | err := errors.New("a") 24 | tr := NewTransaction() 25 | tr.SetBeginError(err) 26 | tx, err2 := tr.Begin() 27 | assert.Nil(t, tx) 28 | assert.Same(t, err, err2) 29 | } 30 | 31 | func TestTransaction_SetEndError(t *testing.T) { 32 | err := errors.New("a") 33 | tr := NewTransaction() 34 | tr.SetEndError(err) 35 | tx, err2 := tr.Begin() 36 | assert.NoError(t, err2) 37 | assert.Same(t, err, tx.End(context.Background())) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/scene/cluster.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | type Cluster struct { 4 | id ClusterID 5 | name string 6 | property PropertyID 7 | } 8 | 9 | func NewCluster(cid ClusterID, name string, pid PropertyID) (*Cluster, error) { 10 | if cid.IsNil() { 11 | return nil, ErrInvalidID 12 | } 13 | return &Cluster{ 14 | id: cid, 15 | name: name, 16 | property: pid, 17 | }, nil 18 | } 19 | 20 | func (c *Cluster) ID() ClusterID { 21 | if c == nil { 22 | return ClusterID{} 23 | } 24 | return c.id 25 | } 26 | 27 | func (c *Cluster) Name() string { 28 | if c == nil { 29 | return "" 30 | } 31 | return c.name 32 | } 33 | 34 | func (c *Cluster) Property() PropertyID { 35 | if c == nil { 36 | return PropertyID{} 37 | } 38 | return c.property 39 | } 40 | 41 | func (c *Cluster) Rename(name string) { 42 | if c == nil { 43 | return 44 | } 45 | c.name = name 46 | } 47 | 48 | func (c *Cluster) UpdateProperty(pid PropertyID) { 49 | if c == nil { 50 | return 51 | } 52 | c.property = pid 53 | } 54 | -------------------------------------------------------------------------------- /internal/infrastructure/memory/config.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/reearth/reearth-backend/internal/usecase/repo" 8 | "github.com/reearth/reearth-backend/pkg/config" 9 | ) 10 | 11 | type Config struct { 12 | lock sync.Mutex 13 | locked bool 14 | data *config.Config 15 | } 16 | 17 | func NewConfig() repo.Config { 18 | return &Config{} 19 | } 20 | 21 | func (r *Config) LockAndLoad(ctx context.Context) (*config.Config, error) { 22 | r.lock.Lock() 23 | r.locked = true 24 | return r.data, nil 25 | } 26 | 27 | func (r *Config) Save(ctx context.Context, c *config.Config) error { 28 | if c != nil { 29 | r.data = c 30 | } 31 | return nil 32 | } 33 | 34 | func (r *Config) SaveAndUnlock(ctx context.Context, c *config.Config) error { 35 | _ = r.Save(ctx, c) 36 | return r.Unlock(ctx) 37 | } 38 | 39 | func (r *Config) Unlock(_ context.Context) error { 40 | if !r.locked { 41 | return nil 42 | } 43 | r.lock.Unlock() 44 | r.locked = false 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/dataset/graph_loader.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type GraphLoader func(context.Context, ID, ...FieldID) (List, *Field, error) 8 | 9 | func GraphLoaderFromMap(m Map) GraphLoader { 10 | return func(ctx context.Context, root ID, fields ...FieldID) (List, *Field, error) { 11 | list, field := m.GraphSearchByFields(root, fields...) 12 | return list, field, nil 13 | } 14 | } 15 | 16 | func GraphLoaderFromMapAndGraph(m Map, g GraphLoader) GraphLoader { 17 | return func(ctx context.Context, root ID, fields ...FieldID) (List, *Field, error) { 18 | if m != nil { 19 | if len(fields) == 0 { 20 | return List{m[root]}, nil, nil 21 | } 22 | if len(fields) == 1 { 23 | ds := m[root] 24 | return List{ds}, ds.Field(fields[0]), nil 25 | } 26 | list, field := m.GraphSearchByFields(root, fields...) 27 | if list != nil && field != nil { 28 | return list, field, nil 29 | } 30 | } 31 | 32 | // it needs looking up dataset graph 33 | return g(ctx, root, fields...) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/file/zip_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMockZipReader(t *testing.T) { 11 | z := MockZipReader([]string{"a", "b", "c/", "c/d"}) 12 | assert.Equal(t, "a", z.File[0].Name) 13 | assert.Equal(t, "b", z.File[1].Name) 14 | assert.Equal(t, "c/", z.File[2].Name) 15 | assert.Equal(t, "c/d", z.File[3].Name) 16 | 17 | for _, f := range []string{"a", "b", "c/d"} { 18 | zf, err := z.Open(f) 19 | assert.NoError(t, err) 20 | b, err := io.ReadAll(zf) 21 | assert.NoError(t, err) 22 | assert.Equal(t, []byte{}, b) 23 | assert.NoError(t, zf.Close()) 24 | } 25 | } 26 | 27 | func TestZipBasePath(t *testing.T) { 28 | assert.Equal(t, "aaa", ZipBasePath(MockZipReader([]string{"aaa/", "aaa/a"}))) 29 | assert.Equal(t, "", ZipBasePath(MockZipReader([]string{"aaa/", "aaa/a", "b"}))) 30 | assert.Equal(t, "", ZipBasePath(MockZipReader([]string{"aaa"}))) 31 | assert.Equal(t, "", ZipBasePath(MockZipReader([]string{"aaa/", "aaa/a", "b/", "b/c"}))) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/property/value_camera_test.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCamera_Clone(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | Camera, Expected *Camera 13 | }{ 14 | { 15 | Name: "nil Camera", 16 | }, 17 | { 18 | Name: "cloned", 19 | Camera: &Camera{ 20 | Lat: 1, 21 | Lng: 1, 22 | Altitude: 2, 23 | Heading: 4, 24 | Pitch: 5, 25 | Roll: 6, 26 | FOV: 7, 27 | }, 28 | Expected: &Camera{ 29 | Lat: 1, 30 | Lng: 1, 31 | Altitude: 2, 32 | Heading: 4, 33 | Pitch: 5, 34 | Roll: 6, 35 | FOV: 7, 36 | }, 37 | }, 38 | } 39 | 40 | for _, tc := range tests { 41 | tc := tc 42 | t.Run(tc.Name, func(t *testing.T) { 43 | t.Parallel() 44 | res := tc.Camera.Clone() 45 | assert.Equal(t, tc.Expected, res) 46 | if tc.Expected != nil { 47 | assert.NotSame(t, tc.Expected, res) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/infrastructure/mailer/sendgrid.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/internal/usecase/gateway" 5 | "github.com/sendgrid/sendgrid-go" 6 | "github.com/sendgrid/sendgrid-go/helpers/mail" 7 | ) 8 | 9 | type sendgridMailer struct { 10 | name string 11 | email string 12 | client *sendgrid.Client 13 | } 14 | 15 | func NewSendGrid(senderName, senderEmail, api string) gateway.Mailer { 16 | return &sendgridMailer{ 17 | name: senderName, 18 | email: senderEmail, 19 | client: sendgrid.NewSendClient(api), 20 | } 21 | } 22 | 23 | func (m *sendgridMailer) SendMail(to []gateway.Contact, subject, plainContent, htmlContent string) error { 24 | for _, t := range to { 25 | sender := mail.NewEmail(m.name, m.email) 26 | receiver := mail.NewEmail(t.Name, t.Email) 27 | message := mail.NewSingleEmail(sender, subject, receiver, plainContent, htmlContent) 28 | _, err := m.client.Send(message) 29 | if err != nil { 30 | return err 31 | } 32 | } 33 | 34 | logMail(to, subject) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/adapter/gql/gqlmodel/convert_property_test.go: -------------------------------------------------------------------------------- 1 | package gqlmodel 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/reearth/reearth-backend/pkg/property" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFromPropertyValueAndType(t *testing.T) { 12 | type args struct { 13 | v interface{} 14 | t ValueType 15 | } 16 | 17 | tests := []struct { 18 | name string 19 | args args 20 | want *property.Value 21 | }{ 22 | { 23 | name: "number", 24 | args: args{ 25 | v: 1.1, 26 | t: ValueTypeNumber, 27 | }, 28 | want: property.ValueTypeNumber.ValueFrom(1.1), 29 | }, 30 | { 31 | name: "json number", 32 | args: args{ 33 | v: json.Number("1.1"), 34 | t: ValueTypeNumber, 35 | }, 36 | want: property.ValueTypeNumber.ValueFrom(1.1), 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | tt := tt 42 | t.Run(tt.name, func(t *testing.T) { 43 | t.Parallel() 44 | assert.Equal(t, tt.want, FromPropertyValueAndType(tt.args.v, tt.args.t)) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/asset/asset.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | var ( 9 | ErrEmptyTeamID = errors.New("require team id") 10 | ErrEmptyURL = errors.New("require valid url") 11 | ErrEmptySize = errors.New("file size cannot be zero") 12 | ) 13 | 14 | type Asset struct { 15 | id ID 16 | createdAt time.Time 17 | team TeamID 18 | name string // file name 19 | size int64 // file size 20 | url string 21 | contentType string 22 | } 23 | 24 | func (a *Asset) ID() ID { 25 | return a.id 26 | } 27 | 28 | func (a *Asset) Team() TeamID { 29 | return a.team 30 | } 31 | 32 | func (a *Asset) Name() string { 33 | return a.name 34 | } 35 | 36 | func (a *Asset) Size() int64 { 37 | return a.size 38 | } 39 | 40 | func (a *Asset) URL() string { 41 | return a.url 42 | } 43 | 44 | func (a *Asset) ContentType() string { 45 | return a.contentType 46 | } 47 | 48 | func (a *Asset) CreatedAt() time.Time { 49 | if a == nil { 50 | return time.Time{} 51 | } 52 | return a.id.Timestamp() 53 | } 54 | -------------------------------------------------------------------------------- /pkg/dataset/field.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | type Field struct { 4 | field FieldID 5 | value *Value 6 | source string 7 | } 8 | 9 | func NewField(field FieldID, value *Value, source string) *Field { 10 | if value == nil { 11 | return nil 12 | } 13 | return &Field{ 14 | field: field, 15 | value: value, 16 | } 17 | } 18 | 19 | func (d *Field) Field() (i FieldID) { 20 | if d == nil { 21 | return 22 | } 23 | return d.field 24 | } 25 | 26 | func (d *Field) FieldRef() *FieldID { 27 | if d == nil { 28 | return nil 29 | } 30 | return d.field.Ref() 31 | } 32 | 33 | func (d *Field) IsEmpty() bool { 34 | return d == nil || d.field.IsNil() || d.value == nil 35 | } 36 | 37 | func (d *Field) Value() *Value { 38 | if d == nil { 39 | return nil 40 | } 41 | return d.value.Clone() 42 | } 43 | 44 | func (d *Field) Type() ValueType { 45 | if d == nil { 46 | return ValueTypeUnknown 47 | } 48 | return d.value.Type() 49 | } 50 | 51 | func (d *Field) Source() string { 52 | if d == nil { 53 | return "" 54 | } 55 | return d.source 56 | } 57 | -------------------------------------------------------------------------------- /internal/infrastructure/google/fetch.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | func sheetURL(fileId string, sheetName string) string { 11 | gurl := url.URL{ 12 | Scheme: "https", 13 | Host: "docs.google.com", 14 | Path: fmt.Sprintf("spreadsheets/d/%s/gviz/tq", fileId), 15 | } 16 | 17 | queryValues := gurl.Query() 18 | queryValues.Set("tqx", "out:csv") 19 | queryValues.Set("sheet", sheetName) 20 | gurl.RawQuery = queryValues.Encode() 21 | 22 | return gurl.String() 23 | } 24 | 25 | func fetchCSV(token string, fileId string, sheetName string) (*io.ReadCloser, error) { 26 | u := sheetURL(fileId, sheetName) 27 | req, err := http.NewRequest("GET", u, nil) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | req.Header.Set("Authorization", "Bearer "+token) 33 | res, err := http.DefaultClient.Do(req) 34 | if err != nil { 35 | return nil, err 36 | } 37 | if res.StatusCode != http.StatusOK { 38 | return nil, fmt.Errorf("StatusCode=%d", res.StatusCode) 39 | } 40 | 41 | return &res.Body, nil 42 | } 43 | -------------------------------------------------------------------------------- /tools/cmd/shapefiletest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | 7 | "github.com/jonas-p/go-shp" 8 | ) 9 | 10 | func main() { 11 | // points to write 12 | points := []shp.Point{ 13 | {X: 10.0, Y: 10.0}, 14 | {X: 10.0, Y: 15.0}, 15 | {X: 15.0, Y: 15.0}, 16 | {X: 15.0, Y: 10.0}, 17 | } 18 | 19 | // fields to write 20 | fields := []shp.Field{ 21 | // String attribute field with length 25 22 | shp.StringField("NAME", 25), 23 | } 24 | 25 | // create and open a shapefile for writing points 26 | shape, err := shp.Create("points.shp", shp.POINT) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | defer shape.Close() 31 | 32 | // setup fields for attributes 33 | if err := shape.SetFields(fields); err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | // write points and attributes 38 | for n, point := range points { 39 | shape.Write(&point) 40 | 41 | // write attribute for object n for field 0 (NAME) 42 | if err := shape.WriteAttribute(n, 0, "Point "+strconv.Itoa(n+1)); err != nil { 43 | log.Fatal(err) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/value/url.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "net/url" 4 | 5 | var TypeURL Type = "url" 6 | 7 | type propertyURL struct{} 8 | 9 | func (p *propertyURL) I2V(i interface{}) (interface{}, bool) { 10 | if v, ok := i.(url.URL); ok { 11 | return &v, true 12 | } 13 | 14 | if v, ok := i.(*url.URL); ok && v != nil { 15 | return p.I2V(*v) // clone URL 16 | } 17 | 18 | if v, ok := i.(string); ok { 19 | if u, err := url.Parse(v); err == nil { 20 | return u, true 21 | } 22 | } 23 | 24 | if v, ok := i.(*string); ok && v != nil { 25 | return p.I2V(*v) 26 | } 27 | 28 | return nil, false 29 | } 30 | 31 | func (*propertyURL) V2I(v interface{}) (interface{}, bool) { 32 | u, ok := v.(*url.URL) 33 | if !ok { 34 | return nil, false 35 | } 36 | if u == nil { 37 | return "", true 38 | } 39 | return u.String(), true 40 | } 41 | 42 | func (*propertyURL) Validate(i interface{}) bool { 43 | _, ok := i.(*url.URL) 44 | return ok 45 | } 46 | 47 | func (v *Value) ValueURL() (vv *url.URL, ok bool) { 48 | if v == nil { 49 | return 50 | } 51 | vv, ok = v.v.(*url.URL) 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /gqlgen.yml: -------------------------------------------------------------------------------- 1 | # .gqlgen.yml example 2 | # 3 | # Refer to https://gqlgen.com/config/ 4 | # for detailed .gqlgen.yml documentation. 5 | 6 | schema: 7 | - schema.graphql 8 | exec: 9 | filename: internal/adapter/gql/generated.go 10 | model: 11 | filename: internal/adapter/gql/gqlmodel/models_gen.go 12 | resolver: 13 | filename: internal/adapter/gql/resolver.go 14 | type: Resolver 15 | models: 16 | DateTime: 17 | model: github.com/99designs/gqlgen/graphql.Time 18 | FileSize: 19 | model: github.com/99designs/gqlgen/graphql.Int64 20 | Cursor: 21 | model: github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel.Cursor 22 | URL: 23 | model: github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel.URL 24 | TranslatedString: 25 | model: github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel.Map 26 | Lang: 27 | model: github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel.Lang 28 | ID: 29 | model: github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel.ID 30 | DatasetSchema: 31 | fields: 32 | totalCount: 33 | resolver: true 34 | -------------------------------------------------------------------------------- /internal/app/web.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/labstack/echo/v4/middleware" 10 | ) 11 | 12 | type WebConfig map[string]string 13 | 14 | func web(e *echo.Echo, wc WebConfig, a []AuthConfig) { 15 | if _, err := os.Stat("web"); err != nil { 16 | return // web won't be delivered 17 | } 18 | 19 | e.Logger.Info("web: web directory will be delivered\n") 20 | 21 | config := map[string]string{} 22 | if len(a) > 0 { 23 | ac := a[0] 24 | if ac.ISS != "" { 25 | config["auth0Domain"] = strings.TrimSuffix(ac.ISS, "/") 26 | } 27 | if ac.ClientID != nil { 28 | config["auth0ClientId"] = *ac.ClientID 29 | } 30 | if len(ac.AUD) > 0 { 31 | config["auth0Audience"] = ac.AUD[0] 32 | } 33 | } 34 | for k, v := range wc { 35 | config[k] = v 36 | } 37 | 38 | e.GET("/reearth_config.json", func(c echo.Context) error { 39 | return c.JSON(http.StatusOK, config) 40 | }) 41 | 42 | e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ 43 | Root: "web", 44 | Index: "index.html", 45 | Browse: false, 46 | HTML5: true, 47 | })) 48 | } 49 | -------------------------------------------------------------------------------- /internal/usecase/interfaces/asset.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/reearth/reearth-backend/internal/usecase" 8 | "github.com/reearth/reearth-backend/pkg/asset" 9 | "github.com/reearth/reearth-backend/pkg/file" 10 | "github.com/reearth/reearth-backend/pkg/id" 11 | ) 12 | 13 | type AssetFilterType string 14 | 15 | const ( 16 | AssetFilterDate AssetFilterType = "DATE" 17 | AssetFilterSize AssetFilterType = "SIZE" 18 | AssetFilterName AssetFilterType = "NAME" 19 | ) 20 | 21 | type CreateAssetParam struct { 22 | TeamID id.TeamID 23 | File *file.File 24 | } 25 | 26 | var ( 27 | ErrCreateAssetFailed error = errors.New("failed to create asset") 28 | ) 29 | 30 | type Asset interface { 31 | Fetch(context.Context, []id.AssetID, *usecase.Operator) ([]*asset.Asset, error) 32 | FindByTeam(context.Context, id.TeamID, *string, *asset.SortType, *usecase.Pagination, *usecase.Operator) ([]*asset.Asset, *usecase.PageInfo, error) 33 | Create(context.Context, CreateAssetParam, *usecase.Operator) (*asset.Asset, error) 34 | Remove(context.Context, id.AssetID, *usecase.Operator) (id.AssetID, error) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/value/rect.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "github.com/mitchellh/mapstructure" 4 | 5 | var TypeRect Type = "rect" 6 | 7 | type Rect struct { 8 | West float64 `json:"west" mapstructure:"west"` 9 | South float64 `json:"south" mapstructure:"south"` 10 | East float64 `json:"east" mapstructure:"east"` 11 | North float64 `json:"north" mapstructure:"north"` 12 | } 13 | 14 | type propertyRect struct{} 15 | 16 | func (p *propertyRect) I2V(i interface{}) (interface{}, bool) { 17 | if v, ok := i.(Rect); ok { 18 | return v, true 19 | } else if v, ok := i.(*Rect); ok { 20 | if v != nil { 21 | return p.I2V(*v) 22 | } 23 | return nil, false 24 | } 25 | 26 | v := Rect{} 27 | if err := mapstructure.Decode(i, &v); err == nil { 28 | return v, false 29 | } 30 | 31 | return nil, false 32 | } 33 | 34 | func (*propertyRect) V2I(v interface{}) (interface{}, bool) { 35 | return v, true 36 | } 37 | 38 | func (*propertyRect) Validate(i interface{}) bool { 39 | _, ok := i.(Rect) 40 | return ok 41 | } 42 | 43 | func (v *Value) ValueRect() (vv Rect, ok bool) { 44 | if v == nil { 45 | return 46 | } 47 | vv, ok = v.v.(Rect) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /pkg/layer/encoding/exporter.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/layer" 7 | "github.com/reearth/reearth-backend/pkg/layer/merging" 8 | ) 9 | 10 | type Exporter struct { 11 | Merger *merging.Merger 12 | Sealer *merging.Sealer 13 | Encoder Encoder 14 | } 15 | 16 | func (e *Exporter) ExportLayerByID(ctx context.Context, l layer.ID) error { 17 | if e == nil { 18 | return nil 19 | } 20 | m, err := e.Merger.MergeLayerFromID(ctx, l, nil) 21 | if err != nil { 22 | return err 23 | } 24 | return e.Encode(ctx, m) 25 | } 26 | 27 | func (e *Exporter) ExportLayer(ctx context.Context, l layer.Layer) error { 28 | if e == nil { 29 | return nil 30 | } 31 | m, err := e.Merger.MergeLayer(ctx, l, nil) 32 | if err != nil { 33 | return err 34 | } 35 | return e.Encode(ctx, m) 36 | } 37 | 38 | func (e *Exporter) Encode(ctx context.Context, m merging.MergedLayer) error { 39 | if e == nil { 40 | return nil 41 | } 42 | s, err := e.Sealer.Seal(ctx, m) 43 | if err != nil { 44 | return err 45 | } 46 | err = e.Encoder.Encode(s) 47 | if err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/scene/lock.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | // LockMode はシーンのロック状態を表します。 4 | type LockMode string 5 | 6 | const ( 7 | // LockModeFree はロックがかかっていない状態です。 8 | LockModeFree LockMode = "" 9 | // LockModePending は処理待ち中です。データの変更は無制限に変更はできます。 10 | LockModePending LockMode = "pending" 11 | // LockModePluginUpgrading はプラグインをアップグレード中です。シーンへの各種操作ができません。 12 | LockModePluginUpgrading LockMode = "plugin upgrading" 13 | // LockModeDatasetSyncing はデータセットを同期中です。シーンへの各種操作ができません。 14 | LockModeDatasetSyncing LockMode = "dataset syncing" 15 | // LockModePublishing はシーンを書き出し中です。シーンへの各種操作ができません。 16 | LockModePublishing LockMode = "publishing" 17 | ) 18 | 19 | func (l LockMode) IsLocked() bool { 20 | switch l { 21 | case LockModeFree: 22 | return false 23 | case LockModePending: 24 | return false 25 | } 26 | return true 27 | } 28 | 29 | func (l LockMode) Validate() (LockMode, bool) { 30 | switch l { 31 | case LockModeFree: 32 | fallthrough 33 | case LockModePending: 34 | fallthrough 35 | case LockModePluginUpgrading: 36 | fallthrough 37 | case LockModeDatasetSyncing: 38 | fallthrough 39 | case LockModePublishing: 40 | return l, true 41 | } 42 | return l, false 43 | } 44 | -------------------------------------------------------------------------------- /pkg/value/string.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | var TypeString Type = "string" 9 | 10 | type propertyString struct{} 11 | 12 | func (p *propertyString) I2V(i interface{}) (interface{}, bool) { 13 | if v, ok := i.(string); ok { 14 | return v, true 15 | } else if v, ok := i.(float64); ok { 16 | return strconv.FormatFloat(v, 'f', -1, 64), true 17 | } else if v, ok := i.(bool); ok && v { 18 | return "true", true 19 | } else if v, ok := i.(*string); ok && v != nil { 20 | return p.I2V(*v) 21 | } else if v, ok := i.(*float64); ok && v != nil { 22 | return p.I2V(*v) 23 | } else if v, ok := i.(*bool); ok && v != nil { 24 | return p.I2V(*v) 25 | } else if v, ok := i.(fmt.Stringer); ok && v != nil { 26 | return v.String(), true 27 | } 28 | return nil, false 29 | } 30 | 31 | func (*propertyString) V2I(v interface{}) (interface{}, bool) { 32 | return v, true 33 | } 34 | 35 | func (*propertyString) Validate(i interface{}) bool { 36 | _, ok := i.(string) 37 | return ok 38 | } 39 | 40 | func (v *Value) ValueString() (vv string, ok bool) { 41 | if v == nil { 42 | return 43 | } 44 | vv, ok = v.v.(string) 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /internal/usecase/gateway/file.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net/url" 8 | 9 | "github.com/reearth/reearth-backend/pkg/file" 10 | "github.com/reearth/reearth-backend/pkg/id" 11 | ) 12 | 13 | var ( 14 | ErrInvalidFile error = errors.New("invalid file") 15 | ErrFailedToUploadFile error = errors.New("failed to upload file") 16 | ErrFileTooLarge error = errors.New("file too large") 17 | ErrFailedToRemoveFile error = errors.New("failed to remove file") 18 | ) 19 | 20 | type File interface { 21 | ReadAsset(context.Context, string) (io.ReadCloser, error) 22 | UploadAsset(context.Context, *file.File) (*url.URL, error) 23 | RemoveAsset(context.Context, *url.URL) error 24 | ReadPluginFile(context.Context, id.PluginID, string) (io.ReadCloser, error) 25 | UploadPluginFile(context.Context, id.PluginID, *file.File) error 26 | RemovePlugin(context.Context, id.PluginID) error 27 | UploadBuiltScene(context.Context, io.Reader, string) error 28 | ReadBuiltSceneFile(context.Context, string) (io.ReadCloser, error) 29 | MoveBuiltScene(context.Context, string, string) error 30 | RemoveBuiltScene(context.Context, string) error 31 | } 32 | -------------------------------------------------------------------------------- /internal/usecase/interactor/team_test.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/reearth/reearth-backend/internal/infrastructure/memory" 8 | "github.com/reearth/reearth-backend/internal/usecase" 9 | "github.com/reearth/reearth-backend/pkg/id" 10 | "github.com/reearth/reearth-backend/pkg/user" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestCreateTeam(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | db := memory.New() 18 | 19 | u := user.New().NewID().Email("aaa@bbb.com").Team(id.NewTeamID()).MustBuild() 20 | teamUC := NewTeam(db) 21 | op := &usecase.Operator{User: u.ID()} 22 | team, err := teamUC.Create(ctx, "team name", u.ID(), op) 23 | 24 | assert.Nil(t, err) 25 | assert.NotNil(t, team) 26 | 27 | resultTeams, _ := teamUC.Fetch(ctx, []id.TeamID{team.ID()}, &usecase.Operator{ 28 | ReadableTeams: []id.TeamID{team.ID()}, 29 | }) 30 | 31 | assert.NotNil(t, resultTeams) 32 | assert.NotEmpty(t, resultTeams) 33 | assert.Equal(t, resultTeams[0].ID(), team.ID()) 34 | assert.Equal(t, resultTeams[0].Name(), "team name") 35 | assert.Equal(t, user.TeamIDList{resultTeams[0].ID()}, op.OwningTeams) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/tag/loader.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | import "context" 4 | 5 | type Loader func(context.Context, ...ID) ([]*Tag, error) 6 | type SceneLoader func(context.Context, SceneID) ([]*Tag, error) 7 | 8 | func LoaderFrom(data List) Loader { 9 | return func(ctx context.Context, ids ...ID) ([]*Tag, error) { 10 | res := make([]*Tag, 0, len(ids)) 11 | for _, i := range ids { 12 | found := false 13 | for _, d := range data { 14 | if i == d.ID() { 15 | res = append(res, &d) 16 | found = true 17 | break 18 | } 19 | } 20 | if !found { 21 | res = append(res, nil) 22 | } 23 | } 24 | return res, nil 25 | } 26 | } 27 | 28 | func LoaderFromMap(data map[ID]Tag) Loader { 29 | return func(ctx context.Context, ids ...ID) ([]*Tag, error) { 30 | res := make([]*Tag, 0, len(ids)) 31 | for _, i := range ids { 32 | if d, ok := data[i]; ok { 33 | res = append(res, &d) 34 | } else { 35 | res = append(res, nil) 36 | } 37 | } 38 | return res, nil 39 | } 40 | } 41 | 42 | func SceneLoaderFrom(data List) SceneLoader { 43 | return func(ctx context.Context, id SceneID) ([]*Tag, error) { 44 | return data.FilterByScene(id).Refs(), nil 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/mongodoc/pagination.go: -------------------------------------------------------------------------------- 1 | package mongodoc 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase" 7 | ) 8 | 9 | type Pagination struct { 10 | Before *string 11 | After *string 12 | First *int 13 | Last *int 14 | } 15 | 16 | func PaginationFrom(pagination *usecase.Pagination) *Pagination { 17 | if pagination == nil { 18 | return nil 19 | } 20 | return &Pagination{ 21 | Before: (*string)(pagination.Before), 22 | After: (*string)(pagination.After), 23 | First: pagination.First, 24 | Last: pagination.Last, 25 | } 26 | } 27 | 28 | func (p *Pagination) SortDirection() int { 29 | if p != nil && p.Last != nil { 30 | return -1 31 | } 32 | return 1 33 | } 34 | 35 | func (p *Pagination) Parameters() (limit int64, op string, cursor *string, err error) { 36 | if first, after := p.First, p.After; first != nil { 37 | limit = int64(*first) 38 | op = "$gt" 39 | cursor = after 40 | return 41 | } 42 | if last, before := p.Last, p.Before; last != nil { 43 | limit = int64(*last) 44 | op = "$lt" 45 | cursor = before 46 | return 47 | } 48 | return 0, "", nil, errors.New("neither first nor last are set") 49 | } 50 | -------------------------------------------------------------------------------- /pkg/dataset/schema_field.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | type SchemaField struct { 4 | id FieldID 5 | name string 6 | dataType ValueType 7 | source string 8 | ref *SchemaID 9 | } 10 | 11 | func (d *SchemaField) ID() (i FieldID) { 12 | if d == nil { 13 | return 14 | } 15 | return d.id 16 | } 17 | 18 | func (d *SchemaField) IDRef() *FieldID { 19 | if d == nil { 20 | return nil 21 | } 22 | return d.id.Ref() 23 | } 24 | 25 | func (d *SchemaField) Name() (n string) { 26 | if d == nil { 27 | return 28 | } 29 | return d.name 30 | } 31 | 32 | func (d *SchemaField) Ref() *SchemaID { 33 | if d == nil { 34 | return nil 35 | } 36 | return d.ref 37 | } 38 | 39 | func (d *SchemaField) Type() (v ValueType) { 40 | if d == nil { 41 | return 42 | } 43 | return d.dataType 44 | } 45 | 46 | func (d *SchemaField) Source() (s string) { 47 | if d == nil { 48 | return 49 | } 50 | return d.source 51 | } 52 | 53 | func (d *SchemaField) Clone() *SchemaField { 54 | if d == nil { 55 | return nil 56 | } 57 | return &SchemaField{ 58 | id: d.id, 59 | name: d.name, 60 | dataType: d.dataType, 61 | source: d.source, 62 | ref: d.ref.CopyRef(), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/usecase/interactor/published_test.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase/interfaces" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRenderIndex(t *testing.T) { 11 | assert.Equal(t, ` 12 | xxx> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | `, renderIndex( 24 | ` 25 | Foobar 26 | `, 27 | "https://xxss.com", 28 | interfaces.ProjectPublishedMetadata{ 29 | Title: "xxx>", 30 | Description: "desc", 31 | Image: "hogehoge", 32 | Noindex: true, 33 | }, 34 | )) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/user/role.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // RoleOwner is a role who can have full controll of project 10 | RoleOwner = Role("owner") 11 | // RoleWriter is a role who can read and write project 12 | RoleWriter = Role("writer") 13 | // RoleReader is a role who can read project 14 | RoleReader = Role("reader") 15 | 16 | roles = []Role{ 17 | RoleOwner, 18 | RoleWriter, 19 | RoleReader, 20 | } 21 | 22 | ErrInvalidRole = errors.New("invalid role") 23 | ) 24 | 25 | type Role string 26 | 27 | func checkRole(role Role) bool { 28 | switch role { 29 | case RoleOwner: 30 | return true 31 | case RoleWriter: 32 | return true 33 | case RoleReader: 34 | return true 35 | } 36 | return false 37 | } 38 | 39 | func RoleFromString(r string) (Role, error) { 40 | role := Role(strings.ToLower(r)) 41 | 42 | if checkRole(role) { 43 | return role, nil 44 | } 45 | return role, ErrInvalidRole 46 | } 47 | 48 | func (r Role) Includes(role Role) bool { 49 | for i, r2 := range roles { 50 | if r == r2 { 51 | for _, r3 := range roles[i:] { 52 | if role == r3 { 53 | return true 54 | } 55 | } 56 | } 57 | } 58 | return false 59 | } 60 | -------------------------------------------------------------------------------- /internal/usecase/repo/dataset_schema.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase" 7 | "github.com/reearth/reearth-backend/pkg/dataset" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | ) 10 | 11 | type DatasetSchema interface { 12 | Filtered(SceneFilter) DatasetSchema 13 | FindByID(context.Context, id.DatasetSchemaID) (*dataset.Schema, error) 14 | FindByIDs(context.Context, id.DatasetSchemaIDList) (dataset.SchemaList, error) 15 | FindByScene(context.Context, id.SceneID, *usecase.Pagination) (dataset.SchemaList, *usecase.PageInfo, error) 16 | FindBySceneAll(context.Context, id.SceneID) (dataset.SchemaList, error) 17 | FindBySceneAndSource(context.Context, id.SceneID, string) (dataset.SchemaList, error) 18 | FindDynamicByID(context.Context, id.DatasetSchemaID) (*dataset.Schema, error) 19 | FindAllDynamicByScene(context.Context, id.SceneID) (dataset.SchemaList, error) 20 | Save(context.Context, *dataset.Schema) error 21 | SaveAll(context.Context, dataset.SchemaList) error 22 | Remove(context.Context, id.DatasetSchemaID) error 23 | RemoveAll(context.Context, id.DatasetSchemaIDList) error 24 | RemoveByScene(context.Context, id.SceneID) error 25 | } 26 | -------------------------------------------------------------------------------- /pkg/user/team_builder.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | type TeamBuilder struct { 4 | t *Team 5 | members map[ID]Role 6 | personal bool 7 | } 8 | 9 | func NewTeam() *TeamBuilder { 10 | return &TeamBuilder{t: &Team{}} 11 | } 12 | 13 | func (b *TeamBuilder) Build() (*Team, error) { 14 | if b.t.id.IsNil() { 15 | return nil, ErrInvalidID 16 | } 17 | if b.members == nil { 18 | b.t.members = NewMembers() 19 | } else { 20 | b.t.members = NewMembersWith(b.members) 21 | } 22 | b.t.members.fixed = b.personal 23 | return b.t, nil 24 | } 25 | 26 | func (b *TeamBuilder) MustBuild() *Team { 27 | r, err := b.Build() 28 | if err != nil { 29 | panic(err) 30 | } 31 | return r 32 | } 33 | 34 | func (b *TeamBuilder) ID(id TeamID) *TeamBuilder { 35 | b.t.id = id 36 | return b 37 | } 38 | 39 | func (b *TeamBuilder) NewID() *TeamBuilder { 40 | b.t.id = NewTeamID() 41 | return b 42 | } 43 | 44 | func (b *TeamBuilder) Name(name string) *TeamBuilder { 45 | b.t.name = name 46 | return b 47 | } 48 | 49 | func (b *TeamBuilder) Members(members map[ID]Role) *TeamBuilder { 50 | b.members = members 51 | return b 52 | } 53 | 54 | func (b *TeamBuilder) Personal(p bool) *TeamBuilder { 55 | b.personal = p 56 | return b 57 | } 58 | -------------------------------------------------------------------------------- /internal/infrastructure/fs/plugin_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/reearth/reearth-backend/pkg/i18n" 8 | "github.com/reearth/reearth-backend/pkg/plugin" 9 | "github.com/spf13/afero" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestPlugin(t *testing.T) { 14 | ctx := context.Background() 15 | fs := NewPlugin(mockPluginFS()) 16 | p, err := fs.FindByID(ctx, plugin.MustID("testplugin~1.0.0")) 17 | assert.NoError(t, err) 18 | assert.Equal(t, plugin.New().ID(plugin.MustID("testplugin~1.0.0")).Name(i18n.String{ 19 | "en": "testplugin", 20 | "ja": "テストプラグイン", 21 | "zh-CN": "测试插件", 22 | }).MustBuild(), p) 23 | } 24 | 25 | func mockPluginFS() afero.Fs { 26 | files := map[string]string{ 27 | "plugins/testplugin~1.0.0/reearth.yml": `{ "id": "testplugin", "version": "1.0.0", "name": "testplugin" }`, 28 | "plugins/testplugin~1.0.0/reearth_ja.yml": `{ "name": "テストプラグイン" }`, 29 | "plugins/testplugin~1.0.0/reearth_zh-CN.yml": `{ "name": "测试插件" }`, 30 | } 31 | 32 | fs := afero.NewMemMapFs() 33 | for name, content := range files { 34 | f, _ := fs.Create(name) 35 | _, _ = f.WriteString(content) 36 | _ = f.Close() 37 | } 38 | return fs 39 | } 40 | -------------------------------------------------------------------------------- /pkg/tag/id.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | import "github.com/reearth/reearth-backend/pkg/id" 4 | 5 | type ID = id.TagID 6 | type SceneID = id.SceneID 7 | type DatasetID = id.DatasetID 8 | type DatasetSchemaID = id.DatasetSchemaID 9 | type DatasetFieldID = id.DatasetFieldID 10 | 11 | type IDList = id.TagIDList 12 | 13 | var NewID = id.NewTagID 14 | var NewSceneID = id.NewSceneID 15 | var NewDatasetID = id.NewDatasetID 16 | var NewDatasetSchemaID = id.NewDatasetSchemaID 17 | var NewDatasetFieldID = id.NewDatasetFieldID 18 | 19 | var MustID = id.MustTagID 20 | var MustSceneID = id.MustSceneID 21 | var MustDatasetID = id.MustDatasetID 22 | var MustDatasetSchemaID = id.MustDatasetSchemaID 23 | var MustDatasetFieldID = id.MustDatasetFieldID 24 | 25 | var IDFrom = id.TagIDFrom 26 | var SceneIDFrom = id.SceneIDFrom 27 | var DatasetIDFrom = id.DatasetIDFrom 28 | var DatasetSchemaIDFrom = id.DatasetSchemaIDFrom 29 | var DatasetFieldIDFrom = id.DatasetFieldIDFrom 30 | 31 | var IDFromRef = id.TagIDFromRef 32 | var SceneIDFromRef = id.SceneIDFromRef 33 | var DatasetIDFromRef = id.DatasetIDFromRef 34 | var DatasetSchemaIDFromRef = id.DatasetSchemaIDFromRef 35 | var DatasetFieldIDFromRef = id.DatasetFieldIDFromRef 36 | 37 | var ErrInvalidID = id.ErrInvalidID 38 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/mongo_test.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/reearth/reearth-backend/internal/infrastructure/mongo/mongodoc" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | "go.mongodb.org/mongo-driver/x/mongo/driver/uuid" 14 | ) 15 | 16 | func connect(t *testing.T) func(*testing.T) *mongodoc.Client { 17 | t.Helper() 18 | 19 | // Skip unit testing if "REEARTH_DB" is not configured 20 | // See details: https://github.com/reearth/reearth/issues/273 21 | db := os.Getenv("REEARTH_DB") 22 | if db == "" { 23 | t.SkipNow() 24 | return nil 25 | } 26 | 27 | c, _ := mongo.Connect( 28 | context.Background(), 29 | options.Client(). 30 | ApplyURI(db). 31 | SetConnectTimeout(time.Second*10), 32 | ) 33 | 34 | return func(t *testing.T) *mongodoc.Client { 35 | t.Helper() 36 | 37 | database, _ := uuid.New() 38 | databaseName := "reearth-test-" + hex.EncodeToString(database[:]) 39 | client := mongodoc.NewClient(databaseName, c) 40 | 41 | t.Cleanup(func() { 42 | _ = c.Database(databaseName).Drop(context.Background()) 43 | }) 44 | 45 | return client 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/infrastructure/memory/transaction.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase/repo" 7 | ) 8 | 9 | type Transaction struct { 10 | committed int 11 | beginerror error 12 | enderror error 13 | } 14 | 15 | type Tx struct { 16 | t *Transaction 17 | committed bool 18 | enderror error 19 | } 20 | 21 | func NewTransaction() *Transaction { 22 | return &Transaction{} 23 | } 24 | 25 | func (t *Transaction) SetBeginError(err error) { 26 | t.beginerror = err 27 | } 28 | 29 | func (t *Transaction) SetEndError(err error) { 30 | t.enderror = err 31 | } 32 | 33 | func (t *Transaction) Committed() int { 34 | return t.committed 35 | } 36 | 37 | func (t *Transaction) Begin() (repo.Tx, error) { 38 | if t.beginerror != nil { 39 | return nil, t.beginerror 40 | } 41 | return &Tx{t: t, enderror: t.enderror}, nil 42 | } 43 | 44 | func (t *Tx) Commit() { 45 | t.committed = true 46 | } 47 | 48 | func (t *Tx) End(_ context.Context) error { 49 | if t.enderror != nil { 50 | return t.enderror 51 | } 52 | if t.t != nil && t.committed { 53 | t.t.committed++ 54 | } 55 | return nil 56 | } 57 | 58 | func (t *Tx) IsCommitted() bool { 59 | return t.committed 60 | } 61 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/migration/210310145844_remove_preview_token.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/labstack/gommon/log" 7 | "github.com/reearth/reearth-backend/internal/infrastructure/mongo/mongodoc" 8 | "go.mongodb.org/mongo-driver/bson" 9 | ) 10 | 11 | func RemovePreviewToken(ctx context.Context, c DBClient) error { 12 | col := c.WithCollection("project") 13 | 14 | return col.Find(ctx, bson.D{}, &mongodoc.BatchConsumer{ 15 | Size: 1000, 16 | Callback: func(rows []bson.Raw) error { 17 | 18 | ids := make([]string, 0, len(rows)) 19 | newRows := make([]interface{}, 0, len(rows)) 20 | 21 | log.Infof("migration: RemoveProjectPreviewToken: hit projects: %d\n", len(rows)) 22 | 23 | for _, row := range rows { 24 | doc := bson.M{} 25 | if err := bson.Unmarshal(row, &doc); err != nil { 26 | return err 27 | } 28 | 29 | if doc["publishmentstatus"] == "limited" { 30 | pt := doc["previewtoken"] 31 | doc["alias"] = pt 32 | } 33 | delete(doc, "previewtoken") 34 | 35 | id := doc["id"].(string) 36 | ids = append(ids, id) 37 | newRows = append(newRows, doc) 38 | } 39 | 40 | return col.SaveAll(ctx, ids, newRows) 41 | }, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/dataset/schema_list_test.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDatasetSchemaMapGraphSearchByFields(t *testing.T) { 10 | did1 := NewSchemaID() 11 | did2 := NewSchemaID() 12 | did3 := NewSchemaID() 13 | fid1 := NewFieldID() 14 | fid2 := NewFieldID() 15 | fid3 := NewFieldID() 16 | sid := NewSceneID() 17 | f1, _ := NewSchemaField().ID(fid1).Type(ValueTypeString).Ref(&did2).Build() 18 | f2, _ := NewSchemaField().ID(fid2).Type(ValueTypeString).Ref(&did3).Build() 19 | f3, _ := NewSchemaField().ID(fid3).Type(ValueTypeString).Build() 20 | d1, _ := NewSchema().ID(did1).Scene(sid).Fields([]*SchemaField{ 21 | f1, 22 | }).Build() 23 | d2, _ := NewSchema().ID(did2).Scene(sid).Fields([]*SchemaField{ 24 | f2, 25 | }).Build() 26 | d3, _ := NewSchema().ID(did3).Scene(sid).Fields([]*SchemaField{ 27 | f3, 28 | }).Build() 29 | 30 | m := SchemaList{d1, d2, d3}.Map() 31 | 32 | res, resf := m.GraphSearchByFields(did1, fid1, fid2, fid3) 33 | assert.Equal(t, SchemaList{d1, d2, d3}, res) 34 | assert.Equal(t, f3, resf) 35 | 36 | res2, resf2 := m.GraphSearchByFields(did1, fid1, fid3, fid2) 37 | assert.Equal(t, SchemaList{d1, d2}, res2) 38 | assert.Nil(t, resf2) 39 | } 40 | -------------------------------------------------------------------------------- /internal/usecase/interactor/layer_test.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/reearth/reearth-backend/internal/infrastructure/memory" 8 | "github.com/reearth/reearth-backend/internal/usecase" 9 | "github.com/reearth/reearth-backend/pkg/id" 10 | "github.com/reearth/reearth-backend/pkg/layer" 11 | "github.com/reearth/reearth-backend/pkg/scene" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestCreateInfobox(t *testing.T) { 16 | ctx := context.Background() 17 | 18 | db := memory.New() 19 | scene, _ := scene.New().NewID().Team(id.NewTeamID()).Project(id.NewProjectID()).RootLayer(id.NewLayerID()).Build() 20 | _ = db.Scene.Save(ctx, scene) 21 | il := NewLayer(db) 22 | 23 | l, _ := layer.NewItem().NewID().Scene(scene.ID()).Build() 24 | _ = db.Layer.Save(ctx, l) 25 | 26 | i, _ := il.CreateInfobox(ctx, l.ID(), &usecase.Operator{ 27 | WritableScenes: []id.SceneID{scene.ID()}, 28 | }) 29 | assert.NotNil(t, i) 30 | l, err := db.Layer.FindItemByID(ctx, l.ID()) 31 | assert.NoError(t, err) 32 | infobox := l.Infobox() 33 | assert.NotNil(t, infobox) 34 | property, _ := db.Property.FindByID(ctx, infobox.Property()) 35 | assert.NotNil(t, property) 36 | assert.NotNil(t, property.Schema()) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/id/idx/ulid.go: -------------------------------------------------------------------------------- 1 | package idx 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | 8 | "github.com/oklog/ulid" 9 | "github.com/reearth/reearth-backend/pkg/util" 10 | ) 11 | 12 | var ( 13 | entropyLock sync.Mutex 14 | // not safe for concurrent 15 | entropy = ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0) 16 | ) 17 | 18 | func generateID() ulid.ULID { 19 | entropyLock.Lock() 20 | newID := ulid.MustNew(ulid.Timestamp(time.Now().UTC()), entropy) 21 | entropyLock.Unlock() 22 | return newID 23 | } 24 | 25 | func generateAllID(n int) []ulid.ULID { 26 | ids := make([]ulid.ULID, 0, n) 27 | entropyLock.Lock() 28 | for i := 0; i < n; i++ { 29 | newID := ulid.MustNew(ulid.Timestamp(time.Now().UTC()), entropy) 30 | ids = append(ids, newID) 31 | } 32 | entropyLock.Unlock() 33 | return ids 34 | } 35 | 36 | func parseID(id string) (parsedID ulid.ULID, e error) { 37 | if includeUpperCase(id) { 38 | return parsedID, ErrInvalidID 39 | } 40 | return ulid.Parse(id) 41 | } 42 | 43 | func includeUpperCase(s string) bool { 44 | for _, c := range s { 45 | if 'A' <= c && c <= 'Z' { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | 52 | func mustParseID(id string) ulid.ULID { 53 | return util.Must(parseID(id)) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/value/string_test.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_propertyString_I2V(t *testing.T) { 11 | s := "foobar" 12 | n := 1.12 13 | u, _ := url.Parse("https://reearth.io") 14 | 15 | tests := []struct { 16 | name string 17 | args []interface{} 18 | want1 interface{} 19 | want2 bool 20 | }{ 21 | { 22 | name: "string", 23 | args: []interface{}{s, &s}, 24 | want1: "foobar", 25 | want2: true, 26 | }, 27 | { 28 | name: "number", 29 | args: []interface{}{n, &n}, 30 | want1: "1.12", 31 | want2: true, 32 | }, 33 | { 34 | name: "url", 35 | args: []interface{}{u}, 36 | want1: "https://reearth.io", 37 | want2: true, 38 | }, 39 | { 40 | name: "nil", 41 | args: []interface{}{(*string)(nil), nil}, 42 | want1: nil, 43 | want2: false, 44 | }, 45 | } 46 | 47 | for _, tt := range tests { 48 | tt := tt 49 | t.Run(tt.name, func(t *testing.T) { 50 | t.Parallel() 51 | p := &propertyString{} 52 | for i, v := range tt.args { 53 | got1, got2 := p.I2V(v) 54 | assert.Equal(t, tt.want1, got1, "test %d", i) 55 | assert.Equal(t, tt.want2, got2, "test %d", i) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/tag/group_builder.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | type GroupBuilder struct { 4 | g *Group 5 | } 6 | 7 | func NewGroup() *GroupBuilder { 8 | return &GroupBuilder{g: &Group{}} 9 | } 10 | 11 | func GroupFrom(t Tag) *Group { 12 | li, ok := t.(*Group) 13 | if !ok { 14 | return nil 15 | } 16 | return li 17 | } 18 | 19 | func (b *GroupBuilder) Build() (*Group, error) { 20 | if b.g.id.IsNil() { 21 | return nil, ErrInvalidID 22 | } 23 | if b.g.sceneId.IsNil() { 24 | return nil, ErrInvalidSceneID 25 | } 26 | if b.g.label == "" { 27 | return nil, ErrEmptyLabel 28 | } 29 | return b.g, nil 30 | } 31 | 32 | func (b *GroupBuilder) MustBuild() *Group { 33 | res, err := b.Build() 34 | if err != nil { 35 | panic(err) 36 | } 37 | return res 38 | } 39 | 40 | func (b *GroupBuilder) ID(tid ID) *GroupBuilder { 41 | b.g.id = tid 42 | return b 43 | } 44 | 45 | func (b *GroupBuilder) NewID() *GroupBuilder { 46 | b.g.id = NewID() 47 | return b 48 | } 49 | 50 | func (b *GroupBuilder) Label(l string) *GroupBuilder { 51 | b.g.label = l 52 | return b 53 | } 54 | 55 | func (b *GroupBuilder) Scene(sid SceneID) *GroupBuilder { 56 | b.g.sceneId = sid 57 | return b 58 | } 59 | 60 | func (b *GroupBuilder) Tags(tl IDList) *GroupBuilder { 61 | b.g.tags = tl.Clone() 62 | return b 63 | } 64 | -------------------------------------------------------------------------------- /pkg/dataset/schema_list.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | type SchemaList []*Schema 4 | 5 | func (dsl SchemaList) Map() SchemaMap { 6 | if dsl == nil { 7 | return nil 8 | } 9 | m := SchemaMap{} 10 | for _, d := range dsl { 11 | if d != nil { 12 | m[d.ID()] = d 13 | } 14 | } 15 | return m 16 | } 17 | 18 | type SchemaMap map[SchemaID]*Schema 19 | 20 | func (dsm SchemaMap) Slice() SchemaList { 21 | if dsm == nil { 22 | return nil 23 | } 24 | res := make(SchemaList, 0, len(dsm)) 25 | for _, ds := range dsm { 26 | if ds != nil { 27 | res = append(res, ds) 28 | } 29 | } 30 | return res 31 | } 32 | 33 | func (dsm SchemaMap) GraphSearchByFields(root SchemaID, fields ...FieldID) (SchemaList, *SchemaField) { 34 | res := make(SchemaList, 0, len(fields)) 35 | currentDs := dsm[root] 36 | if currentDs == nil { 37 | return res, nil 38 | } 39 | for i, f := range fields { 40 | if currentDs == nil { 41 | return res, nil 42 | } 43 | res = append(res, currentDs) 44 | field := currentDs.Field(f) 45 | if field == nil { 46 | return res, nil 47 | } 48 | if len(fields)-1 == i { 49 | return res, field 50 | } else if r := field.Ref(); r != nil { 51 | currentDs = dsm[*r] 52 | } else { 53 | return res, nil 54 | } 55 | } 56 | return res, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/adapter/gql/gqlmodel/convert_tag.go: -------------------------------------------------------------------------------- 1 | package gqlmodel 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/pkg/id" 5 | "github.com/reearth/reearth-backend/pkg/tag" 6 | "github.com/reearth/reearth-backend/pkg/util" 7 | ) 8 | 9 | func ToTagItem(ti *tag.Item) *TagItem { 10 | if ti == nil { 11 | return nil 12 | } 13 | 14 | return &TagItem{ 15 | ID: IDFrom(ti.ID()), 16 | SceneID: IDFrom(ti.Scene()), 17 | Label: ti.Label(), 18 | ParentID: IDFromRef(ti.Parent()), 19 | LinkedDatasetID: IDFromRef(ti.LinkedDatasetID()), 20 | LinkedDatasetSchemaID: IDFromRef(ti.LinkedDatasetSchemaID()), 21 | LinkedDatasetFieldID: IDFromRef(ti.LinkedDatasetFieldID()), 22 | } 23 | } 24 | 25 | func ToTagGroup(tg *tag.Group) *TagGroup { 26 | if tg == nil { 27 | return nil 28 | } 29 | 30 | return &TagGroup{ 31 | ID: IDFrom(tg.ID()), 32 | SceneID: IDFrom(tg.Scene()), 33 | Label: tg.Label(), 34 | TagIds: util.Map(tg.Tags(), IDFrom[id.Tag]), 35 | } 36 | } 37 | 38 | func ToTag(t tag.Tag) Tag { 39 | if t == nil { 40 | return nil 41 | } 42 | 43 | switch ty := t.(type) { 44 | case *tag.Item: 45 | return ToTagItem(ty) 46 | case *tag.Group: 47 | return ToTagGroup(ty) 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver_property_test.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_actualValue(t *testing.T) { 11 | value := 300 12 | 13 | type args struct { 14 | datasetLoader DatasetDataLoader 15 | value interface{} 16 | links []*gqlmodel.PropertyFieldLink 17 | overridden bool 18 | } 19 | var tests = []struct { 20 | name string 21 | args args 22 | want interface{} 23 | wantErr bool 24 | }{ 25 | { 26 | "Overridden value", 27 | args{ 28 | datasetLoader: nil, 29 | value: value, 30 | links: nil, 31 | overridden: true, 32 | }, 33 | 300, 34 | false, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | tt := tt 40 | t.Run(tt.name, func(t *testing.T) { 41 | t.Parallel() 42 | got, err := actualValue(tt.args.datasetLoader, tt.args.value, tt.args.links, tt.args.overridden) 43 | if (err != nil) != tt.wantErr { 44 | t.Errorf("actualValue() error = %v, wantErr %v", err, tt.wantErr) 45 | return 46 | } 47 | temp := got.(*interface{}) 48 | t2 := (*temp).(int) 49 | assert.Equal(t, tt.want, t2) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/layer/infobox_field_builder.go: -------------------------------------------------------------------------------- 1 | package layer 2 | 3 | type InfoboxFieldBuilder struct { 4 | i *InfoboxField 5 | } 6 | 7 | func NewInfoboxField() *InfoboxFieldBuilder { 8 | return &InfoboxFieldBuilder{i: &InfoboxField{}} 9 | } 10 | 11 | func (b *InfoboxFieldBuilder) Build() (*InfoboxField, error) { 12 | if b.i.id.IsNil() || 13 | string(b.i.extension) == "" || 14 | b.i.property.IsNil() { 15 | return nil, ErrInvalidID 16 | } 17 | return b.i, nil 18 | } 19 | 20 | func (b *InfoboxFieldBuilder) MustBuild() *InfoboxField { 21 | i, err := b.Build() 22 | if err != nil { 23 | panic(err) 24 | } 25 | return i 26 | } 27 | 28 | func (b *InfoboxFieldBuilder) ID(id InfoboxFieldID) *InfoboxFieldBuilder { 29 | b.i.id = id 30 | return b 31 | } 32 | 33 | func (b *InfoboxFieldBuilder) NewID() *InfoboxFieldBuilder { 34 | b.i.id = NewInfoboxFieldID() 35 | return b 36 | } 37 | 38 | func (b *InfoboxFieldBuilder) Plugin(plugin PluginID) *InfoboxFieldBuilder { 39 | b.i.plugin = plugin 40 | return b 41 | } 42 | 43 | func (b *InfoboxFieldBuilder) Extension(extension PluginExtensionID) *InfoboxFieldBuilder { 44 | b.i.extension = extension 45 | return b 46 | } 47 | 48 | func (b *InfoboxFieldBuilder) Property(p PropertyID) *InfoboxFieldBuilder { 49 | b.i.property = p 50 | return b 51 | } 52 | -------------------------------------------------------------------------------- /internal/usecase/repo/property.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/property" 8 | ) 9 | 10 | type Property interface { 11 | Filtered(SceneFilter) Property 12 | FindByID(context.Context, id.PropertyID) (*property.Property, error) 13 | FindByIDs(context.Context, id.PropertyIDList) (property.List, error) 14 | FindLinkedAll(context.Context, id.SceneID) (property.List, error) 15 | FindByDataset(context.Context, id.DatasetSchemaID, id.DatasetID) (property.List, error) 16 | FindBySchema(context.Context, []id.PropertySchemaID, id.SceneID) (property.List, error) 17 | FindByPlugin(context.Context, id.PluginID, id.SceneID) (property.List, error) 18 | Save(context.Context, *property.Property) error 19 | SaveAll(context.Context, property.List) error 20 | UpdateSchemaPlugin(context.Context, id.PluginID, id.PluginID, id.SceneID) error 21 | Remove(context.Context, id.PropertyID) error 22 | RemoveAll(context.Context, id.PropertyIDList) error 23 | RemoveByScene(context.Context, id.SceneID) error 24 | } 25 | 26 | func PropertyLoaderFrom(r Property) property.Loader { 27 | return func(ctx context.Context, ids ...id.PropertyID) (property.List, error) { 28 | return r.FindByIDs(ctx, ids) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/dataset/id.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | import "github.com/reearth/reearth-backend/pkg/id" 4 | 5 | type ID = id.DatasetID 6 | type FieldID = id.DatasetFieldID 7 | type SchemaID = id.DatasetSchemaID 8 | type SceneID = id.SceneID 9 | 10 | var NewID = id.NewDatasetID 11 | var NewSchemaID = id.NewDatasetSchemaID 12 | var NewFieldID = id.NewDatasetFieldID 13 | var NewSceneID = id.NewSceneID 14 | 15 | var MustID = id.MustDatasetID 16 | var MustSchemaID = id.MustDatasetSchemaID 17 | var MustFieldID = id.MustDatasetFieldID 18 | var MustSceneID = id.MustSceneID 19 | 20 | var IDFrom = id.DatasetIDFrom 21 | var SchemaIDFrom = id.DatasetSchemaIDFrom 22 | var FieldIDFrom = id.DatasetFieldIDFrom 23 | var SceneIDFrom = id.SceneIDFrom 24 | 25 | var IDFromRef = id.DatasetIDFromRef 26 | var SchemaIDFromRef = id.DatasetSchemaIDFromRef 27 | var FieldIDFromRef = id.DatasetFieldIDFromRef 28 | var SceneIDFromRef = id.SceneIDFromRef 29 | 30 | type IDSet = id.DatasetIDSet 31 | type SchemaIDSet = id.DatasetSchemaIDSet 32 | type FieldIDSet = id.DatasetFieldIDSet 33 | type SceneIDSet = id.SceneIDSet 34 | 35 | var NewIDSet = id.NewDatasetIDSet 36 | var NewSchemaIDset = id.NewDatasetSchemaIDSet 37 | var NewFieldIDset = id.NewDatasetFieldIDSet 38 | var NewSceneIDset = id.NewSceneIDSet 39 | 40 | var ErrInvalidID = id.ErrInvalidID 41 | -------------------------------------------------------------------------------- /internal/usecase/interfaces/team.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/reearth/reearth-backend/internal/usecase" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | "github.com/reearth/reearth-backend/pkg/user" 10 | ) 11 | 12 | var ( 13 | ErrOwnerCannotLeaveTheTeam = errors.New("owner user cannot leave from the team") 14 | ErrCannotChangeOwnerRole = errors.New("cannot change the role of the team owner") 15 | ErrCannotDeleteTeam = errors.New("cannot delete team because at least one project is left") 16 | ) 17 | 18 | type Team interface { 19 | Fetch(context.Context, []id.TeamID, *usecase.Operator) ([]*user.Team, error) 20 | FindByUser(context.Context, id.UserID, *usecase.Operator) ([]*user.Team, error) 21 | Create(context.Context, string, id.UserID, *usecase.Operator) (*user.Team, error) 22 | Update(context.Context, id.TeamID, string, *usecase.Operator) (*user.Team, error) 23 | AddMember(context.Context, id.TeamID, id.UserID, user.Role, *usecase.Operator) (*user.Team, error) 24 | RemoveMember(context.Context, id.TeamID, id.UserID, *usecase.Operator) (*user.Team, error) 25 | UpdateMember(context.Context, id.TeamID, id.UserID, user.Role, *usecase.Operator) (*user.Team, error) 26 | Remove(context.Context, id.TeamID, *usecase.Operator) error 27 | } 28 | -------------------------------------------------------------------------------- /internal/app/usecase.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/labstack/echo/v4" 7 | "github.com/reearth/reearth-backend/internal/adapter" 8 | "github.com/reearth/reearth-backend/internal/usecase/gateway" 9 | "github.com/reearth/reearth-backend/internal/usecase/interactor" 10 | "github.com/reearth/reearth-backend/internal/usecase/repo" 11 | ) 12 | 13 | func UsecaseMiddleware(r *repo.Container, g *gateway.Container, config interactor.ContainerConfig) echo.MiddlewareFunc { 14 | return ContextMiddleware(func(ctx context.Context) context.Context { 15 | var r2 *repo.Container 16 | if op := adapter.Operator(ctx); op != nil && r != nil { 17 | // apply filters to repos 18 | r2 = r.Filtered( 19 | repo.TeamFilterFromOperator(op), 20 | repo.SceneFilterFromOperator(op), 21 | ) 22 | } else { 23 | r2 = r 24 | } 25 | 26 | uc := interactor.NewContainer(r2, g, config) 27 | ctx = adapter.AttachUsecases(ctx, &uc) 28 | return ctx 29 | }) 30 | } 31 | 32 | func ContextMiddleware(fn func(ctx context.Context) context.Context) echo.MiddlewareFunc { 33 | return func(next echo.HandlerFunc) echo.HandlerFunc { 34 | return func(c echo.Context) error { 35 | req := c.Request() 36 | c.SetRequest(req.WithContext(fn(req.Context()))) 37 | return next(c) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/layer/infobox_field.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/reearth/reearth-backend/tools/cmd/idgen --name InfoboxField --output ../id 2 | 3 | package layer 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/reearth/reearth-backend/pkg/property" 9 | ) 10 | 11 | type InfoboxField struct { 12 | id InfoboxFieldID 13 | plugin PluginID 14 | extension PluginExtensionID 15 | property PropertyID 16 | } 17 | 18 | func (i *InfoboxField) ID() InfoboxFieldID { 19 | return i.id 20 | } 21 | 22 | func (i *InfoboxField) Plugin() PluginID { 23 | return i.plugin 24 | } 25 | 26 | func (i *InfoboxField) Extension() PluginExtensionID { 27 | return i.extension 28 | } 29 | 30 | func (i *InfoboxField) Property() PropertyID { 31 | return i.property 32 | } 33 | 34 | func (i *InfoboxField) PropertyRef() *PropertyID { 35 | if i == nil { 36 | return nil 37 | } 38 | return i.property.Ref() 39 | } 40 | 41 | func (i *InfoboxField) ValidateProperty(pm property.Map) error { 42 | if i == nil || pm == nil { 43 | return nil 44 | } 45 | 46 | lp := pm[i.property] 47 | if lp == nil { 48 | return errors.New("property does not exist") 49 | } 50 | if !lp.Schema().Equal(NewPropertySchemaID(i.plugin, i.extension.String())) { 51 | return errors.New("property has a invalid schema") 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver_mutation_asset.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel" 7 | "github.com/reearth/reearth-backend/internal/usecase/interfaces" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | ) 10 | 11 | func (r *mutationResolver) CreateAsset(ctx context.Context, input gqlmodel.CreateAssetInput) (*gqlmodel.CreateAssetPayload, error) { 12 | tid, err := gqlmodel.ToID[id.Team](input.TeamID) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | res, err := usecases(ctx).Asset.Create(ctx, interfaces.CreateAssetParam{ 18 | TeamID: tid, 19 | File: gqlmodel.FromFile(&input.File), 20 | }, getOperator(ctx)) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return &gqlmodel.CreateAssetPayload{Asset: gqlmodel.ToAsset(res)}, nil 26 | } 27 | 28 | func (r *mutationResolver) RemoveAsset(ctx context.Context, input gqlmodel.RemoveAssetInput) (*gqlmodel.RemoveAssetPayload, error) { 29 | aid, err := gqlmodel.ToID[id.Asset](input.AssetID) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | res, err2 := usecases(ctx).Asset.Remove(ctx, aid, getOperator(ctx)) 35 | if err2 != nil { 36 | return nil, err2 37 | } 38 | 39 | return &gqlmodel.RemoveAssetPayload{AssetID: gqlmodel.IDFrom(res)}, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/value/polygon.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "github.com/mitchellh/mapstructure" 4 | 5 | var TypePolygon Type = "polygon" 6 | 7 | type Polygon []Coordinates 8 | 9 | func PolygonFrom(rings [][]float64) Polygon { 10 | p := make([]Coordinates, 0, len(rings)) 11 | for _, ring := range rings { 12 | p = append(p, CoordinatesFrom(ring)) 13 | } 14 | return p 15 | } 16 | 17 | type propertyPolygon struct{} 18 | 19 | func (p *propertyPolygon) I2V(i interface{}) (interface{}, bool) { 20 | if v, ok := i.(Polygon); ok { 21 | return v, true 22 | } 23 | 24 | if v, ok := i.(*Polygon); ok { 25 | if v != nil { 26 | return p.I2V(*v) 27 | } 28 | return nil, false 29 | } 30 | 31 | v := Polygon{} 32 | if err := mapstructure.Decode(i, &v); err == nil { 33 | return v, true 34 | } 35 | 36 | v2 := [][]float64{} 37 | if err := mapstructure.Decode(i, &v); err == nil { 38 | return PolygonFrom(v2), true 39 | } 40 | 41 | return nil, false 42 | } 43 | 44 | func (*propertyPolygon) V2I(v interface{}) (interface{}, bool) { 45 | return v, true 46 | } 47 | 48 | func (*propertyPolygon) Validate(i interface{}) bool { 49 | _, ok := i.(Polygon) 50 | return ok 51 | } 52 | 53 | func (v *Value) ValuePolygon() (vv Polygon, ok bool) { 54 | if v == nil { 55 | return 56 | } 57 | vv, ok = v.v.(Polygon) 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMust(t *testing.T) { 11 | a := &struct{}{} 12 | err := errors.New("ERR") 13 | assert.Same(t, a, Must(a, nil)) 14 | assert.PanicsWithValue(t, err, func() { 15 | _ = Must(a, err) 16 | }) 17 | } 18 | 19 | func TestIsZero(t *testing.T) { 20 | assert.True(t, IsZero(0)) 21 | assert.False(t, IsZero(-1)) 22 | assert.True(t, IsZero(struct { 23 | A int 24 | B string 25 | }{})) 26 | assert.False(t, IsZero(struct { 27 | A int 28 | B string 29 | }{A: 1})) 30 | assert.True(t, IsZero((*(struct{}))(nil))) 31 | assert.False(t, IsZero((*(struct{}))(&struct{}{}))) 32 | } 33 | 34 | func TestIsNotZero(t *testing.T) { 35 | assert.False(t, IsNotZero(0)) 36 | assert.True(t, IsNotZero(-1)) 37 | assert.False(t, IsNotZero(struct { 38 | A int 39 | B string 40 | }{})) 41 | assert.True(t, IsNotZero(struct { 42 | A int 43 | B string 44 | }{A: 1})) 45 | assert.False(t, IsNotZero((*(struct{}))(nil))) 46 | assert.True(t, IsNotZero((*(struct{}))(&struct{}{}))) 47 | } 48 | 49 | func TestDeref(t *testing.T) { 50 | assert.Equal(t, struct{ A int }{}, Deref((*(struct{ A int }))(nil))) 51 | assert.Equal(t, struct{ A int }{A: 1}, Deref((*(struct{ A int }))(&struct{ A int }{A: 1}))) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/value/latlng.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "github.com/mitchellh/mapstructure" 4 | 5 | type LatLng struct { 6 | Lat float64 `json:"lat" mapstructure:"lat"` 7 | Lng float64 `json:"lng" mapstructure:"lng"` 8 | } 9 | 10 | func (l *LatLng) Clone() *LatLng { 11 | if l == nil { 12 | return nil 13 | } 14 | return &LatLng{ 15 | Lat: l.Lat, 16 | Lng: l.Lng, 17 | } 18 | } 19 | 20 | var TypeLatLng Type = "latlng" 21 | 22 | type propertyLatLng struct{} 23 | 24 | func (p *propertyLatLng) I2V(i interface{}) (interface{}, bool) { 25 | switch v := i.(type) { 26 | case LatLng: 27 | return v, true 28 | case LatLngHeight: 29 | return LatLng{Lat: v.Lat, Lng: v.Lng}, true 30 | case *LatLng: 31 | if v != nil { 32 | return p.I2V(*v) 33 | } 34 | case *LatLngHeight: 35 | if v != nil { 36 | return p.I2V(*v) 37 | } 38 | } 39 | 40 | v := LatLng{} 41 | if err := mapstructure.Decode(i, &v); err != nil { 42 | return nil, false 43 | } 44 | return v, true 45 | } 46 | 47 | func (*propertyLatLng) V2I(v interface{}) (interface{}, bool) { 48 | return v, true 49 | } 50 | 51 | func (*propertyLatLng) Validate(i interface{}) bool { 52 | _, ok := i.(LatLng) 53 | return ok 54 | } 55 | 56 | func (v *Value) ValueLatLng() (vv LatLng, ok bool) { 57 | if v == nil { 58 | return 59 | } 60 | vv, ok = v.v.(LatLng) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/migration/220309174648_add_scene_field_to_property_schema.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/labstack/gommon/log" 7 | "github.com/reearth/reearth-backend/internal/infrastructure/mongo/mongodoc" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | "go.mongodb.org/mongo-driver/bson" 10 | ) 11 | 12 | func AddSceneFieldToPropertySchema(ctx context.Context, c DBClient) error { 13 | col := c.WithCollection("propertySchema") 14 | 15 | return col.Find(ctx, bson.M{}, &mongodoc.BatchConsumer{ 16 | Size: 1000, 17 | Callback: func(rows []bson.Raw) error { 18 | ids := make([]string, 0, len(rows)) 19 | newRows := make([]interface{}, 0, len(rows)) 20 | 21 | log.Infof("migration: AddSceneFieldToPropertySchema: hit property schemas: %d\n", len(rows)) 22 | 23 | for _, row := range rows { 24 | var doc mongodoc.PropertySchemaDocument 25 | if err := bson.Unmarshal(row, &doc); err != nil { 26 | return err 27 | } 28 | 29 | s, err := id.PropertySchemaIDFrom(doc.ID) 30 | if err != nil || s.Plugin().Scene() == nil { 31 | continue 32 | } 33 | 34 | doc.Scene = s.Plugin().Scene().StringRef() 35 | ids = append(ids, doc.ID) 36 | newRows = append(newRows, doc) 37 | } 38 | 39 | return col.SaveAll(ctx, ids, newRows) 40 | }, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/user/initializer.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "golang.org/x/text/language" 5 | ) 6 | 7 | type InitParams struct { 8 | Email string 9 | Name string 10 | Sub *Auth 11 | Password *string 12 | Lang *language.Tag 13 | Theme *Theme 14 | UserID *ID 15 | TeamID *TeamID 16 | } 17 | 18 | func Init(p InitParams) (*User, *Team, error) { 19 | if p.UserID == nil { 20 | p.UserID = NewID().Ref() 21 | } 22 | if p.TeamID == nil { 23 | p.TeamID = NewTeamID().Ref() 24 | } 25 | if p.Lang == nil { 26 | p.Lang = &language.Tag{} 27 | } 28 | if p.Theme == nil { 29 | t := ThemeDefault 30 | p.Theme = &t 31 | } 32 | if p.Sub == nil { 33 | p.Sub = GenReearthSub(p.UserID.String()) 34 | } 35 | 36 | b := New(). 37 | ID(*p.UserID). 38 | Name(p.Name). 39 | Email(p.Email). 40 | Auths([]Auth{*p.Sub}). 41 | Lang(*p.Lang). 42 | Theme(*p.Theme) 43 | if p.Password != nil { 44 | b = b.PasswordPlainText(*p.Password) 45 | } 46 | u, err := b.Build() 47 | if err != nil { 48 | return nil, nil, err 49 | } 50 | 51 | // create a user's own team 52 | t, err := NewTeam(). 53 | ID(*p.TeamID). 54 | Name(p.Name). 55 | Members(map[ID]Role{u.ID(): RoleOwner}). 56 | Personal(true). 57 | Build() 58 | if err != nil { 59 | return nil, nil, err 60 | } 61 | u.UpdateTeam(t.ID()) 62 | 63 | return u, t, err 64 | } 65 | -------------------------------------------------------------------------------- /pkg/user/team_list.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | type TeamList []*Team 4 | 5 | func (l TeamList) FilterByID(ids ...TeamID) TeamList { 6 | if l == nil { 7 | return nil 8 | } 9 | 10 | res := make(TeamList, 0, len(l)) 11 | for _, id := range ids { 12 | var t2 *Team 13 | for _, t := range l { 14 | if t.ID() == id { 15 | t2 = t 16 | break 17 | } 18 | } 19 | if t2 != nil { 20 | res = append(res, t2) 21 | } 22 | } 23 | return res 24 | } 25 | func (l TeamList) FilterByUserRole(u ID, r Role) TeamList { 26 | if l == nil || u.IsNil() || r == "" { 27 | return nil 28 | } 29 | 30 | res := make(TeamList, 0, len(l)) 31 | for _, t := range l { 32 | tr := t.Members().GetRole(u) 33 | if tr == r { 34 | res = append(res, t) 35 | } 36 | } 37 | return res 38 | } 39 | 40 | func (l TeamList) FilterByUserRoleIncluding(u ID, r Role) TeamList { 41 | if l == nil || u.IsNil() || r == "" { 42 | return nil 43 | } 44 | 45 | res := make(TeamList, 0, len(l)) 46 | for _, t := range l { 47 | tr := t.Members().GetRole(u) 48 | if tr.Includes(r) { 49 | res = append(res, t) 50 | } 51 | } 52 | return res 53 | } 54 | 55 | func (l TeamList) IDs() []TeamID { 56 | if l == nil { 57 | return nil 58 | } 59 | 60 | res := make([]TeamID, 0, len(l)) 61 | for _, t := range l { 62 | res = append(res, t.ID()) 63 | } 64 | return res 65 | } 66 | -------------------------------------------------------------------------------- /internal/adapter/gql/resolver_team.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/adapter/gql/gqlmodel" 7 | "github.com/reearth/reearth-backend/internal/usecase" 8 | ) 9 | 10 | func (r *Resolver) Team() TeamResolver { 11 | return &teamResolver{r} 12 | } 13 | 14 | func (r *Resolver) TeamMember() TeamMemberResolver { 15 | return &teamMemberResolver{r} 16 | } 17 | 18 | type teamResolver struct{ *Resolver } 19 | 20 | func (r *teamResolver) Assets(ctx context.Context, obj *gqlmodel.Team, first *int, last *int, after *usecase.Cursor, before *usecase.Cursor) (*gqlmodel.AssetConnection, error) { 21 | return loaders(ctx).Asset.FindByTeam(ctx, obj.ID, nil, nil, &gqlmodel.Pagination{ 22 | First: first, 23 | Last: last, 24 | After: after, 25 | Before: before, 26 | }) 27 | } 28 | 29 | func (r *teamResolver) Projects(ctx context.Context, obj *gqlmodel.Team, includeArchived *bool, first *int, last *int, after *usecase.Cursor, before *usecase.Cursor) (*gqlmodel.ProjectConnection, error) { 30 | return loaders(ctx).Project.FindByTeam(ctx, obj.ID, first, last, before, after) 31 | } 32 | 33 | type teamMemberResolver struct{ *Resolver } 34 | 35 | func (r *teamMemberResolver) User(ctx context.Context, obj *gqlmodel.TeamMember) (*gqlmodel.User, error) { 36 | return dataloaders(ctx).User.Load(obj.UserID) 37 | } 38 | -------------------------------------------------------------------------------- /internal/usecase/interfaces/published.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/url" 7 | 8 | "github.com/reearth/reearth-backend/pkg/project" 9 | ) 10 | 11 | type ProjectPublishedMetadata struct { 12 | Title string `json:"title,omitempty"` 13 | Description string `json:"description,omitempty"` 14 | Image string `json:"image,omitempty"` 15 | Noindex bool `json:"noindex,omitempty"` 16 | IsBasicAuthActive bool `json:"isBasicAuthActive,omitempty"` 17 | BasicAuthUsername string `json:"basicAuthUsername,omitempty"` 18 | BasicAuthPassword string `json:"basicAuthPassword,omitempty"` 19 | } 20 | 21 | func ProjectPublishedMetadataFrom(prj *project.Project) ProjectPublishedMetadata { 22 | return ProjectPublishedMetadata{ 23 | Title: prj.PublicTitle(), 24 | Description: prj.PublicDescription(), 25 | Image: prj.PublicImage(), 26 | Noindex: prj.PublicNoIndex(), 27 | IsBasicAuthActive: prj.IsBasicAuthActive(), 28 | BasicAuthUsername: prj.BasicAuthUsername(), 29 | BasicAuthPassword: prj.BasicAuthPassword(), 30 | } 31 | } 32 | 33 | type Published interface { 34 | Metadata(context.Context, string) (ProjectPublishedMetadata, error) 35 | Data(context.Context, string) (io.Reader, error) 36 | Index(context.Context, string, *url.URL) (string, error) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Cache holds data can be accessed synchronously. The data will be automatically updated when it expires. 10 | type Cache[T any] struct { 11 | updater func(context.Context, T) (T, error) 12 | expiresIn time.Duration 13 | updatedAt time.Time 14 | lock sync.Mutex 15 | data T 16 | now func() time.Time 17 | } 18 | 19 | func New[T any](updater func(context.Context, T) (T, error), expiresIn time.Duration) *Cache[T] { 20 | return &Cache[T]{updater: updater, expiresIn: expiresIn} 21 | } 22 | 23 | func (c *Cache[T]) Get(ctx context.Context) (res T, _ error) { 24 | if c == nil { 25 | return 26 | } 27 | 28 | c.lock.Lock() 29 | defer c.lock.Unlock() 30 | 31 | if c.updatedAt.IsZero() || c.updatedAt.Add(c.expiresIn).Before(c.currentTime()) { 32 | if err := c.update(ctx); err != nil { 33 | return c.data, err 34 | } 35 | } 36 | return c.data, nil 37 | } 38 | 39 | func (c *Cache[T]) update(ctx context.Context) error { 40 | var err error 41 | data, err := c.updater(ctx, c.data) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | c.data = data 47 | c.updatedAt = c.currentTime() 48 | return nil 49 | } 50 | 51 | func (c *Cache[T]) currentTime() time.Time { 52 | if c.now == nil { 53 | return time.Now() 54 | } 55 | return c.now() 56 | } 57 | -------------------------------------------------------------------------------- /pkg/plugin/pluginpack/package_test.go: -------------------------------------------------------------------------------- 1 | package pluginpack 2 | 3 | import ( 4 | "archive/zip" 5 | "os" 6 | "testing" 7 | 8 | "github.com/reearth/reearth-backend/pkg/i18n" 9 | "github.com/reearth/reearth-backend/pkg/plugin" 10 | "github.com/reearth/reearth-backend/pkg/plugin/manifest" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestPackageFromZip(t *testing.T) { 15 | expected := &manifest.Manifest{ 16 | Plugin: plugin.New(). 17 | ID(plugin.MustID("testplugin~1.0.1")). 18 | Name(i18n.String{"en": "testplugin", "ja": "テストプラグイン", "zh-CN": "测试插件"}). 19 | MustBuild(), 20 | } 21 | 22 | f, err := os.Open("testdata/test.zip") 23 | assert.NoError(t, err) 24 | defer func() { 25 | _ = f.Close() 26 | }() 27 | 28 | p, err := PackageFromZip(f, nil, 10000) 29 | assert.NoError(t, err) 30 | assert.Equal(t, expected, p.Manifest) 31 | 32 | var files []string 33 | for { 34 | n, err := p.Files.Next() 35 | assert.NoError(t, err) 36 | if n == nil { 37 | break 38 | } 39 | files = append(files, n.Path) 40 | } 41 | assert.Equal(t, []string{"index.js"}, files) 42 | } 43 | 44 | func TestPackageFromZip2(t *testing.T) { 45 | f, err := os.Open("testdata/test.zip") 46 | assert.NoError(t, err) 47 | defer func() { 48 | _ = f.Close() 49 | }() 50 | 51 | _, err = PackageFromZip(f, nil, 100) 52 | assert.ErrorIs(t, err, zip.ErrFormat) 53 | } 54 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/migration/201217132559_add_scene_widget_id.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/labstack/gommon/log" 7 | "github.com/reearth/reearth-backend/internal/infrastructure/mongo/mongodoc" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | "go.mongodb.org/mongo-driver/bson" 10 | ) 11 | 12 | func AddSceneWidgetId(ctx context.Context, c DBClient) error { 13 | col := c.WithCollection("scene") 14 | 15 | return col.Find(ctx, bson.D{}, &mongodoc.BatchConsumer{ 16 | Size: 1000, 17 | Callback: func(rows []bson.Raw) error { 18 | 19 | ids := make([]string, 0, len(rows)) 20 | newRows := make([]interface{}, 0, len(rows)) 21 | 22 | log.Infof("migration: AddSceneWidgetId: hit scenes: %d\n", len(rows)) 23 | 24 | for _, row := range rows { 25 | var doc mongodoc.SceneDocument 26 | if err := bson.Unmarshal(row, &doc); err != nil { 27 | return err 28 | } 29 | 30 | widgets := make([]mongodoc.SceneWidgetDocument, 0, len(doc.Widgets)) 31 | for _, w := range doc.Widgets { 32 | if w.ID == "" { 33 | w.ID = id.NewWidgetID().String() 34 | } 35 | widgets = append(widgets, w) 36 | } 37 | doc.Widgets = widgets 38 | 39 | ids = append(ids, doc.ID) 40 | newRows = append(newRows, doc) 41 | } 42 | 43 | return col.SaveAll(ctx, ids, newRows) 44 | }, 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/i18n/string.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | const DefaultLang = "en" 4 | 5 | type String map[string]string // key should use BCP 47 representation 6 | 7 | func StringFrom(s string) String { 8 | if s == "" { 9 | return String{} 10 | } 11 | return String{DefaultLang: s} 12 | } 13 | 14 | func (s String) WithDefault(d string) String { 15 | if s == nil && d == "" { 16 | return nil 17 | } 18 | 19 | res := s.Clone() 20 | if res == nil { 21 | res = String{} 22 | } 23 | if d != "" { 24 | res[DefaultLang] = d 25 | } 26 | return res 27 | } 28 | 29 | func (s String) WithDefaultRef(d *string) String { 30 | if d == nil { 31 | return s.Clone() 32 | } 33 | return s.WithDefault(*d) 34 | } 35 | 36 | func (s String) Translated(lang ...string) string { 37 | if s == nil { 38 | return "" 39 | } 40 | for _, l := range lang { 41 | if s, ok := s[l]; ok { 42 | return s 43 | } 44 | } 45 | return s.String() 46 | } 47 | 48 | func (s String) Clone() String { 49 | if len(s) == 0 { 50 | return nil 51 | } 52 | s2 := make(String, len(s)) 53 | for k, v := range s { 54 | s2[k] = v 55 | } 56 | return s2 57 | } 58 | 59 | func (s String) String() string { 60 | if s == nil { 61 | return "" 62 | } 63 | return s[DefaultLang] 64 | } 65 | 66 | func (s String) StringRef() *string { 67 | if s == nil { 68 | return nil 69 | } 70 | st := s[DefaultLang] 71 | return &st 72 | } 73 | -------------------------------------------------------------------------------- /internal/infrastructure/mailer/smtp.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "errors" 5 | "net/smtp" 6 | 7 | "github.com/reearth/reearth-backend/internal/usecase/gateway" 8 | ) 9 | 10 | type smtpMailer struct { 11 | host string 12 | port string 13 | email string 14 | username string 15 | password string 16 | } 17 | 18 | func NewSMTP(host, port, username, email, password string) gateway.Mailer { 19 | return &smtpMailer{ 20 | host: host, 21 | port: port, 22 | username: username, 23 | email: email, 24 | password: password, 25 | } 26 | } 27 | 28 | func (m *smtpMailer) SendMail(to []gateway.Contact, subject, plainContent, htmlContent string) error { 29 | emails, err := verifyEmails(to) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | msg := &message{ 35 | to: emails, 36 | from: m.email, 37 | subject: subject, 38 | plainContent: plainContent, 39 | htmlContent: htmlContent, 40 | } 41 | 42 | encodedMsg, err := msg.encodeMessage() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | auth := smtp.PlainAuth("", m.username, m.password, m.host) 48 | if len(m.host) == 0 { 49 | return errors.New("invalid smtp url") 50 | } 51 | 52 | if err := smtp.SendMail(m.host+":"+m.port, auth, m.email, emails, encodedMsg); err != nil { 53 | return err 54 | } 55 | 56 | logMail(to, subject) 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/plugin/manifest/parser_translation.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | // Generating types with schema typer for translation schema is disabled because some fields are wrongly typed. 4 | // DISABLED go:generate go run github.com/idubinskiy/schematyper -o schema_translation_gen.go --package manifest --prefix Translation ../../../schemas/plugin_manifest_translation.json 5 | 6 | import ( 7 | "errors" 8 | "io" 9 | 10 | "github.com/goccy/go-yaml" 11 | ) 12 | 13 | var ( 14 | ErrInvalidManifestTranslation error = errors.New("invalid manifest translation") 15 | ErrFailedToParseManifestTranslation error = errors.New("failed to parse plugin manifest translation") 16 | ) 17 | 18 | func ParseTranslation(source io.Reader) (TranslationRoot, error) { 19 | root := TranslationRoot{} 20 | if err := yaml.NewDecoder(source).Decode(&root); err != nil { 21 | return root, ErrFailedToParseManifestTranslation 22 | } 23 | 24 | return root, nil 25 | } 26 | 27 | func ParseTranslationFromBytes(source []byte) (TranslationRoot, error) { 28 | tr := TranslationRoot{} 29 | if err := yaml.Unmarshal(source, &tr); err != nil { 30 | return tr, ErrFailedToParseManifestTranslation 31 | } 32 | return tr, nil 33 | } 34 | 35 | func MustParseTranslationFromBytes(source []byte) TranslationRoot { 36 | m, err := ParseTranslationFromBytes(source) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return m 41 | } 42 | -------------------------------------------------------------------------------- /pkg/property/field_builder.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import "fmt" 4 | 5 | type FieldBuilder struct { 6 | p *Field 7 | } 8 | 9 | func NewField(field FieldID) *FieldBuilder { 10 | return &FieldBuilder{ 11 | p: &Field{ 12 | field: field, 13 | }, 14 | } 15 | } 16 | 17 | func FieldFrom(sf *SchemaField) *FieldBuilder { 18 | if sf == nil { 19 | return NewField("") 20 | } 21 | return &FieldBuilder{ 22 | p: &Field{ 23 | field: sf.ID(), 24 | v: NewOptionalValue(sf.Type(), nil), 25 | }, 26 | } 27 | } 28 | 29 | func (b *FieldBuilder) Build() *Field { 30 | if b.p.field == "" || b.p.v == nil { 31 | return nil 32 | } 33 | return b.p 34 | } 35 | 36 | func (b *FieldBuilder) MustBuild() *Field { 37 | f := b.Build() 38 | if f == nil { 39 | panic(fmt.Sprintf("field ID or type is invalid: id=%s, type=%s", b.p.field, b.p.v.Type())) 40 | } 41 | return f 42 | } 43 | 44 | func (b *FieldBuilder) Field(field FieldID) *FieldBuilder { 45 | b.p.field = field 46 | return b 47 | } 48 | 49 | func (b *FieldBuilder) Value(v *OptionalValue) *FieldBuilder { 50 | b.p.v = v.Clone() 51 | return b 52 | } 53 | 54 | func (b *FieldBuilder) Type(t ValueType) *FieldBuilder { 55 | if b.p.v.Type() != t { 56 | b.p.v = NewOptionalValue(t, nil) 57 | } 58 | return b 59 | } 60 | 61 | func (b *FieldBuilder) Links(l *Links) *FieldBuilder { 62 | b.p.links = l.Clone() 63 | return b 64 | } 65 | -------------------------------------------------------------------------------- /pkg/shp/sequentialreader_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func openFile(name string, t *testing.T) *os.File { 11 | f, err := os.Open(name) 12 | if err != nil { 13 | t.Fatalf("Failed to open %s: %v", name, err) 14 | } 15 | return f 16 | } 17 | 18 | func getShapesSequentially(prefix string, t *testing.T) (shapes []Shape) { 19 | shp := openFile(prefix+".shp", t) 20 | // dbf := openFile(prefix+".dbf", t) 21 | 22 | sr := SequentialReaderFromExt(shp /*, dbf*/) 23 | err := sr.Err() 24 | assert.Nil(t, err, "Error when iterating over the shapefile header") 25 | 26 | for sr.Next() { 27 | _, shape := sr.Shape() 28 | shapes = append(shapes, shape) 29 | } 30 | err = sr.Err() 31 | assert.Nil(t, err, "Error when iterating over the shapes") 32 | 33 | err = sr.Close() 34 | assert.Nil(t, err, "Could not close sequential reader") 35 | 36 | return shapes 37 | } 38 | 39 | func TestSequentialReader(t *testing.T) { 40 | tests := testData 41 | 42 | for _, tc := range tests { 43 | tc := tc 44 | t.Run(tc.name, func(t *testing.T) { 45 | t.Parallel() 46 | shapes := getShapesSequentially(tc.name, t) 47 | assert.Equal(t, tc.count, len(shapes), "Number of shapes for %s read was wrong. Wanted %d, got %d.", tc.name, tc.count, len(shapes)) 48 | tc.tester(t, tc.points, shapes) 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/dataset/schema_field_builder.go: -------------------------------------------------------------------------------- 1 | package dataset 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type SchemaFieldBuilder struct { 8 | d *SchemaField 9 | } 10 | 11 | func NewSchemaField() *SchemaFieldBuilder { 12 | return &SchemaFieldBuilder{d: &SchemaField{}} 13 | } 14 | 15 | func (b *SchemaFieldBuilder) Build() (*SchemaField, error) { 16 | if b.d.id.IsNil() { 17 | return nil, ErrInvalidID 18 | } 19 | if !b.d.dataType.Default() { 20 | return nil, errors.New("invalid value type") 21 | } 22 | return b.d, nil 23 | } 24 | 25 | func (b *SchemaFieldBuilder) MustBuild() *SchemaField { 26 | r, err := b.Build() 27 | if err != nil { 28 | panic(err) 29 | } 30 | return r 31 | } 32 | 33 | func (b *SchemaFieldBuilder) ID(id FieldID) *SchemaFieldBuilder { 34 | b.d.id = id 35 | return b 36 | } 37 | 38 | func (b *SchemaFieldBuilder) NewID() *SchemaFieldBuilder { 39 | b.d.id = NewFieldID() 40 | return b 41 | } 42 | 43 | func (b *SchemaFieldBuilder) Name(name string) *SchemaFieldBuilder { 44 | b.d.name = name 45 | return b 46 | } 47 | 48 | func (b *SchemaFieldBuilder) Type(dataType ValueType) *SchemaFieldBuilder { 49 | b.d.dataType = dataType 50 | return b 51 | } 52 | 53 | func (b *SchemaFieldBuilder) Source(source string) *SchemaFieldBuilder { 54 | b.d.source = source 55 | return b 56 | } 57 | 58 | func (b *SchemaFieldBuilder) Ref(ref *SchemaID) *SchemaFieldBuilder { 59 | b.d.ref = ref.CopyRef() 60 | return b 61 | } 62 | -------------------------------------------------------------------------------- /pkg/writer/seeker_closer.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | // reference: https://stackoverflow.com/questions/45836767/using-an-io-writeseeker-without-a-file-in-go 9 | type WriterSeeker struct { 10 | buffer []byte 11 | position int 12 | } 13 | 14 | func (sc *WriterSeeker) Write(p []byte) (int, error) { 15 | minCap := sc.position + len(p) 16 | if minCap > cap(sc.buffer) { 17 | b2 := make([]byte, len(sc.buffer), minCap+len(p)) 18 | copy(b2, sc.buffer) 19 | sc.buffer = b2 20 | } 21 | if minCap > len(sc.buffer) { 22 | sc.buffer = sc.buffer[:minCap] 23 | } 24 | copy(sc.buffer[sc.position:], p) 25 | sc.position += len(p) 26 | return len(p), nil 27 | } 28 | 29 | func (sc *WriterSeeker) Seek(offset int64, whence int) (int64, error) { 30 | newPos, offs := 0, int(offset) 31 | switch whence { 32 | case io.SeekStart: 33 | newPos = offs 34 | case io.SeekCurrent: 35 | newPos = sc.position + offs 36 | case io.SeekEnd: 37 | newPos = len(sc.buffer) + offs 38 | } 39 | if newPos < 0 { 40 | return 0, errors.New("negative result pos") 41 | } 42 | sc.position = newPos 43 | return int64(newPos), nil 44 | } 45 | 46 | func (sc *WriterSeeker) WriteTo(w io.Writer) (int64, error) { 47 | i, err := w.Write(sc.buffer) 48 | return int64(i), err 49 | } 50 | 51 | func (sc *WriterSeeker) Buffer() []byte { 52 | b := make([]byte, len(sc.buffer)) 53 | copy(b, sc.buffer) 54 | return b 55 | } 56 | -------------------------------------------------------------------------------- /pkg/scene/list_test.go: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestList_FilterByID(t *testing.T) { 10 | sid1 := NewID() 11 | sid2 := NewID() 12 | t1 := &Scene{id: sid1} 13 | t2 := &Scene{id: sid2} 14 | 15 | assert.Equal(t, List{t1}, List{t1, t2}.FilterByID(sid1)) 16 | assert.Equal(t, List{t2}, List{t1, t2}.FilterByID(sid2)) 17 | assert.Equal(t, List{t1, t2}, List{t1, t2}.FilterByID(sid1, sid2)) 18 | assert.Equal(t, List{}, List{t1, t2}.FilterByID(NewID())) 19 | assert.Equal(t, List(nil), List(nil).FilterByID(sid1)) 20 | } 21 | 22 | func TestList_FilterByTeam(t *testing.T) { 23 | tid1 := NewTeamID() 24 | tid2 := NewTeamID() 25 | t1 := &Scene{id: NewID(), team: tid1} 26 | t2 := &Scene{id: NewID(), team: tid2} 27 | 28 | assert.Equal(t, List{t1}, List{t1, t2}.FilterByTeam(tid1)) 29 | assert.Equal(t, List{t2}, List{t1, t2}.FilterByTeam(tid2)) 30 | assert.Equal(t, List{t1, t2}, List{t1, t2}.FilterByTeam(tid1, tid2)) 31 | assert.Equal(t, List{}, List{t1, t2}.FilterByTeam(NewTeamID())) 32 | assert.Equal(t, List(nil), List(nil).FilterByTeam(tid1)) 33 | } 34 | 35 | func TestTeamList_IDs(t *testing.T) { 36 | sid1 := NewID() 37 | sid2 := NewID() 38 | t1 := &Scene{id: sid1} 39 | t2 := &Scene{id: sid2} 40 | 41 | assert.Equal(t, []ID{sid1, sid2}, List{t1, t2}.IDs()) 42 | assert.Equal(t, []ID{}, List{}.IDs()) 43 | assert.Equal(t, []ID(nil), List(nil).IDs()) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/property/loader.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Loader func(context.Context, ...ID) (List, error) 8 | 9 | type SchemaLoader func(context.Context, ...SchemaID) (SchemaList, error) 10 | 11 | func LoaderFrom(data []*Property) Loader { 12 | return func(ctx context.Context, ids ...ID) (List, error) { 13 | res := make([]*Property, 0, len(ids)) 14 | for _, i := range ids { 15 | found := false 16 | for _, d := range data { 17 | if i == d.ID() { 18 | res = append(res, d) 19 | found = true 20 | break 21 | } 22 | } 23 | if !found { 24 | res = append(res, nil) 25 | } 26 | } 27 | return res, nil 28 | } 29 | } 30 | 31 | func LoaderFromMap(data map[ID]*Property) Loader { 32 | return func(ctx context.Context, ids ...ID) (List, error) { 33 | res := make([]*Property, 0, len(ids)) 34 | for _, i := range ids { 35 | if d, ok := data[i]; ok { 36 | res = append(res, d) 37 | } else { 38 | res = append(res, nil) 39 | } 40 | } 41 | return res, nil 42 | } 43 | } 44 | 45 | func SchemaLoaderFromMap(data map[SchemaID]*Schema) SchemaLoader { 46 | return func(ctx context.Context, ids ...SchemaID) (SchemaList, error) { 47 | res := make([]*Schema, 0, len(ids)) 48 | for _, i := range ids { 49 | if d, ok := data[i]; ok { 50 | res = append(res, d) 51 | } else { 52 | res = append(res, nil) 53 | } 54 | } 55 | return res, nil 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/value/type.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | type Type string 4 | 5 | type TypeProperty interface { 6 | I2V(interface{}) (interface{}, bool) 7 | V2I(interface{}) (interface{}, bool) 8 | Validate(interface{}) bool 9 | } 10 | 11 | type TypePropertyMap = map[Type]TypeProperty 12 | 13 | var TypeUnknown = Type("") 14 | 15 | var defaultTypes = TypePropertyMap{ 16 | TypeBool: &propertyBool{}, 17 | TypeCoordinates: &propertyCoordinates{}, 18 | TypeLatLng: &propertyLatLng{}, 19 | TypeLatLngHeight: &propertyLatLngHeight{}, 20 | TypeNumber: &propertyNumber{}, 21 | TypePolygon: &propertyPolygon{}, 22 | TypeRect: &propertyRect{}, 23 | TypeRef: &propertyRef{}, 24 | TypeString: &propertyString{}, 25 | TypeURL: &propertyURL{}, 26 | } 27 | 28 | func (t Type) Default() bool { 29 | _, ok := defaultTypes[t] 30 | return ok 31 | } 32 | 33 | func (t Type) None() *Optional { 34 | return NewOptional(t, nil) 35 | } 36 | 37 | func (t Type) ValueFrom(i interface{}, p TypePropertyMap) *Value { 38 | if t == TypeUnknown || i == nil { 39 | return nil 40 | } 41 | 42 | if p != nil { 43 | if vt, ok := p[t]; ok && vt != nil { 44 | if v, ok2 := vt.I2V(i); ok2 { 45 | return &Value{p: p, v: v, t: t} 46 | } 47 | } 48 | } 49 | 50 | if vt, ok := defaultTypes[t]; ok && vt != nil { 51 | if v, ok2 := vt.I2V(i); ok2 { 52 | return &Value{p: p, v: v, t: t} 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/migration/220214180713_split_schema_of_properties.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/labstack/gommon/log" 7 | "github.com/reearth/reearth-backend/internal/infrastructure/mongo/mongodoc" 8 | "github.com/reearth/reearth-backend/pkg/id" 9 | "go.mongodb.org/mongo-driver/bson" 10 | ) 11 | 12 | func SplitSchemaOfProperties(ctx context.Context, c DBClient) error { 13 | col := c.WithCollection("property") 14 | 15 | return col.Find(ctx, bson.M{ 16 | "schema": bson.M{"$exists": true}, 17 | }, &mongodoc.BatchConsumer{ 18 | Size: 1000, 19 | Callback: func(rows []bson.Raw) error { 20 | ids := make([]string, 0, len(rows)) 21 | newRows := make([]interface{}, 0, len(rows)) 22 | 23 | log.Infof("migration: SplitSchemaOfProperties: hit properties: %d\n", len(rows)) 24 | 25 | for _, row := range rows { 26 | var doc mongodoc.PropertyDocument 27 | if err := bson.Unmarshal(row, &doc); err != nil { 28 | return err 29 | } 30 | if doc.Schema == "" { 31 | continue 32 | } 33 | 34 | s, err := id.PropertySchemaIDFrom(doc.Schema) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | doc.Schema = "" 40 | doc.SchemaPlugin = s.Plugin().String() 41 | doc.SchemaName = s.ID() 42 | 43 | ids = append(ids, doc.ID) 44 | newRows = append(newRows, doc) 45 | } 46 | 47 | return col.SaveAll(ctx, ids, newRows) 48 | }, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /internal/infrastructure/mongo/mongodoc/consumer_test.go: -------------------------------------------------------------------------------- 1 | package mongodoc 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "go.mongodb.org/mongo-driver/bson" 9 | ) 10 | 11 | var _ Consumer = FuncConsumer(nil) 12 | 13 | func TestBatchConsumer(t *testing.T) { 14 | c := &BatchConsumer{ 15 | Size: 10, 16 | Callback: func(r []bson.Raw) error { 17 | assert.Equal(t, []bson.Raw{[]byte{0}, []byte{1}, []byte{2}, []byte{3}, []byte{4}}, r) 18 | return nil 19 | }, 20 | } 21 | 22 | for i := 0; i < 5; i++ { 23 | r := bson.Raw([]byte{byte(i)}) 24 | assert.Nil(t, c.Consume(r)) 25 | } 26 | assert.Nil(t, c.Consume(nil)) 27 | } 28 | 29 | func TestBatchConsumerWithManyRows(t *testing.T) { 30 | counter := 0 31 | c := &BatchConsumer{ 32 | Size: 1, 33 | Callback: func(r []bson.Raw) error { 34 | if counter >= 5 { 35 | assert.Equal(t, []bson.Raw{}, r) 36 | return nil 37 | } 38 | assert.Equal(t, []bson.Raw{[]byte{byte(counter)}}, r) 39 | counter++ 40 | return nil 41 | }, 42 | } 43 | 44 | for i := 0; i < 5; i++ { 45 | r := bson.Raw([]byte{byte(i)}) 46 | assert.Nil(t, c.Consume(r)) 47 | } 48 | assert.Nil(t, c.Consume(nil)) 49 | } 50 | 51 | func TestBatchConsumerWithError(t *testing.T) { 52 | c := &BatchConsumer{ 53 | Size: 1, 54 | Callback: func(r []bson.Raw) error { 55 | return errors.New("hoge") 56 | }, 57 | } 58 | 59 | assert.EqualError(t, c.Consume(nil), "hoge") 60 | } 61 | -------------------------------------------------------------------------------- /pkg/value/bool_test.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_propertyBool_I2V(t *testing.T) { 10 | tr := true 11 | fa := false 12 | trs1 := "true" 13 | trs2 := "TRUE" 14 | trs3 := "True" 15 | trs4 := "T" 16 | trs5 := "t" 17 | trs6 := "1" 18 | fas1 := "false" 19 | fas2 := "FALSE" 20 | fas3 := "False" 21 | fas4 := "F" 22 | fas5 := "f" 23 | fas6 := "0" 24 | 25 | tests := []struct { 26 | name string 27 | args []interface{} 28 | want1 interface{} 29 | want2 bool 30 | }{ 31 | { 32 | name: "true", 33 | args: []interface{}{tr, trs1, trs2, trs3, trs4, trs5, trs6, &tr, &trs1, &trs2, &trs3, &trs4, &trs5, &trs6}, 34 | want1: true, 35 | want2: true, 36 | }, 37 | { 38 | name: "false", 39 | args: []interface{}{fa, fas1, fas2, fas3, fas4, fas5, fas6, &fa, &fas1, &fas2, &fas3, &fas4, &fas5, &fas6}, 40 | want1: false, 41 | want2: true, 42 | }, 43 | { 44 | name: "nil", 45 | args: []interface{}{"foo", (*bool)(nil), (*string)(nil), nil}, 46 | want1: nil, 47 | want2: false, 48 | }, 49 | } 50 | 51 | for _, tt := range tests { 52 | tt := tt 53 | t.Run(tt.name, func(t *testing.T) { 54 | t.Parallel() 55 | 56 | p := &propertyBool{} 57 | for i, v := range tt.args { 58 | got1, got2 := p.I2V(v) 59 | assert.Equal(t, tt.want1, got1, "test %d", i) 60 | assert.Equal(t, tt.want2, got2, "test %d", i) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/usecase/repo/project.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/usecase" 7 | "github.com/reearth/reearth-backend/pkg/id" 8 | "github.com/reearth/reearth-backend/pkg/project" 9 | ) 10 | 11 | type Project interface { 12 | Filtered(TeamFilter) Project 13 | FindByIDs(context.Context, id.ProjectIDList) ([]*project.Project, error) 14 | FindByID(context.Context, id.ProjectID) (*project.Project, error) 15 | FindByTeam(context.Context, id.TeamID, *usecase.Pagination) ([]*project.Project, *usecase.PageInfo, error) 16 | FindByPublicName(context.Context, string) (*project.Project, error) 17 | CountByTeam(context.Context, id.TeamID) (int, error) 18 | Save(context.Context, *project.Project) error 19 | Remove(context.Context, id.ProjectID) error 20 | } 21 | 22 | func IterateProjectsByTeam(repo Project, ctx context.Context, tid id.TeamID, batch int, callback func([]*project.Project) error) error { 23 | pagination := usecase.NewPagination(&batch, nil, nil, nil) 24 | 25 | for { 26 | projects, info, err := repo.FindByTeam(ctx, tid, pagination) 27 | if err != nil { 28 | return err 29 | } 30 | if len(projects) == 0 { 31 | break 32 | } 33 | 34 | if err := callback(projects); err != nil { 35 | return err 36 | } 37 | 38 | if !info.HasNextPage() { 39 | break 40 | } 41 | 42 | c := usecase.Cursor(projects[len(projects)-1].ID().String()) 43 | pagination.After = &c 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/property/builder.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | type Builder struct { 4 | p *Property 5 | } 6 | 7 | func New() *Builder { 8 | return &Builder{p: &Property{}} 9 | } 10 | 11 | func (b *Builder) Build() (*Property, error) { 12 | if b.p.id.IsNil() { 13 | return nil, ErrInvalidID 14 | } 15 | if b.p.scene.IsNil() { 16 | return nil, ErrInvalidSceneID 17 | } 18 | if b.p.schema.IsNil() { 19 | return nil, ErrInvalidPropertySchemaID 20 | } 21 | return b.p, nil 22 | } 23 | 24 | func (b *Builder) MustBuild() *Property { 25 | p, err := b.Build() 26 | if err != nil { 27 | panic(err) 28 | } 29 | return p 30 | } 31 | 32 | func (b *Builder) ID(id ID) *Builder { 33 | b.p.id = id 34 | return b 35 | } 36 | 37 | func (b *Builder) NewID() *Builder { 38 | b.p.id = NewID() 39 | return b 40 | } 41 | 42 | func (b *Builder) Scene(s SceneID) *Builder { 43 | b.p.scene = s 44 | return b 45 | } 46 | 47 | func (b *Builder) Schema(schema SchemaID) *Builder { 48 | b.p.schema = schema 49 | return b 50 | } 51 | 52 | func (b *Builder) Items(items []Item) *Builder { 53 | if len(items) == 0 { 54 | b.p.items = nil 55 | return b 56 | } 57 | 58 | newItems := make([]Item, 0, len(items)) 59 | ids := map[ItemID]struct{}{} 60 | for _, f := range items { 61 | if f == nil { 62 | continue 63 | } 64 | if _, ok := ids[f.ID()]; ok { 65 | continue 66 | } 67 | ids[f.ID()] = struct{}{} 68 | newItems = append(newItems, f) 69 | } 70 | 71 | b.p.items = newItems 72 | return b 73 | } 74 | -------------------------------------------------------------------------------- /pkg/property/schema_builder.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrInvalidSceneID = errors.New("invalid scene id") 9 | ErrInvalidPropertySchemaID = errors.New("invalid property schema id") 10 | ErrInvalidValue = errors.New("invalid value") 11 | ErrInvalidPropertyLinkableField = errors.New("invalid property linkable field") 12 | ErrInvalidVersion = errors.New("invalid version") 13 | ) 14 | 15 | type SchemaBuilder struct { 16 | p *Schema 17 | } 18 | 19 | func NewSchema() *SchemaBuilder { 20 | return &SchemaBuilder{p: &Schema{}} 21 | } 22 | 23 | func (b *SchemaBuilder) Build() (*Schema, error) { 24 | if b.p.id.IsNil() { 25 | return nil, ErrInvalidID 26 | } 27 | if !b.p.linkable.Validate(b.p) { 28 | return nil, ErrInvalidPropertyLinkableField 29 | } 30 | return b.p, nil 31 | } 32 | 33 | func (b *SchemaBuilder) MustBuild() *Schema { 34 | p, err := b.Build() 35 | if err != nil { 36 | panic(err) 37 | } 38 | return p 39 | } 40 | 41 | func (b *SchemaBuilder) ID(id SchemaID) *SchemaBuilder { 42 | b.p.id = id 43 | return b 44 | } 45 | 46 | func (b *SchemaBuilder) Version(version int) *SchemaBuilder { 47 | b.p.version = version 48 | return b 49 | } 50 | 51 | func (b *SchemaBuilder) Groups(groups *SchemaGroupList) *SchemaBuilder { 52 | b.p.groups = groups 53 | return b 54 | } 55 | 56 | func (b *SchemaBuilder) LinkableFields(l LinkableFields) *SchemaBuilder { 57 | b.p.linkable = l 58 | return b 59 | } 60 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":semanticCommits", 5 | ":semanticCommitScopeDisabled", 6 | ":maintainLockFilesWeekly", 7 | ":enableVulnerabilityAlertsWithLabel(security)" 8 | ], 9 | "postUpdateOptions": [ 10 | "gomodTidy", 11 | "gomodUpdateImportPaths" 12 | ], 13 | "packageRules": [ 14 | { 15 | "enabledManagers": [ 16 | "gomod" 17 | ], 18 | "matchPackagePatterns": [ 19 | "*" 20 | ], 21 | "groupName": "dependencies", 22 | "groupSlug": "gomod", 23 | "semanticCommitType": "chore", 24 | "schedule": [ 25 | "before 3:00 am on the 4th day of the month" 26 | ] 27 | }, 28 | { 29 | "enabledManagers": [ 30 | "dockerfile", 31 | "docker-compose" 32 | ], 33 | "matchPackagePatterns": [ 34 | "*" 35 | ], 36 | "groupName": "docker dependencies", 37 | "groupSlug": "docker", 38 | "semanticCommitType": "chore", 39 | "schedule": [ 40 | "before 3:00 am on the 4th day of the month" 41 | ] 42 | }, 43 | { 44 | "enabledManagers": [ 45 | "github-actions" 46 | ], 47 | "matchPackagePatterns": [ 48 | "*" 49 | ], 50 | "groupName": "github actions dependencies", 51 | "groupSlug": "github-actions", 52 | "semanticCommitType": "ci", 53 | "schedule": [ 54 | "before 3am on the fourth day of the month" 55 | ] 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /pkg/property/value_dataset.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "github.com/reearth/reearth-backend/pkg/dataset" 5 | "github.com/reearth/reearth-backend/pkg/value" 6 | ) 7 | 8 | type ValueAndDatasetValue struct { 9 | t ValueType 10 | d *dataset.Value 11 | p *Value 12 | } 13 | 14 | func NewValueAndDatasetValue(ty ValueType, d *dataset.Value, p *Value) *ValueAndDatasetValue { 15 | if !ty.Valid() { 16 | return nil 17 | } 18 | 19 | if d != nil && ValueType(d.Type()) != ty { 20 | d = d.Cast(dataset.ValueType(ty)) 21 | } 22 | 23 | if p != nil && p.Type() != ty { 24 | p = p.Cast(ty) 25 | } 26 | 27 | return &ValueAndDatasetValue{ 28 | t: ty, 29 | d: d, 30 | p: p, 31 | } 32 | } 33 | 34 | func (v *ValueAndDatasetValue) Type() ValueType { 35 | if v == nil { 36 | return ValueTypeUnknown 37 | } 38 | return v.t 39 | } 40 | 41 | func (v *ValueAndDatasetValue) DatasetValue() *dataset.Value { 42 | if v == nil || v.t == ValueTypeUnknown { 43 | return nil 44 | } 45 | return v.d 46 | } 47 | 48 | func (v *ValueAndDatasetValue) PropertyValue() *Value { 49 | if v == nil || v.t == ValueTypeUnknown { 50 | return nil 51 | } 52 | return v.p 53 | } 54 | 55 | func (v *ValueAndDatasetValue) Value() *Value { 56 | if v == nil || v.t == ValueTypeUnknown { 57 | return nil 58 | } 59 | if v.d != nil { 60 | return valueFromDataset(v.d) 61 | } 62 | return v.p 63 | } 64 | 65 | func valueFromDataset(v *dataset.Value) *Value { 66 | return ValueType(value.Type(v.Type())).ValueFrom(v.Value()) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/shp/shapetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ShapeType"; DO NOT EDIT. 2 | 3 | package shp 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[NULL-0] 12 | _ = x[POINT-1] 13 | _ = x[POLYLINE-3] 14 | _ = x[POLYGON-5] 15 | _ = x[MULTIPOINT-8] 16 | _ = x[POINTZ-11] 17 | _ = x[POLYLINEZ-13] 18 | _ = x[POLYGONZ-15] 19 | _ = x[MULTIPOINTZ-18] 20 | _ = x[POINTM-21] 21 | _ = x[POLYLINEM-23] 22 | _ = x[POLYGONM-25] 23 | _ = x[MULTIPOINTM-28] 24 | _ = x[MULTIPATCH-31] 25 | } 26 | 27 | const _ShapeType_name = "NULLPOINTPOLYLINEPOLYGONMULTIPOINTPOINTZPOLYLINEZPOLYGONZMULTIPOINTZPOINTMPOLYLINEMPOLYGONMMULTIPOINTMMULTIPATCH" 28 | 29 | var _ShapeType_map = map[ShapeType]string{ 30 | 0: _ShapeType_name[0:4], 31 | 1: _ShapeType_name[4:9], 32 | 3: _ShapeType_name[9:17], 33 | 5: _ShapeType_name[17:24], 34 | 8: _ShapeType_name[24:34], 35 | 11: _ShapeType_name[34:40], 36 | 13: _ShapeType_name[40:49], 37 | 15: _ShapeType_name[49:57], 38 | 18: _ShapeType_name[57:68], 39 | 21: _ShapeType_name[68:74], 40 | 23: _ShapeType_name[74:83], 41 | 25: _ShapeType_name[83:91], 42 | 28: _ShapeType_name[91:102], 43 | 31: _ShapeType_name[102:112], 44 | } 45 | 46 | func (i ShapeType) String() string { 47 | if str, ok := _ShapeType_map[i]; ok { 48 | return str 49 | } 50 | return "ShapeType(" + strconv.FormatInt(int64(i), 10) + ")" 51 | } 52 | -------------------------------------------------------------------------------- /pkg/value/latlngheight.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "github.com/mitchellh/mapstructure" 4 | 5 | type LatLngHeight struct { 6 | Lat float64 `json:"lat" mapstructure:"lat"` 7 | Lng float64 `json:"lng" mapstructure:"lng"` 8 | Height float64 `json:"height" mapstructure:"height"` 9 | } 10 | 11 | func (l *LatLngHeight) Clone() *LatLngHeight { 12 | if l == nil { 13 | return nil 14 | } 15 | return &LatLngHeight{ 16 | Lat: l.Lat, 17 | Lng: l.Lng, 18 | Height: l.Height, 19 | } 20 | } 21 | 22 | var TypeLatLngHeight Type = "latlngheight" 23 | 24 | type propertyLatLngHeight struct{} 25 | 26 | func (p *propertyLatLngHeight) I2V(i interface{}) (interface{}, bool) { 27 | switch v := i.(type) { 28 | case LatLngHeight: 29 | return v, true 30 | case LatLng: 31 | return LatLngHeight{Lat: v.Lat, Lng: v.Lng, Height: 0}, true 32 | case *LatLngHeight: 33 | if v != nil { 34 | return p.I2V(*v) 35 | } 36 | case *LatLng: 37 | if v != nil { 38 | return p.I2V(*v) 39 | } 40 | } 41 | 42 | v := LatLngHeight{} 43 | if err := mapstructure.Decode(i, &v); err == nil { 44 | return v, true 45 | } 46 | return nil, false 47 | } 48 | 49 | func (*propertyLatLngHeight) V2I(v interface{}) (interface{}, bool) { 50 | return v, true 51 | } 52 | 53 | func (*propertyLatLngHeight) Validate(i interface{}) bool { 54 | _, ok := i.(LatLngHeight) 55 | return ok 56 | } 57 | 58 | func (v *Value) ValueLatLngHeight() (vv LatLngHeight, ok bool) { 59 | if v == nil { 60 | return 61 | } 62 | vv, ok = v.v.(LatLngHeight) 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /pkg/id/idx/set.go: -------------------------------------------------------------------------------- 1 | package idx 2 | 3 | type Set[T Type] struct { 4 | l List[T] 5 | m map[ID[T]]struct{} 6 | } 7 | 8 | func NewSet[T Type](id ...ID[T]) *Set[T] { 9 | s := &Set[T]{} 10 | s.Add(id...) 11 | return s 12 | } 13 | 14 | func (s *Set[T]) Has(id ...ID[T]) bool { 15 | if s == nil || s.m == nil { 16 | return false 17 | } 18 | for _, i := range id { 19 | if _, ok := s.m[i]; ok { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | func (s *Set[T]) List() List[T] { 27 | if s == nil { 28 | return nil 29 | } 30 | return s.l.Clone() 31 | } 32 | 33 | func (s *Set[T]) Clone() *Set[T] { 34 | if s == nil { 35 | return nil 36 | } 37 | return NewSet(s.l...) 38 | } 39 | 40 | func (s *Set[T]) Add(id ...ID[T]) { 41 | if s == nil { 42 | return 43 | } 44 | for _, i := range id { 45 | if !s.Has(i) { 46 | if s.m == nil { 47 | s.m = map[ID[T]]struct{}{} 48 | } 49 | s.m[i] = struct{}{} 50 | s.l = append(s.l, i) 51 | } 52 | } 53 | } 54 | 55 | func (s *Set[T]) Merge(sets ...*Set[T]) { 56 | if s == nil { 57 | return 58 | } 59 | for _, t := range sets { 60 | if t != nil { 61 | s.Add(t.l...) 62 | } 63 | } 64 | } 65 | 66 | func (s *Set[T]) Concat(sets ...*Set[T]) *Set[T] { 67 | if s == nil { 68 | return nil 69 | } 70 | ns := s.Clone() 71 | ns.Merge(sets...) 72 | return ns 73 | } 74 | 75 | func (s *Set[T]) Delete(id ...ID[T]) { 76 | if s == nil { 77 | return 78 | } 79 | for _, i := range id { 80 | s.l = s.l.Delete(i) 81 | if s.m != nil { 82 | delete(s.m, i) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/asset/builder.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Builder struct { 8 | a *Asset 9 | } 10 | 11 | func New() *Builder { 12 | return &Builder{a: &Asset{}} 13 | } 14 | 15 | func (b *Builder) Build() (*Asset, error) { 16 | if b.a.id.IsNil() { 17 | return nil, ErrInvalidID 18 | } 19 | if b.a.team.IsNil() { 20 | return nil, ErrEmptyTeamID 21 | } 22 | if b.a.url == "" { 23 | return nil, ErrEmptyURL 24 | } 25 | if b.a.size <= 0 { 26 | return nil, ErrEmptySize 27 | } 28 | if b.a.createdAt.IsZero() { 29 | b.a.createdAt = b.a.CreatedAt() 30 | } 31 | return b.a, nil 32 | } 33 | 34 | func (b *Builder) MustBuild() *Asset { 35 | r, err := b.Build() 36 | if err != nil { 37 | panic(err) 38 | } 39 | return r 40 | } 41 | 42 | func (b *Builder) ID(id ID) *Builder { 43 | b.a.id = id 44 | return b 45 | } 46 | 47 | func (b *Builder) NewID() *Builder { 48 | b.a.id = NewID() 49 | return b 50 | } 51 | 52 | func (b *Builder) Team(team TeamID) *Builder { 53 | b.a.team = team 54 | return b 55 | } 56 | 57 | func (b *Builder) Name(name string) *Builder { 58 | b.a.name = name 59 | return b 60 | } 61 | 62 | func (b *Builder) Size(size int64) *Builder { 63 | b.a.size = size 64 | return b 65 | } 66 | 67 | func (b *Builder) URL(url string) *Builder { 68 | b.a.url = url 69 | return b 70 | } 71 | 72 | func (b *Builder) ContentType(contentType string) *Builder { 73 | b.a.contentType = contentType 74 | return b 75 | } 76 | 77 | func (b *Builder) CreatedAt(createdAt time.Time) *Builder { 78 | b.a.createdAt = createdAt 79 | return b 80 | } 81 | -------------------------------------------------------------------------------- /pkg/value/optional.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | type Optional struct { 4 | t Type 5 | v *Value 6 | } 7 | 8 | func NewOptional(t Type, v *Value) *Optional { 9 | if t == TypeUnknown || (v != nil && v.Type() != t) { 10 | return nil 11 | } 12 | return &Optional{ 13 | t: t, 14 | v: v, 15 | } 16 | } 17 | 18 | func OptionalFrom(v *Value) *Optional { 19 | if v.Type() == TypeUnknown { 20 | return nil 21 | } 22 | return &Optional{ 23 | t: v.Type(), 24 | v: v, 25 | } 26 | } 27 | 28 | func (ov *Optional) Type() Type { 29 | if ov == nil { 30 | return TypeUnknown 31 | } 32 | return ov.t 33 | } 34 | 35 | func (ov *Optional) Value() *Value { 36 | if ov == nil || ov.t == TypeUnknown || ov.v == nil { 37 | return nil 38 | } 39 | return ov.v.Clone() 40 | } 41 | 42 | func (ov *Optional) TypeAndValue() (Type, *Value) { 43 | return ov.Type(), ov.Value() 44 | } 45 | 46 | func (ov *Optional) SetValue(v *Value) { 47 | if ov == nil || ov.t == TypeUnknown || (v != nil && ov.t != v.Type()) { 48 | return 49 | } 50 | ov.v = v.Clone() 51 | } 52 | 53 | func (ov *Optional) Clone() *Optional { 54 | if ov == nil { 55 | return nil 56 | } 57 | return &Optional{ 58 | t: ov.t, 59 | v: ov.v.Clone(), 60 | } 61 | } 62 | 63 | // Cast tries to convert the value to the new type and generates a new Optional. 64 | func (ov *Optional) Cast(t Type, p TypePropertyMap) *Optional { 65 | if ov == nil || ov.t == TypeUnknown { 66 | return nil 67 | } 68 | if ov.v == nil { 69 | return NewOptional(t, nil) 70 | } 71 | 72 | nv := ov.v.Cast(t, p) 73 | return NewOptional(t, nv) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/layer/decoding/decoder.go: -------------------------------------------------------------------------------- 1 | package decoding 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/reearth/reearth-backend/pkg/layer" 7 | "github.com/reearth/reearth-backend/pkg/property" 8 | ) 9 | 10 | type Decoder interface { 11 | Decode() (Result, error) 12 | } 13 | 14 | type Result struct { 15 | Root *layer.IDList 16 | Layers layer.Map 17 | Properties property.Map 18 | } 19 | 20 | func (r Result) RootLayers() layer.List { 21 | return r.Layers.Pick(r.Root) 22 | } 23 | 24 | func (r Result) Merge(r2 Result) Result { 25 | root := r.Root.Clone() 26 | root.Merge(r2.Root) 27 | return Result{ 28 | Root: root, 29 | Layers: r.Layers.Merge(r2.Layers), 30 | Properties: r.Properties.Merge(r2.Properties), 31 | } 32 | } 33 | 34 | func (r Result) MergeInitializerResult(r2 layer.InitializerResult) Result { 35 | return Result{ 36 | Root: r.Root.Clone().AppendLayers(r2.Root), 37 | Layers: r.Layers.Merge(r2.Layers), 38 | Properties: r.Properties.Merge(r2.Properties), 39 | } 40 | } 41 | 42 | func (r Result) Validate() error { 43 | for _, l := range r.Layers.List().Deref() { 44 | if err := l.ValidateProperties(r.Properties); err != nil { 45 | return fmt.Errorf("layer %s is invalid: %w", l.ID(), err) 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func resultFrom(lg *layer.Group, layers layer.Map, properties property.Map) (r Result, err error) { 52 | r = Result{ 53 | Root: layer.NewIDList([]layer.ID{lg.ID()}), 54 | Layers: layers.Add(lg.LayerRef()), 55 | Properties: properties, 56 | } 57 | err = r.Validate() 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /internal/adapter/gql/context.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/internal/adapter" 7 | "github.com/reearth/reearth-backend/internal/usecase" 8 | "github.com/reearth/reearth-backend/internal/usecase/interfaces" 9 | "github.com/reearth/reearth-backend/pkg/user" 10 | "golang.org/x/text/language" 11 | ) 12 | 13 | type ContextKey string 14 | 15 | const ( 16 | contextLoaders ContextKey = "loaders" 17 | contextDataloaders ContextKey = "dataloaders" 18 | ) 19 | 20 | func AttachUsecases(ctx context.Context, u *interfaces.Container, enableDataLoaders bool) context.Context { 21 | loaders := NewLoaders(u) 22 | dataloaders := loaders.DataLoadersWith(ctx, enableDataLoaders) 23 | 24 | ctx = adapter.AttachUsecases(ctx, u) 25 | ctx = context.WithValue(ctx, contextLoaders, loaders) 26 | ctx = context.WithValue(ctx, contextDataloaders, dataloaders) 27 | 28 | return ctx 29 | } 30 | 31 | func getUser(ctx context.Context) *user.User { 32 | return adapter.User(ctx) 33 | } 34 | 35 | func getLang(ctx context.Context, lang *language.Tag) string { 36 | return adapter.Lang(ctx, lang) 37 | } 38 | 39 | func getOperator(ctx context.Context) *usecase.Operator { 40 | return adapter.Operator(ctx) 41 | } 42 | 43 | func usecases(ctx context.Context) *interfaces.Container { 44 | return adapter.Usecases(ctx) 45 | } 46 | 47 | func loaders(ctx context.Context) *Loaders { 48 | return ctx.Value(contextLoaders).(*Loaders) 49 | } 50 | 51 | func dataloaders(ctx context.Context) *DataLoaders { 52 | return ctx.Value(contextDataloaders).(*DataLoaders) 53 | } 54 | -------------------------------------------------------------------------------- /internal/usecase/pageinfo.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | type PageInfo struct { 4 | totalCount int 5 | startCursor *Cursor 6 | endCursor *Cursor 7 | hasNextPage bool 8 | hasPreviousPage bool 9 | } 10 | 11 | func NewPageInfo(totalCount int, startCursor *Cursor, endCursor *Cursor, hasNextPage bool, hasPreviousPage bool) *PageInfo { 12 | var sc Cursor 13 | var ec Cursor 14 | if startCursor != nil { 15 | sc = *startCursor 16 | } 17 | if endCursor != nil { 18 | ec = *endCursor 19 | } 20 | 21 | return &PageInfo{ 22 | totalCount: totalCount, 23 | startCursor: &sc, 24 | endCursor: &ec, 25 | hasNextPage: hasNextPage, 26 | hasPreviousPage: hasPreviousPage, 27 | } 28 | } 29 | 30 | func EmptyPageInfo() *PageInfo { 31 | return &PageInfo{ 32 | totalCount: 0, 33 | startCursor: nil, 34 | endCursor: nil, 35 | hasNextPage: false, 36 | hasPreviousPage: false, 37 | } 38 | } 39 | 40 | func (p *PageInfo) TotalCount() int { 41 | if p == nil { 42 | return 0 43 | } 44 | return p.totalCount 45 | } 46 | 47 | func (p *PageInfo) StartCursor() *Cursor { 48 | if p == nil { 49 | return nil 50 | } 51 | return p.startCursor 52 | } 53 | 54 | func (p *PageInfo) EndCursor() *Cursor { 55 | if p == nil { 56 | return nil 57 | } 58 | return p.endCursor 59 | } 60 | 61 | func (p *PageInfo) HasNextPage() bool { 62 | if p == nil { 63 | return false 64 | } 65 | return p.hasNextPage 66 | } 67 | 68 | func (p *PageInfo) HasPreviousPage() bool { 69 | if p == nil { 70 | return false 71 | } 72 | return p.hasPreviousPage 73 | } 74 | -------------------------------------------------------------------------------- /pkg/plugin/list_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestList_Find(t *testing.T) { 10 | p1 := &Plugin{id: MustID("foo~1.0.0")} 11 | p2 := &Plugin{id: MustID("bar~1.0.0")} 12 | assert.Equal(t, p1, List{p1, p2}.Find(p1.ID())) 13 | assert.Nil(t, List{p1, p2}.Find(MustID("hoge~1.0.0"))) 14 | assert.Nil(t, List(nil).Find(p1.ID())) 15 | } 16 | 17 | func TestList_Concat(t *testing.T) { 18 | p1 := &Plugin{id: MustID("foo~1.0.0")} 19 | p2 := &Plugin{id: MustID("bar~1.0.0")} 20 | assert.Equal(t, List{p1, p2, p2}, List{p1, p2}.Concat(List{p2})) 21 | assert.Equal(t, List{p1}, List(nil).Concat(List{p1})) 22 | assert.Equal(t, List{p1}, List{p1}.Concat(nil)) 23 | } 24 | 25 | func TestList_Map(t *testing.T) { 26 | p1 := &Plugin{id: MustID("foo~1.0.0")} 27 | p2 := &Plugin{id: MustID("bar~1.0.0")} 28 | assert.Equal(t, Map{p1.ID(): p1, p2.ID(): p2}, List{p1, p2}.Map()) 29 | assert.Equal(t, Map{}, List(nil).Map()) 30 | } 31 | 32 | func TestList_MapToIDs(t *testing.T) { 33 | p1 := &Plugin{id: MustID("foo~1.0.0")} 34 | p2 := &Plugin{id: MustID("bar~1.0.0")} 35 | assert.Equal(t, List{nil, p2}, List{p1, p2}.MapToIDs([]ID{MustID("hoge~1.0.0"), p2.ID()})) 36 | assert.Equal(t, List{}, List{p1, p2}.MapToIDs(nil)) 37 | assert.Equal(t, List{nil}, List(nil).MapToIDs([]ID{p1.ID()})) 38 | } 39 | 40 | func TestMap_List(t *testing.T) { 41 | p1 := &Plugin{id: MustID("foo~1.0.0")} 42 | p2 := &Plugin{id: MustID("bar~1.0.0")} 43 | assert.Equal(t, List{p1, p2}, Map{p1.ID(): p1, p2.ID(): p2}.List()) 44 | assert.Nil(t, Map(nil).List()) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/czml/czml.go: -------------------------------------------------------------------------------- 1 | package czml 2 | 3 | type Feature struct { 4 | Id string `json:"id"` 5 | Name string `json:"name"` 6 | Polygon *Polygon `json:"polygon,omitempty"` 7 | Polyline *Polyline `json:"polyline,omitempty"` 8 | Position *Position `json:"position,omitempty"` 9 | Point *Point `json:"point,omitempty"` 10 | } 11 | type Polyline struct { 12 | Positions Position `json:"positions"` 13 | Material *Material `json:"material,omitempty"` 14 | Width float64 `json:"width,omitempty"` 15 | } 16 | type Polygon struct { 17 | Positions Position `json:"positions"` 18 | Fill bool `json:"fill,omitempty"` 19 | Material *Material `json:"material,omitempty"` 20 | Stroke bool `json:"outline,omitempty"` 21 | StrokeColor *Color `json:"outlineColor,omitempty"` 22 | StrokeWidth float64 `json:"outlineWidth,omitempty"` 23 | } 24 | type Point struct { 25 | Color string `json:"color,omitempty"` 26 | PixelSize float64 `json:"pixelSize,omitempty"` 27 | } 28 | type Position struct { 29 | CartographicDegrees []float64 `json:"cartographicDegrees"` 30 | } 31 | type Material struct { 32 | SolidColor *SolidColor `json:"solidColor,omitempty"` 33 | PolylineOutline *PolylineOutline `json:"polylineOutline,omitempty"` 34 | } 35 | type PolylineOutline struct { 36 | Color *Color `json:"color"` 37 | } 38 | type SolidColor struct { 39 | Color *Color `json:"color"` 40 | } 41 | type Color struct { 42 | RGBA []int64 `json:"rgba,omitempty"` 43 | RGBAF []float64 `json:"rgbaf,omitempty"` 44 | Reference string `json:"reference,omitempty"` 45 | } 46 | -------------------------------------------------------------------------------- /pkg/property/item.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/dataset" 7 | ) 8 | 9 | type Item interface { 10 | ID() ItemID 11 | IDRef() *ItemID 12 | SchemaGroup() SchemaGroupID 13 | SchemaGroupRef() *SchemaGroupID 14 | HasLinkedField() bool 15 | Datasets() []DatasetID 16 | FieldsByLinkedDataset(DatasetSchemaID, DatasetID) []*Field 17 | IsDatasetLinked(DatasetSchemaID, DatasetID) bool 18 | IsEmpty() bool 19 | Prune() bool 20 | MigrateSchema(context.Context, *Schema, dataset.Loader) 21 | MigrateDataset(DatasetMigrationParam) 22 | ValidateSchema(*SchemaGroup) error 23 | Fields(*Pointer) []*Field 24 | RemoveFields(*Pointer) bool 25 | CloneItem() Item 26 | GroupAndFields(*Pointer) []GroupAndField 27 | GuessSchema() *SchemaGroup 28 | } 29 | 30 | type itemBase struct { 31 | ID ItemID 32 | SchemaGroup SchemaGroupID 33 | } 34 | 35 | func ToGroup(i Item) *Group { 36 | g, _ := i.(*Group) 37 | return g 38 | } 39 | 40 | func ToGroupList(i Item) *GroupList { 41 | g, _ := i.(*GroupList) 42 | return g 43 | } 44 | 45 | func InitItemFrom(psg *SchemaGroup) Item { 46 | if psg == nil { 47 | return nil 48 | } 49 | if psg.IsList() { 50 | return InitGroupListFrom(psg) 51 | } 52 | return InitGroupFrom(psg) 53 | } 54 | 55 | type GroupAndField struct { 56 | ParentGroup *GroupList 57 | Group *Group 58 | Field *Field 59 | } 60 | 61 | func (f GroupAndField) SchemaFieldPointer() SchemaFieldPointer { 62 | return SchemaFieldPointer{ 63 | SchemaGroup: f.Group.SchemaGroup(), 64 | Field: f.Field.Field(), 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/layer/builder.go: -------------------------------------------------------------------------------- 1 | package layer 2 | 3 | type Builder struct { 4 | base layerBase 5 | } 6 | 7 | func New() *Builder { 8 | return &Builder{base: layerBase{visible: true}} 9 | } 10 | 11 | func (b *Builder) Group() *GroupBuilder { 12 | return NewGroup().base(b.base) 13 | } 14 | 15 | func (b *Builder) Item() *ItemBuilder { 16 | return NewItem().base(b.base) 17 | } 18 | 19 | func (b *Builder) ID(id ID) *Builder { 20 | b.base.id = id 21 | return b 22 | } 23 | 24 | func (b *Builder) NewID() *Builder { 25 | b.base.id = NewID() 26 | return b 27 | } 28 | 29 | func (b *Builder) Scene(s SceneID) *Builder { 30 | b.base.scene = s 31 | return b 32 | } 33 | 34 | func (b *Builder) Name(name string) *Builder { 35 | b.base.name = name 36 | return b 37 | } 38 | 39 | func (b *Builder) IsVisible(visible bool) *Builder { 40 | b.base.visible = visible 41 | return b 42 | } 43 | 44 | func (b *Builder) IsVisibleRef(visible *bool) *Builder { 45 | if visible != nil { 46 | b.base.visible = *visible 47 | } 48 | return b 49 | } 50 | 51 | func (b *Builder) Plugin(plugin *PluginID) *Builder { 52 | b.base.plugin = plugin.CopyRef() 53 | return b 54 | } 55 | 56 | func (b *Builder) Extension(extension *PluginExtensionID) *Builder { 57 | b.base.extension = extension.CloneRef() 58 | return b 59 | } 60 | 61 | func (b *Builder) Property(p *PropertyID) *Builder { 62 | b.base.property = p.CopyRef() 63 | return b 64 | } 65 | 66 | func (b *Builder) Infobox(infobox *Infobox) *Builder { 67 | b.base.infobox = infobox 68 | return b 69 | } 70 | 71 | func (b *Builder) Tags(tags *TagList) *Builder { 72 | b.base.tags = tags 73 | return b 74 | } 75 | -------------------------------------------------------------------------------- /internal/usecase/repo/tag.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/reearth/reearth-backend/pkg/id" 7 | "github.com/reearth/reearth-backend/pkg/tag" 8 | ) 9 | 10 | type Tag interface { 11 | Filtered(SceneFilter) Tag 12 | FindByID(context.Context, id.TagID) (tag.Tag, error) 13 | FindByIDs(context.Context, id.TagIDList) ([]*tag.Tag, error) 14 | FindByScene(context.Context, id.SceneID) ([]*tag.Tag, error) 15 | FindItemByID(context.Context, id.TagID) (*tag.Item, error) 16 | FindItemByIDs(context.Context, id.TagIDList) ([]*tag.Item, error) 17 | FindGroupByID(context.Context, id.TagID) (*tag.Group, error) 18 | FindGroupByIDs(context.Context, id.TagIDList) ([]*tag.Group, error) 19 | FindRootsByScene(context.Context, id.SceneID) ([]*tag.Tag, error) 20 | FindGroupByItem(context.Context, id.TagID) (*tag.Group, error) 21 | Save(context.Context, tag.Tag) error 22 | SaveAll(context.Context, []*tag.Tag) error 23 | Remove(context.Context, id.TagID) error 24 | RemoveAll(context.Context, id.TagIDList) error 25 | RemoveByScene(context.Context, id.SceneID) error 26 | } 27 | 28 | func TagLoaderFrom(r Tag) tag.Loader { 29 | return func(ctx context.Context, ids ...id.TagID) ([]*tag.Tag, error) { 30 | return r.FindByIDs(ctx, ids) 31 | } 32 | } 33 | 34 | func TagSceneLoaderFrom(r Tag, scenes []id.SceneID) tag.SceneLoader { 35 | return func(ctx context.Context, id id.SceneID) ([]*tag.Tag, error) { 36 | found := false 37 | for _, s := range scenes { 38 | if id == s { 39 | found = true 40 | break 41 | } 42 | } 43 | if !found { 44 | return nil, nil 45 | } 46 | return r.FindByScene(ctx, id) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/user/password_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "golang.org/x/crypto/bcrypt" 8 | ) 9 | 10 | func TestBcryptPasswordEncoder(t *testing.T) { 11 | got, err := (&BcryptPasswordEncoder{}).Encode("abc") 12 | assert.NoError(t, err) 13 | err = bcrypt.CompareHashAndPassword(got, []byte("abc")) 14 | assert.NoError(t, err) 15 | 16 | ok, err := (&BcryptPasswordEncoder{}).Verify("abc", got) 17 | assert.NoError(t, err) 18 | assert.True(t, ok) 19 | ok, err = (&BcryptPasswordEncoder{}).Verify("abcd", got) 20 | assert.NoError(t, err) 21 | assert.False(t, ok) 22 | } 23 | 24 | func TestMockPasswordEncoder(t *testing.T) { 25 | got, err := (&MockPasswordEncoder{Mock: []byte("ABC")}).Encode("ABC") 26 | assert.NoError(t, err) 27 | assert.Equal(t, got, []byte("ABC")) 28 | got, err = (&MockPasswordEncoder{Mock: []byte("ABC")}).Encode("abc") 29 | assert.NoError(t, err) 30 | assert.Equal(t, got, []byte("ABC")) 31 | 32 | ok, err := (&MockPasswordEncoder{Mock: []byte("ABC")}).Verify("ABC", got) 33 | assert.NoError(t, err) 34 | assert.True(t, ok) 35 | ok, err = (&MockPasswordEncoder{Mock: []byte("ABC")}).Verify("abc", got) 36 | assert.NoError(t, err) 37 | assert.False(t, ok) 38 | } 39 | 40 | func TestNoopPasswordEncoder(t *testing.T) { 41 | got, err := (&NoopPasswordEncoder{}).Encode("abc") 42 | assert.NoError(t, err) 43 | assert.Equal(t, got, []byte("abc")) 44 | 45 | ok, err := (&NoopPasswordEncoder{}).Verify("abc", got) 46 | assert.NoError(t, err) 47 | assert.True(t, ok) 48 | ok, err = (&NoopPasswordEncoder{}).Verify("abcd", got) 49 | assert.NoError(t, err) 50 | assert.False(t, ok) 51 | } 52 | --------------------------------------------------------------------------------