diff --git a/.github/workflows/go-test-firebase.yml b/.github/workflows/go-test-firebase.yml new file mode 100644 index 0000000..e7abf57 --- /dev/null +++ b/.github/workflows/go-test-firebase.yml @@ -0,0 +1,53 @@ +name: GoTestFirebaseEmulator +on: + push: + branches: + - main + - develop + pull_request: + paths: + - '**.go' + - '.github/**' +jobs: + go-test-with-firebase-emulator: + runs-on: ubuntu-latest + container: + image: ghcr.io/a-company-jp/imagemagick + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + packages: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + cache: true + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Download Go modules + shell: bash + if: ${{ steps.setup-go.outputs.cache-hit != 'true' }} + run: go mod download + - name: Install firebase-emulator + run: npm install -g firebase-tools + - name: Run Firebase Emulator + run: | + firebase setup:emulators:database + firebase emulators:start --only database & + - name: 'Waiting for the emulator to start up' + run: sleep 20 + - name: Run tests + env: + FIREBASE_DATABASE_EMULATOR_HOST: localhost:9000 + ENV_LOCATION: ${{ github.workspace }}/pkg/config/setting.testing.yaml + run: go test -p=1 -race -coverprofile=coverage.txt -covermode=atomic ./... #gosetup diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 0000000..416d844 --- /dev/null +++ b/database.rules.json @@ -0,0 +1,11 @@ +{ + "rules": { + ".read": "now < 1680274800000", + ".write": "now < 1680274800000", + "LGTMs": { + ".indexOn": [ + "createdAt" + ] + } + } +} diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..36a314c --- /dev/null +++ b/firebase.json @@ -0,0 +1,16 @@ +{ + "emulators": { + "database": { + "host": "127.0.0.1", + "port": 9000 + }, + "ui": { + "host": "127.0.0.1", + "enabled": false + }, + "singleProjectMode": true + }, + "database": { + "rules": "database.rules.json" + } +} diff --git a/go.mod b/go.mod index bddffec..4f32a5c 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,13 @@ module lgtm-gen go 1.22.0 require ( - cloud.google.com/go/firestore v1.14.0 cloud.google.com/go/storage v1.40.0 + cloud.google.com/go/vision v1.2.0 + cloud.google.com/go/vision/v2 v2.8.0 + firebase.google.com/go/v4 v4.14.0 github.com/gin-contrib/cors v1.7.1 github.com/gin-gonic/gin v1.9.1 + github.com/godruoyi/go-snowflake v0.0.2 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 google.golang.org/api v0.170.0 @@ -18,10 +21,10 @@ require ( cloud.google.com/go v0.112.1 // indirect cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/firestore v1.15.0 // indirect cloud.google.com/go/iam v1.1.7 // indirect cloud.google.com/go/longrunning v0.5.5 // indirect - cloud.google.com/go/vision v1.2.0 // indirect - cloud.google.com/go/vision/v2 v2.8.0 // indirect + github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/bytedance/sonic v1.11.3 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect @@ -34,6 +37,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.7 // indirect @@ -64,6 +68,7 @@ require ( golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect + google.golang.org/appengine/v2 v2.0.2 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect diff --git a/go.sum b/go.sum index 7da87de..e79be9a 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGB cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= -cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= +cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= @@ -65,8 +65,12 @@ cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2b cloud.google.com/go/vision/v2 v2.8.0 h1:W52z1b6LdGI66MVhE70g/NFty9zCYYcjdKuycqmlhtg= cloud.google.com/go/vision/v2 v2.8.0/go.mod h1:ocqDiA2j97pvgogdyhoxiQp2ZkDCyr0HWpicywGGRhU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +firebase.google.com/go/v4 v4.14.0 h1:Tc9jWzMUApUFUA5UUx/HcBeZ+LPjlhG2vNRfWJrcMwU= +firebase.google.com/go/v4 v4.14.0/go.mod h1:pLATyL6xH2o9AMe7rqHdmmOUE/Ph7wcwepIs+uiEKPg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -137,6 +141,11 @@ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godruoyi/go-snowflake v0.0.2 h1:rN9imTkrUJ5ZjuwTOi7kTGQFEZSUI3pwPMzAb7uitk4= +github.com/godruoyi/go-snowflake v0.0.2/go.mod h1:6JXMZzmleLpSK9pYpg4LXTcAz54mdYXTeXUvVks17+4= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -190,6 +199,7 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -396,6 +406,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= @@ -609,6 +620,8 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= +google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/pkg/config/Infrastructure.go b/pkg/config/Infrastructure.go index 7779293..f3f0364 100644 --- a/pkg/config/Infrastructure.go +++ b/pkg/config/Infrastructure.go @@ -5,7 +5,12 @@ type Infrastructure struct { } type GoogleCloud struct { - ProjectID string `yaml:"project_id"` - UseCredentialsFile bool `yaml:"use_credentials_file"` - CredentialsFilePath string `yaml:"credentials_file_path"` + ProjectID string `yaml:"project_id"` + UseCredentialsFile bool `yaml:"use_credentials_file"` + CredentialsFilePath string `yaml:"credentials_file_path"` + Firebase Firebase `yaml:"firebase"` +} + +type Firebase struct { + DatabaseURL string `yaml:"database_url"` } diff --git a/pkg/config/setting.testing.yaml b/pkg/config/setting.testing.yaml new file mode 100644 index 0000000..3f9bf6e --- /dev/null +++ b/pkg/config/setting.testing.yaml @@ -0,0 +1,23 @@ +application: + server: + frontend: + protocol: https:// + domain: shion.pro + port: 443 + backend: + protocol: https:// + domain: shion.pro + port: 1305 + gcs: + bucket_name: sample-bucket-name +infrastructure: + google_cloud: + project_id: some-project-id + use_credentials_file: false + credentials_file_path: /dev/null + firebase: + database_url: "http://localhost:9000" +third_party: +service: + authentication: + jwt_secret: "LEN32_AUTH_CIPHER_KEY_TEST_TOKEN" diff --git a/pkg/fb/main.go b/pkg/fb/main.go new file mode 100644 index 0000000..98042fc --- /dev/null +++ b/pkg/fb/main.go @@ -0,0 +1,56 @@ +package fb + +import ( + "context" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/db" + "fmt" + "google.golang.org/api/option" + "lgtm-gen/pkg/config" + "log" +) + +type Firebase struct { + Client *db.Client +} + +func NewFirebase() (*Firebase, error) { + conf := config.Get() + ctx := context.Background() + + var client *firebase.App + var err error + + fconf := &firebase.Config{ + DatabaseURL: conf.Infrastructure.GoogleCloud.Firebase.DatabaseURL, + } + if conf.Infrastructure.GoogleCloud.UseCredentialsFile { + client, err = firebase.NewApp(ctx, fconf) + if err != nil { + return nil, fmt.Errorf("failed to create Firestore client without credential: %w", err) + } + } else { + opt := option.WithCredentialsFile( + conf.Infrastructure.GoogleCloud.CredentialsFilePath, + ) + client, err = firebase.NewApp(ctx, fconf, opt) + if err != nil { + return nil, fmt.Errorf("failed to create Firebase client with credential: %w", err) + } + } + + c, err := client.Database(ctx) + if err != nil { + log.Fatalln("Error initializing database client:", err) + } + + return &Firebase{ + Client: c, + }, nil +} + +func NewWithClient(client *db.Client) Firebase { + return Firebase{ + Client: client, + } +} diff --git a/pkg/fs/main.go b/pkg/fs/main.go deleted file mode 100644 index bd6f972..0000000 --- a/pkg/fs/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package fs - -import ( - "context" - "fmt" - "lgtm-gen/pkg/config" - - "cloud.google.com/go/firestore" - "google.golang.org/api/option" -) - -type Firestore struct { - Client *firestore.Client -} - -func NewFirestore() (*Firestore, error) { - conf := config.Get() - ctx := context.Background() - - var client *firestore.Client - var err error - if conf.Infrastructure.GoogleCloud.UseCredentialsFile { - client, err = firestore.NewClient(ctx, conf.Infrastructure.GoogleCloud.ProjectID, option.WithCredentialsFile(conf.Infrastructure.GoogleCloud.CredentialsFilePath)) - if err != nil { - return nil, fmt.Errorf("failed to create Firestore client with credentials file: %w", err) - } - } else { - client, err = firestore.NewClient(ctx, conf.Infrastructure.GoogleCloud.ProjectID) - if err != nil { - return nil, fmt.Errorf("failed to create Firestore client: %w", err) - } - } - - return &Firestore{ - Client: client, - }, nil -} diff --git a/pkg/snowflake/snowflake.go b/pkg/snowflake/snowflake.go new file mode 100644 index 0000000..3f8ac0a --- /dev/null +++ b/pkg/snowflake/snowflake.go @@ -0,0 +1,24 @@ +package snowflake + +import ( + "github.com/godruoyi/go-snowflake" + "strconv" +) + +type Snowflake int64 + +func init() { + snowflake.SetMachineID(305) +} + +func NewSnowflake() Snowflake { + return Snowflake(snowflake.ID()) +} + +func (s Snowflake) Int64() int64 { + return int64(s) +} + +func (s Snowflake) String() string { + return strconv.FormatInt(int64(s), 10) +} diff --git a/pkg/snowflake/snowflake_test.go b/pkg/snowflake/snowflake_test.go new file mode 100644 index 0000000..9f67859 --- /dev/null +++ b/pkg/snowflake/snowflake_test.go @@ -0,0 +1,19 @@ +package snowflake + +import ( + "testing" +) + +const TestSize = 1000000 + +func TestSnowflake(t *testing.T) { + var d = make(map[int64]bool, TestSize) + for i := 0; i < TestSize; i++ { + s := NewSnowflake() + _, has := d[s.Int64()] + if has { + t.Errorf("DUPLICATE DETECTED: %v", s) + } + d[s.Int64()] = true + } +} diff --git a/pkg/testutil/firebase.go b/pkg/testutil/firebase.go new file mode 100644 index 0000000..00830b2 --- /dev/null +++ b/pkg/testutil/firebase.go @@ -0,0 +1,44 @@ +package testutil + +import ( + "context" + fb "firebase.google.com/go/v4" + "firebase.google.com/go/v4/db" + fbPkg "lgtm-gen/pkg/fb" + "log" +) + +type FirebaseTest struct { + db *db.Client +} + +func NewFirebaseTest() *FirebaseTest { + ctx := context.Background() + conf := &fb.Config{ + DatabaseURL: "localhost:9000/?ns=testdb", + } + app, err := fb.NewApp(ctx, conf) + if err != nil { + log.Fatalln("Error initializing app:", err) + } + c, err := app.Database(ctx) + if err != nil { + log.Fatalln("Error initializing database client:", err) + } + return &FirebaseTest{ + db: c, + } +} + +func (f FirebaseTest) GetClient() *fbPkg.Firebase { + fdb := fbPkg.NewWithClient(f.db) + return &fdb +} + +func (f FirebaseTest) Reset() { + err := f.db.NewRef("/").Delete(context.Background()) + if err != nil { + log.Println("failed to reset firebase: ", err) + return + } +} diff --git a/svc/cmd/dev/main.go b/svc/cmd/dev/main.go index fc2c1cc..9d8f422 100644 --- a/svc/cmd/dev/main.go +++ b/svc/cmd/dev/main.go @@ -1,10 +1,10 @@ package main import ( - "lgtm-gen/pkg/fs" + "lgtm-gen/pkg/fb" "lgtm-gen/pkg/gcs" "lgtm-gen/pkg/gvision" - infraFs "lgtm-gen/svc/pkg/infra/firestore" + infraFs "lgtm-gen/svc/pkg/infra/fb" infraGcs "lgtm-gen/svc/pkg/infra/gcs" infraGVision "lgtm-gen/svc/pkg/infra/gvision" "log" @@ -49,7 +49,7 @@ func main() { }) }) - f, err := fs.NewFirestore() + fb, err := fb.NewFirebase() if err != nil { log.Fatalf("failed to connect to firestore, err: %v", err) } @@ -65,7 +65,7 @@ func main() { } apiV1 := r.Group("/api/v1") - if err := Implement(apiV1, f, g, gv); err != nil { + if err := Implement(apiV1, fb, g, gv); err != nil { log.Fatalf("Failed to start server...%v", err) return } @@ -76,7 +76,7 @@ func main() { } } -func Implement(rg *gin.RouterGroup, f *fs.Firestore, g *gcs.GCS, gv *gvision.GVision) error { +func Implement(rg *gin.RouterGroup, f *fb.Firebase, g *gcs.GCS, gv *gvision.GVision) error { lgtmHandler := handler.NewLGTMHandler(infraFs.NewLGTMTable(f), infraGcs.NewLGTMBucket(g), infraGVision.NewSafeSearch(gv)) rg.Handle("POST", "/lgtms", lgtmHandler.CreateLGTM()) diff --git a/svc/pkg/application/handler/lgtm.go b/svc/pkg/application/handler/lgtm.go index 5bcc954..b76bac6 100644 --- a/svc/pkg/application/handler/lgtm.go +++ b/svc/pkg/application/handler/lgtm.go @@ -5,19 +5,21 @@ import ( "io" "lgtm-gen/pkg/config" "lgtm-gen/pkg/lgtmgen" + "lgtm-gen/pkg/snowflake" "lgtm-gen/svc/pkg/application/response" "lgtm-gen/svc/pkg/domain" + "lgtm-gen/svc/pkg/domain/model" "log" "net/http" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) type LGTMHandler struct { lgtmTableRepo domain.ILGTMTableRepository lgtmBucketRepo domain.ILGTMBucketRepository safeSearchRepo domain.ISafeSearchRepository + idGen snowflake.Snowflake } func NewLGTMHandler(lgtmTableRepo domain.ILGTMTableRepository, lgtmBucketRepo domain.ILGTMBucketRepository, safeSearchRepo domain.ISafeSearchRepository) *LGTMHandler { @@ -25,6 +27,7 @@ func NewLGTMHandler(lgtmTableRepo domain.ILGTMTableRepository, lgtmBucketRepo do lgtmTableRepo: lgtmTableRepo, lgtmBucketRepo: lgtmBucketRepo, safeSearchRepo: safeSearchRepo, + idGen: snowflake.NewSnowflake(), } } @@ -60,7 +63,7 @@ func (l LGTMHandler) CreateLGTM() gin.HandlerFunc { } // GCSに保存 - id := uuid.New().String() + id := l.idGen.String() err = l.lgtmBucketRepo.Create(id, lgtm) if err != nil { log.Println(err) @@ -71,7 +74,10 @@ func (l LGTMHandler) CreateLGTM() gin.HandlerFunc { // FireStoreにデータを保存 conf := config.Get() url := fmt.Sprintf("https://storage.googleapis.com/%v/%v", conf.Application.GCS.BucketName, id) - err = l.lgtmTableRepo.Create(id, url) + err = l.lgtmTableRepo.Create(model.LGTM{ + ID: id, + Url: url, + }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/svc/pkg/domain/model/lgtm.go b/svc/pkg/domain/model/lgtm.go index 84a4fc1..e318010 100644 --- a/svc/pkg/domain/model/lgtm.go +++ b/svc/pkg/domain/model/lgtm.go @@ -3,7 +3,7 @@ package model import "time" type LGTM struct { - ID string `firestore:"-"` - Url string `firestore:"url"` - CreatedAt time.Time `firestore:"createdAt"` + ID string `json:"-"` + Url string `json:"url"` + CreatedAt time.Time `json:"createdAt"` } diff --git a/svc/pkg/domain/repository.go b/svc/pkg/domain/repository.go index faec183..2a195eb 100644 --- a/svc/pkg/domain/repository.go +++ b/svc/pkg/domain/repository.go @@ -6,7 +6,7 @@ import ( type ILGTMTableRepository interface { List() ([]model.LGTM, error) - Create(id string, url string) error + Create(target model.LGTM) error } type ILGTMBucketRepository interface { diff --git a/svc/pkg/infra/fb/lgtm.go b/svc/pkg/infra/fb/lgtm.go new file mode 100644 index 0000000..6ba464f --- /dev/null +++ b/svc/pkg/infra/fb/lgtm.go @@ -0,0 +1,66 @@ +package infra + +import ( + "context" + "errors" + "fmt" + "lgtm-gen/pkg/fb" + "lgtm-gen/svc/pkg/domain/model" + "time" +) + +const LGTMFolderName = "LGTMs" + +type LGTMTable struct { + f *fb.Firebase + tokyo *time.Location +} + +func NewLGTMTable(f *fb.Firebase) *LGTMTable { + loc, _ := time.LoadLocation("Asia/Tokyo") + return &LGTMTable{ + f: f, + tokyo: loc, + } +} + +// List get list of lgtm images data +func (l LGTMTable) List() ([]model.LGTM, error) { + ctx := context.Background() + // TODO: pagination + qs, err := l.f.Client.NewRef(LGTMFolderName).OrderByChild("createdAt"). + LimitToLast(20).GetOrdered(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query result: %w", err) + } + + results := make([]model.LGTM, 0, len(qs)) + for _, doc := range qs { + var lgtm = model.LGTM{} + if err := doc.Unmarshal(&lgtm); err != nil { + return nil, fmt.Errorf("failed to unmarshal some result: %w", err) + } + lgtm.ID = doc.Key() + results = append(results, lgtm) + } + return results, nil +} + +func (l LGTMTable) Set(target model.LGTM) error { + if target.ID == "" { + return errors.New("id is required for model.LGTM") + } + ctx := context.Background() + if err := l.f.Client.NewRef(LGTMFolderName).Child(target.ID).Set(ctx, target); err != nil { + return fmt.Errorf("failed to set LGTM data, target: %v, err: %w", target, err) + } + return nil +} + +func (l LGTMTable) Create(target model.LGTM) error { + if !target.CreatedAt.IsZero() { + return errors.New("CreatedAt should be Zero value") + } + target.CreatedAt = time.Now().In(l.tokyo) + return l.Set(target) +} diff --git a/svc/pkg/infra/fb/lgtm_test.go b/svc/pkg/infra/fb/lgtm_test.go new file mode 100644 index 0000000..01bb9ab --- /dev/null +++ b/svc/pkg/infra/fb/lgtm_test.go @@ -0,0 +1,54 @@ +package infra + +import ( + "lgtm-gen/pkg/testutil" + "lgtm-gen/svc/pkg/domain/model" + "testing" + "time" +) + +func TestLGTMTable_Set(t *testing.T) { + fb := testutil.NewFirebaseTest() + defer fb.Reset() + client := NewLGTMTable(fb.GetClient()) + testdata := []model.LGTM{ + { + ID: "1", + Url: "https://example.com/img/1", + CreatedAt: time.Now().Add(-2 * time.Hour), + }, + { + ID: "2", + Url: "https://example.com/img/2", + CreatedAt: time.Now().Add(-time.Hour), + }, + { + ID: "3", + Url: "https://example.com/img/3", + CreatedAt: time.Now().Add(-30 * time.Minute), + }, + } + for _, d := range testdata { + if err := client.Set(d); err != nil { + t.Fatalf("failed to set data, err: %v", err) + } + } + results, err := client.List() + if err != nil { + t.Fatalf("failed to list data, err: %v", err) + } + if len(results) != len(testdata) { + t.Fatalf("len(results) got: %d, want: %d", len(results), len(testdata)) + } + for i, d := range testdata { + if d.ID != results[i].ID { + t.Fatalf("results[%d].ID got: %s, want: %s", i, results[i].ID, d.ID) + } + if d.Url != results[i].Url { + t.Fatalf("results[%d].Url got: %s, want: %s", i, results[i].Url, d.Url) + } + if d.CreatedAt != results[i].CreatedAt { + t.Fatalf("results[%d].CreatedAt got: %v, want: %v", i, results[i].CreatedAt, d.CreatedAt) + } + } +} diff --git a/svc/pkg/infra/firestore/lgtm.go b/svc/pkg/infra/firestore/lgtm.go deleted file mode 100644 index c420317..0000000 --- a/svc/pkg/infra/firestore/lgtm.go +++ /dev/null @@ -1,59 +0,0 @@ -package infra - -import ( - "context" - "lgtm-gen/pkg/fs" - "lgtm-gen/svc/pkg/domain/model" - "time" -) - -const LGTMCollectionName = "lgtms" - -type LGTMTable struct { - f *fs.Firestore -} - -func NewLGTMTable(f *fs.Firestore) *LGTMTable { - return &LGTMTable{ - f: f, - } -} - -// List get list of lgtm images data -func (l LGTMTable) List() ([]model.LGTM, error) { - ctx := context.Background() - // TODO: pagination - docs, err := l.f.Client.Collection(LGTMCollectionName).Documents(ctx).GetAll() - if err != nil { - return nil, err - } - - lgtms := make([]model.LGTM, len(docs)) - for i, doc := range docs { - var lgtm = model.LGTM{} - if err := doc.DataTo(&lgtm); err != nil { - return nil, err - } - lgtm.ID = doc.Ref.ID - lgtms[i] = lgtm - } - - return lgtms, nil -} - -// Create add item to firebase -func (l LGTMTable) Create(id string, url string) error { - ctx := context.Background() - loc, err := time.LoadLocation("Asia/Tokyo") - if err != nil { - return err - } - _, err = l.f.Client.Collection(LGTMCollectionName).Doc(id).Set(ctx, map[string]interface{}{ - "createdAt": time.Now().In(loc), - "url": url, - }) - if err != nil { - return err - } - return nil -}